"""Fact Extractor - Extract structured atomic facts from working memory using LLM.""" import json import logging from typing import Optional, List from lifecycle.llm_parse import parse_llm_json_array logger = logging.getLogger(__name__) FACT_SYSTEM_PROMPT = """你是JSON生成器。输入工作记忆,直接提取结构化事实。 严格规则: 1. 禁止输出分析、推理、思考、解释 2. 第一个字符必须是 [,最后一个字符必须是 ] 3. 每条事实:content(50-200字)、importance(0.0-1.0)、category(分类) 4. 最多20条,无值得提取的事实返回 [] 输出格式(只输出JSON,不输出其他任何文字): [{"content":"事实","importance":0.8,"category":"分类"}]""" class FactExtractor: """Extract atomic facts from working memory using LLM.""" def __init__(self, mysql_store, redis_cache, llm_client, embedding_service): self.mysql = mysql_store self.redis = redis_cache self.llm = llm_client self.embedder = embedding_service def extract(self, agent_id: str, team_id: str, max_memories: int = 20, delete_after: bool = False) -> dict: """Extract structured facts from working memory.""" if not self.llm.available: return {"error": "LLM not configured. Set LLM_API_URL, LLM_API_KEY, LLM_MODEL environment variables."} items = self.redis.get_working_memories(agent_id, limit=max_memories) if not items: return {"facts": [], "count": 0} memory_lines = [] for i, item in enumerate(items): if not isinstance(item, dict): logger.warning('Skipping non-dict working memory item: %r', item) continue content = item.get("content", "")[:300] memory_lines.append(f"[{i+1}] {content}") user_prompt = "以下是需要提取事实的工作记忆:\n\n" + "\n".join(memory_lines) try: facts = parse_llm_json_array( self.llm, FACT_SYSTEM_PROMPT, user_prompt, temperature=0.2, context="extract-facts", ) except ValueError as e: return {"error": str(e)} stored = [] for fact in facts: if not isinstance(fact, dict): logger.warning('Skipping non-dict fact from LLM: %r', fact) continue content = fact.get("content", "").strip() if not content or len(content) < 10: continue importance = fact.get("importance", 0.6) if not isinstance(importance, (int, float)) or importance < 0 or importance > 1: importance = 0.6 category = fact.get("category", "") embedding = self.embedder.embed(content) from storage.vector_search import embedding_to_bytes emb_bytes = embedding_to_bytes(embedding) memory_id = self.mysql.add_personal_memory( agent_id=agent_id, team_id=team_id, content=content, embedding=emb_bytes, importance=float(importance), metadata={ "source": "fact_extraction", "category": category, "extracted_from": "working_memory", }, ) stored.append({ "id": memory_id, "content": content, "importance": importance, "category": category, }) if delete_after and stored: self.redis.clear_working_memory(agent_id) logger.info(f"Deleted working memories for {agent_id} after extracting {len(stored)} facts") return {"facts": stored, "count": len(stored)}