Terminal / memory /distiller.py
Baida—-
fix(registry): unterminated string literal line 692 — force re-sync [skip ci]
889ea3f verified
Raw
History Blame Contribute Delete
5.39 kB
"""
distiller.py — S-SESTO-SENSO-V2: Memory Cognitive Compressive.
Sintetizza i log grezzi delle sessioni in lezioni apprese e concetti chiave.
C1-FIX (2026-07-01): chiamata LLM reale implementata — il distillatore produceva
solo placeholder vuoto (lessons: [], patterns: [], facts: []) invece di estrarre
conoscenza reale. Ora chiama ai_client quando disponibile; fallback euristico
se il client è None o se la chiamata fallisce.
"""
import logging
import json
import re
from typing import List, Dict, Any, Optional
from datetime import datetime, timezone
_logger = logging.getLogger("agente_ai.memory.distiller")
_DISTILL_SYSTEM = (
"Sei un Memory Distiller per un agente AI avanzato. "
"Analizza la sessione e restituisci SOLO un oggetto JSON valido, "
"senza markdown, senza spiegazioni. Formato esatto:\n"
'{"lessons":["..."],"patterns":["..."],"facts":["..."],'
'"completed":true|false,"completion_note":"..."}'
)
def _build_distill_prompt(goal: str, cleaned_msgs: List[Dict]) -> str:
transcript = "\n".join(
f"[{m['role'].upper()}] {m['content'][:300]}" for m in cleaned_msgs[-20:]
)
return (
f"OBIETTIVO: {goal}\n\n"
f"TRASCRIZIONE (ultimi {len(cleaned_msgs[-20:])} messaggi):\n{transcript}\n\n"
"Estrai lessons (errori + soluzioni), patterns (architetture fragili o ricorrenti), "
"facts (info tecniche stabili). Rispondi SOLO con il JSON."
)
def _heuristic_distill(messages: List[Dict], goal: str) -> Dict[str, Any]:
"""Fallback euristico — estrae pattern semplici dal testo."""
errors_found = []
facts_found = []
combined = " ".join(m.get("content", "") for m in messages).lower()
if "errore" in combined or "error" in combined or "exception" in combined:
errors_found.append("Errori rilevati nella sessione — dettagli nel transcript.")
if "completato" in combined or "done" in combined or "success" in combined:
facts_found.append("Task marcato come completato nel transcript.")
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"goal": goal,
"message_count": len(messages),
"lessons": errors_found,
"patterns": [],
"facts": facts_found,
"completed": "completato" in combined or "done" in combined,
"completion_note": "Distillazione euristica (LLM non disponibile).",
"source": "heuristic",
}
class MemoryDistiller:
def __init__(self, ai_client=None):
self.ai_client = ai_client
async def distill_session(self, messages: List[Dict[str, Any]], goal: str) -> Dict[str, Any]:
"""
C1-FIX: chiama l'LLM reale per estrarre lezioni/pattern/fatti.
Fallback euristico se ai_client è None o la chiamata fallisce.
"""
if not messages:
return {}
# 1. Pre-processing: tronca contenuti pesanti
cleaned_msgs = []
for msg in messages:
content = msg.get("content", "")
if len(content) > 2000:
content = content[:1000] + "... [TRUNCATED] ..." + content[-400:]
cleaned_msgs.append({"role": msg.get("role", "user"), "content": content})
# 2. Tentativo LLM reale
if self.ai_client is not None:
try:
result = await self._distill_with_llm(cleaned_msgs, goal)
if result:
return result
except Exception as e:
_logger.warning("[Distiller] LLM call failed (%s) — fallback euristico", e)
# 3. Fallback euristico
return _heuristic_distill(cleaned_msgs, goal)
async def _distill_with_llm(
self, cleaned_msgs: List[Dict], goal: str
) -> Optional[Dict[str, Any]]:
"""Chiamata LLM reale via ai_client (qualsiasi provider con interfaccia openai-compat)."""
prompt = _build_distill_prompt(goal, cleaned_msgs)
response = await self.ai_client.chat.completions.create(
model=getattr(self.ai_client, "_distill_model", "llama-3.1-8b-instant"),
messages=[
{"role": "system", "content": _DISTILL_SYSTEM},
{"role": "user", "content": prompt},
],
max_tokens=600,
temperature=0.1,
)
raw = response.choices[0].message.content or ""
# Estrai JSON dalla risposta (può avere testo attorno)
match = re.search(r"\{[\s\S]*\}", raw)
if not match:
_logger.warning("[Distiller] risposta LLM non contiene JSON valido: %.100s", raw)
return None
parsed = json.loads(match.group(0))
parsed.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
parsed.setdefault("goal", goal)
parsed.setdefault("message_count", len(cleaned_msgs))
parsed["source"] = "llm"
_logger.info(
"[Distiller] distillazione LLM completata: %d lessons, %d facts | goal: %.50s",
len(parsed.get("lessons", [])), len(parsed.get("facts", [])), goal,
)
return parsed
def compress_for_long_term(self, distilled_data: Dict[str, Any]) -> str:
"""Converte i dati distillati in stringa ottimizzata per pgvector/semantic memory."""
return json.dumps(distilled_data, ensure_ascii=False)