""" 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)