# Local Agent V2 — Piano Implementativo > Riferimento architetturale: [`local_agent_v2_mem.md`](local_agent_v2_mem.md) --- ## Panoramica Il Local Agent V2 sostituisce il flusso a 3 call LLM (classification + processing separati) con un'architettura a 2 fasi: 1. **Detect + Preprocess** (Python puro, zero LLM) — identifica il tipo di contenuto e lo pulisce 2. **Single LLM call** (classify + extract + create) — una sola call agentiva con tool calling ### Langfuse: Scoring + Prompt Management (hot-swap) Ogni step include un test set con eval che inviano score a Langfuse. I **prompt sono gestiti da Langfuse Prompt Management** — modificabili dalla UI senza toccare codice. Ogni score è collegato alla **versione esatta del prompt** che lo ha prodotto, permettendo confronto A/B tra versioni. **Workflow iterativo:** 1. Scrivi/modifica il prompt nella UI di Langfuse (es. `unified_processing` v3) 2. Lancia gli eval: `pytest tests/test_agent_runner_v2.py -k eval` 3. Vedi in Langfuse: prompt v3 → score 0.6 4. Modifica il prompt → v4 5. Ri-lancia gli eval → prompt v4 → score 0.9 6. Promuovi v4 a `production` label **Prompt Langfuse da creare (con fallback hardcoded nel codice):** | Nome Langfuse | Usato in | Descrizione | |---|---|---| | `unified_processing` | Step 2 (runner) | Prompt unico: classify + extract + create | | `journey_system_v2` | Step 4 (journey) | Journey chatbot → produce AgentConfig JSON | **Pattern di scoring con prompt version linking:** ```python from app.core.langfuse_client import get_langfuse, get_prompt_or_fallback def run_eval_with_prompt(prompt_name: str, fallback: str, eval_name: str, run_fn): """Esegue un eval collegando score ↔ prompt version.""" lf = get_langfuse() template, prompt_obj = get_prompt_or_fallback(prompt_name, fallback) # Crea trace per l'eval trace = lf.trace(name=f"eval-{eval_name}") if lf else None # Esegui la call LLM dentro una generation linkata al prompt if lf and trace: with lf.start_as_current_observation( as_type="generation", name=eval_name, prompt=prompt_obj, # ← linka alla versione esatta del prompt trace_id=trace.id, ) as gen: result, score = run_fn(template) gen.update(output=str(result)) else: result, score = run_fn(template) # Score collegato al trace → visibile per prompt version in Langfuse if lf and trace: lf.score( trace_id=trace.id, name=eval_name, value=score, data_type="NUMERIC", ) lf.flush() return result, score ``` **In Langfuse vedrai:** ``` Prompt: unified_processing ├── v3 (2026-04-05) → avg score: 0.62 (12 evals) ├── v4 (2026-04-07) → avg score: 0.85 (12 evals) ← production └── v5 (2026-04-08) → avg score: 0.91 (12 evals) ← candidate ``` --- ## Step 1 — Preprocessor: email HTML handler ✅ DONE ## Step 3 — Model e schema: `prompt_template` → `agent_config` ✅ DONE Aggiunto in parallelo a Step 2 come prerequisito: - `app/schemas.py`: `ContentTypeConfig`, `AgentConfig` - `app/models.py`: `agent_config: JSON` (nullable, accanto a `prompt_template`) - `alembic/versions/a3b9c0d1e2f3_add_agent_config_to_local_agents.py` ## Step 2 — Refactor `agent_runner.py`: nuovo flusso per file ✅ DONE **File da creare:** - `app/core/preprocessors/__init__.py` — registry, detect, dispatch - `app/core/preprocessors/base.py` — dataclass `PreprocessResult`, classe base - `app/core/preprocessors/email_html.py` — BeautifulSoup handler **Cosa fa:** - `detect_content_type(filename, raw_content) -> str` — heuristic basata su extension + pattern nel contenuto - `preprocess(content_type, raw_content) -> PreprocessResult` — dispatch al handler corretto - `PreprocessResult`: `{ content_type, clean_text, metadata: {subject, from, to, date, ...} }` **Handler `email_html`:** - Strip `