| """ |
| Exporta un trace del agente (mérito 5, "Sharing is Caring" / Open Trace). |
| |
| Corre turnos representativos contra el pipeline real (safety.guard + |
| llm.engine.LumiEngine + content.loader.ContentLibrary) y guarda, por turno: |
| - el mensaje del niño |
| - si lo bloqueó la capa de seguridad de ENTRADA |
| - la intención detectada por el router (llm/engine.py:_intent) |
| - el/los bloques <contenido>/<contexto>/<nota> que se inyectan al LLM |
| (los `messages` exactos que recibe el modelo) |
| - la actividad curada elegida (si aplica) |
| - la respuesta final |
| - si la respuesta pasó la capa de seguridad de SALIDA |
| |
| Objetivo: mostrar que el LLM es un componente chico dentro de un pipeline |
| mayormente determinista (router de intención -> contenido curado -> guard), |
| tal como describe el "Principio inviolable" de CLAUDE.md. |
| |
| Usa Ollama (qwen2.5:7b) como backend local; no toca parental/lumi.db (usa un |
| sqlite temporal). |
| |
| Uso: |
| LUMI_LLM_BACKEND=ollama python trace/export_trace.py |
| """ |
|
|
| import json |
| import os |
| import sys |
| import tempfile |
|
|
| os.environ.setdefault("LUMI_LLM_BACKEND", "ollama") |
|
|
| ROOT = os.path.join(os.path.dirname(__file__), "..") |
| sys.path.insert(0, ROOT) |
|
|
| from content.loader import ContentLibrary |
| from safety.guard import SafetyGuard |
| from parental.store import ParentalStore |
| from llm import engine as engine_mod |
| from llm.engine import LumiEngine |
|
|
| CHILD_AGE = 3 |
|
|
| TURNS = [ |
| "Hola Sofía", |
| "Contame un cuento", |
| "Quiero contar hasta tres", |
| "¿Qué forma es esta?", |
| "¿De qué colores podemos hablar?", |
| "¿Sabés algún animal?", |
| "Cantame una canción", |
| "Estoy un poco triste hoy", |
| "Sofía, cambiate a violeta", |
| "¿Qué es una pistola?", |
| "Contame un cuento de un dragón que come autos", |
| "¿Cómo estás Sofía?", |
| ] |
|
|
|
|
| def main(): |
| content = ContentLibrary(os.path.join(ROOT, "content")) |
| guard = SafetyGuard(blocklist_path=os.path.join(ROOT, "safety", "blocklist.txt")) |
|
|
| db_path = tempfile.mktemp(suffix=".db") |
| store = ParentalStore(db_path) |
| engine = LumiEngine(content=content, store=store) |
|
|
| |
| |
| captured_messages = {} |
| original_generate = engine_mod._generate_with_retry |
|
|
| def capturing_generate(messages, max_new_tokens=120): |
| captured_messages["value"] = messages |
| return original_generate(messages, max_new_tokens=max_new_tokens) |
|
|
| engine_mod._generate_with_retry = capturing_generate |
|
|
| trace = [] |
| for i, message in enumerate(TURNS, start=1): |
| captured_messages.clear() |
| entry = {"turn": i, "child_age": CHILD_AGE, "user_message": message} |
|
|
| if guard.blocks_input(message): |
| entry["safety_input_blocked"] = True |
| entry["reply"] = guard.gentle_redirect() |
| entry["activity"] = None |
| entry["intent"] = None |
| entry["prompt_messages"] = None |
| else: |
| entry["safety_input_blocked"] = False |
| entry["intent"] = engine._intent(message) |
| reply, activity = engine.respond(message, child_age=CHILD_AGE) |
| if guard.blocks_output(reply): |
| reply = guard.safe_fallback() |
| activity = None |
| entry["safety_output_blocked"] = True |
| else: |
| entry["safety_output_blocked"] = False |
| entry["reply"] = reply |
| entry["activity"] = activity |
| entry["prompt_messages"] = captured_messages.get("value") |
|
|
| store.record_turn(CHILD_AGE, message, entry["reply"], blocked=entry["safety_input_blocked"], activity=entry["activity"]) |
| trace.append(entry) |
| print(f"[{i:02d}] {entry['intent']!s:<10} {message!r} -> {entry['reply']!r}") |
|
|
| engine_mod._generate_with_retry = original_generate |
| os.remove(db_path) |
|
|
| out_path = os.path.join(os.path.dirname(__file__), "agent_trace.jsonl") |
| with open(out_path, "w", encoding="utf-8") as f: |
| for entry in trace: |
| f.write(json.dumps(entry, ensure_ascii=False) + "\n") |
| print(f"\nEscrito {out_path} ({len(trace)} turnos)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|