feat(harness): derive a 3-5 keyword session name (YAKE, no LLM)

New sessions get a concise Italian-keyword `name` instead of the truncated
question. `tht session new` (when no --name is given) derives it via a new
`_extract_name` helper using YAKE (pure-Python, unsupervised, Italian, no LLM),
dropping generic query verbs and keeping the top keywords in reading order;
falls back to `_summarize` if YAKE is unavailable. `create_session` core keeps
its `name=None` default — the policy lives at the CLI layer.

TDD: tests/test_session_name.py (unit + CliRunner integration). Full harness
suite 269 passed; ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:54:54 +02:00
co-authored by Claude Opus 4.8
parent d27afd0896
commit c12bdcd257
4 changed files with 129 additions and 2 deletions
+44
View File
@@ -3,6 +3,8 @@ import shutil
from datetime import UTC, datetime
from pathlib import Path
import yake
from tht.config import DatabaseConfig
from tht.session.models import SessionManifest
from tht.textutil import slugify
@@ -10,6 +12,17 @@ from tht.textutil import slugify
MANIFEST = "session_manifest.yaml"
MAX_SLUG_CHARS = 40
MAX_SUMMARY_CHARS = 120
MAX_NAME_CHARS = 60
MAX_NAME_WORDS = 5
# Generic query verbs/words dropped from a derived session name (lowercased compare);
# they carry no content and would just crowd out the salient keywords.
_STOPNAME = {
"crea", "creare", "elenca", "elencare", "elencami", "mostra", "mostrami", "mostrare",
"dammi", "dai", "trova", "trovami", "cerca", "cercami", "voglio", "vorrei", "fammi",
"quanti", "quante", "quanto", "quanta", "conta", "numero", "lista", "elenco", "vedi",
"visualizza", "ottieni", "restituisci", "calcola", "esponi", "riporta", "fornisci",
}
class SessionError(Exception):
@@ -29,6 +42,37 @@ def _summarize(question: str) -> str:
return first[:MAX_SUMMARY_CHARS]
def _extract_name(question: str) -> str:
"""Nome-sintesi (3-5 parole-chiave) della domanda per la sidebar, senza LLM (YAKE).
Le parole-chiave restano nella lingua della domanda (è contenuto, non chrome UI).
Ripiega su `_summarize` se YAKE non è disponibile o non estrae nulla di utile."""
q = (question or "").strip()
if not q:
return ""
try:
extractor = yake.KeywordExtractor(lan="it", n=1, top=8, dedupLim=0.9)
ranked = [k for k, _ in extractor.extract_keywords(q)]
except Exception:
return _summarize(question)
seen: set[str] = set()
picked: list[str] = []
for k in ranked:
kl = k.lower()
if kl in _STOPNAME or kl in seen:
continue
seen.add(kl)
picked.append(k)
if len(picked) >= MAX_NAME_WORDS:
break
if not picked:
return _summarize(question)
ql = q.lower()
picked.sort(key=lambda k: ql.find(k.lower())) # natural reading order
name = " ".join(picked)
return (name[0].upper() + name[1:])[:MAX_NAME_CHARS]
def render_question_md(question: str, assumptions: list[str] | None = None) -> str:
"""Rende question.md in modo deterministico: domanda + assunzioni opzionali.