Spaces:
Running
Running
Baida—-
fix(registry): unterminated string literal line 692 — force re-sync [skip ci]
889ea3f verified | """ | |
| 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) | |