"""Flask API Routes for Memory System""" import logging from flask import Blueprint, request, current_app, g logger = logging.getLogger(__name__) api = Blueprint("api", __name__) def _get_ms(): """Get MemorySystem instance from app context.""" return current_app.memory_system def _ok(data=None, status=200): return current_app.json_response({"ok": True, "data": data}, status=status) def _err(msg, status=400, code=None): body = {"ok": False, "error": msg} if code: body["code"] = code return current_app.json_response(body, status=status) # ── Health ─────────────────────────────────────────────────────── @api.route("/health", methods=["GET"]) def health(): return _ok({"status": "ok"}) # ── Team management ───────────────────────────────────────────── @api.route("/teams", methods=["POST"]) def create_team(): d = request.json or {} team_id = d.get("team_id") name = d.get("name") if not team_id or not name: return _err("team_id and name required", code="VALIDATION_ERROR") try: team = _get_ms().create_team(team_id, name, d.get("description", ""), d.get("config")) return _ok(team) except Exception as e: return _err(str(e)) @api.route("/teams/", methods=["GET"]) def get_team(team_id): team = _get_ms().get_team(team_id) if not team: return _err("Team not found", 404, code="TEAM_NOT_FOUND") return _ok(team) @api.route("/teams/", methods=["DELETE"]) def delete_team(team_id): _get_ms().delete_team(team_id) return _ok() # ── Agent management ──────────────────────────────────────────── @api.route("/agents", methods=["POST"]) def create_agent(): d = request.json or {} agent_id = d.get("agent_id") name = d.get("name") if not agent_id or not name: return _err("agent_id and name required", code="VALIDATION_ERROR") try: agent = _get_ms().create_agent(agent_id, g.team_id, name, d.get("role", "")) return _ok(agent) except Exception as e: return _err(str(e)) @api.route("/agents/", methods=["GET"]) def list_agents(team_id): if team_id != g.team_id: return _err("Access denied: cannot view other team's agents", 403, code="FORBIDDEN_CROSS_TEAM") agents = _get_ms().get_agents_by_team(team_id) return _ok(agents) @api.route("/agents//", methods=["GET"]) def get_agent(team_id, agent_id): if team_id != g.team_id: return _err("Access denied: cannot view other team's agents", 403, code="FORBIDDEN_CROSS_TEAM") agent = _get_ms().get_agent(agent_id) if not agent: return _err("Agent not found", 404, code="AGENT_NOT_FOUND") return _ok(agent) @api.route("/agents//", methods=["DELETE"]) def delete_agent(team_id, agent_id): if team_id != g.team_id: return _err("Access denied: cannot delete other team's agents", 403, code="FORBIDDEN_CROSS_TEAM") _get_ms().delete_agent(agent_id) return _ok() # ── Personal memories ─────────────────────────────────────────── @api.route("/memories/personal", methods=["POST"]) def add_personal_memory(): d = request.json or {} agent_id = d.get("agent_id") content = d.get("content") if not agent_id or not content: return _err("agent_id and content required", code="VALIDATION_ERROR") try: mem = _get_ms().add_personal_memory( agent_id, content, importance=d.get("importance", 0.5), metadata=d.get("metadata"), enable_dedup=d.get("enable_dedup", True), dedup_threshold=d.get("dedup_threshold", 0.85), ) status = 200 if mem.get("dedup_skipped") else 201 return _ok(mem, status) except Exception as e: return _err(str(e)) @api.route("/memories/personal/search", methods=["POST"]) def search_personal_memories(): d = request.json or {} agent_id = d.get("agent_id") query = d.get("query") if not agent_id: return _err("agent_id required") # Empty query: fallback to recent memories if not query: limit = d.get("limit", 10) results = _get_ms().get_recent_personal_memories(agent_id, limit) return _ok(results) try: results = _get_ms().search_personal_memories( agent_id, query, limit=d.get("limit", 10), min_score=d.get("min_score", 0.3), max_chars_per_memory=d.get("max_chars_per_memory", 0), max_total_chars=d.get("max_total_chars", 0), ) return _ok(results) except Exception as e: return _err(str(e)) @api.route("/memories/personal/recent/", methods=["GET"]) def get_recent_personal_memories(agent_id): limit = request.args.get("limit", 20, type=int) results = _get_ms().get_recent_personal_memories(agent_id, limit) return _ok(results) @api.route("/memories/personal/", methods=["GET"]) def get_personal_memory(memory_id): mem = _get_ms().get_personal_memory(memory_id) if not mem: return _err("Memory not found", 404, code="MEMORY_NOT_FOUND") return _ok(mem) @api.route("/memories/personal/", methods=["PUT"]) def update_personal_memory(memory_id): d = request.json or {} try: ok = _get_ms().update_personal_memory( memory_id, content=d.get("content"), importance=d.get("importance"), metadata=d.get("metadata"), ) if ok: mem = _get_ms().get_personal_memory(memory_id) return _ok(mem) return _err("Memory not found", 404, code="MEMORY_NOT_FOUND") except Exception as e: return _err(str(e)) @api.route("/memories/personal/", methods=["DELETE"]) def delete_personal_memory(memory_id): _get_ms().delete_personal_memory(memory_id) return _ok() # ── Working memory ────────────────────────────────────────────── @api.route("/memories/working", methods=["POST"]) def add_working_memory(): d = request.json or {} agent_id = d.get("agent_id") content = d.get("content") if not agent_id or not content: return _err("agent_id and content required", code="VALIDATION_ERROR") item = _get_ms().add_working_memory(agent_id, content, ttl=d.get("ttl")) compress_result = _get_ms().check_auto_compress(agent_id) if compress_result: item["auto_compressed"] = compress_result return _ok(item, 201) @api.route("/memories/working/", methods=["GET"]) def get_working_memories(agent_id): limit = request.args.get("limit", 20, type=int) results = _get_ms().get_working_memories(agent_id, limit) return _ok(results) @api.route("/memories/working/", methods=["DELETE"]) def clear_working_memory(agent_id): _get_ms().clear_working_memory(agent_id) return _ok() # ── Team memories ─────────────────────────────────────────────── @api.route("/memories/team", methods=["POST"]) def add_team_memory(): d = request.json or {} content = d.get("content") if not content: return _err("content required", code="VALIDATION_ERROR") try: mem = _get_ms().add_team_memory( g.team_id, content, importance=d.get("importance", 0.5), category=d.get("category", "general"), metadata=d.get("metadata"), enable_dedup=d.get("enable_dedup", True), dedup_threshold=d.get("dedup_threshold", 0.85), ) status = 200 if mem.get("dedup_skipped") else 201 return _ok(mem, status) except Exception as e: return _err(str(e)) @api.route("/memories/team/search", methods=["POST"]) def search_team_memories(): d = request.json or {} query = d.get("query") # Empty query: fallback to recent memories if not query: limit = d.get("limit", 10) results = _get_ms().get_recent_team_memories(g.team_id, limit) return _ok(results) try: results = _get_ms().search_team_memories( g.team_id, query, limit=d.get("limit", 10), min_score=d.get("min_score", 0.3), max_chars_per_memory=d.get("max_chars_per_memory", 0), max_total_chars=d.get("max_total_chars", 0), ) return _ok(results) except Exception as e: return _err(str(e)) @api.route("/memories/team/recent", methods=["GET"]) def get_recent_team_memories(): limit = request.args.get("limit", 20, type=int) results = _get_ms().get_recent_team_memories(g.team_id, limit) return _ok(results) @api.route("/memories/team/", methods=["GET"]) def get_team_memory(memory_id): mem = _get_ms().get_team_memory(memory_id) if not mem: return _err("Memory not found", 404, code="MEMORY_NOT_FOUND") return _ok(mem) @api.route("/memories/team/", methods=["PUT"]) def update_team_memory(memory_id): d = request.json or {} try: ok = _get_ms().update_team_memory( memory_id, content=d.get("content"), importance=d.get("importance"), ) if ok: mem = _get_ms().get_team_memory(memory_id) return _ok(mem) return _err("Memory not found", 404, code="MEMORY_NOT_FOUND") except Exception as e: return _err(str(e)) @api.route("/memories/team/", methods=["DELETE"]) def delete_team_memory(memory_id): _get_ms().delete_team_memory(memory_id) return _ok() # ── Lifecycle ─────────────────────────────────────────────────── @api.route("/lifecycle/compress", methods=["POST"]) def compress_working_memories(): d = request.json or {} agent_id = d.get("agent_id") if not agent_id: return _err("agent_id required", code="VALIDATION_ERROR") try: result = _get_ms().compress_working_memories( agent_id, max_items=d.get("target_count", 5), ) return _ok(result) except Exception as e: return _err(str(e)) @api.route("/lifecycle/cleanup", methods=["POST"]) def cleanup_memories(): d = request.json or {} result = _get_ms().cleanup_memories( team_id=d.get("team_id"), max_age_days=d.get("max_age_days", 90), min_importance=d.get("min_importance", 0.2), ) return _ok(result) # ── Stats ─────────────────────────────────────────────────────── @api.route("/stats", methods=["GET"]) def get_stats(): team_id = request.args.get("team_id") if team_id and team_id != g.team_id: return _err("Access denied: cannot view other team's stats", 403, code="FORBIDDEN_CROSS_TEAM") stats = _get_ms().get_stats(g.team_id) return _ok(stats) # -- Pipeline Automation -- @api.route("/lifecycle/auto-config", methods=["GET"]) def get_pipeline_config(): config = _get_ms().get_pipeline_config(g.team_id) if config: return _ok(config.to_dict()) return _ok({"team_id": g.team_id, "compress_every_n": 0, "cleanup_idle_days": 0, "enabled": False}) @api.route("/lifecycle/auto-config", methods=["POST"]) def set_pipeline_config(): d = request.json or {} try: config = _get_ms().set_pipeline_config( team_id=g.team_id, compress_every_n=d.get("compress_every_n"), compress_target_count=d.get("compress_target_count"), cleanup_idle_days=d.get("cleanup_idle_days"), cleanup_min_importance=d.get("cleanup_min_importance"), enabled=d.get("enabled"), warmup_max_memories=d.get("warmup_max_memories", 0), warmup_compress_every_n=d.get("warmup_compress_every_n", 1), ) return _ok(config.to_dict()) except Exception as e: return _err(str(e)) @api.route("/lifecycle/auto-run", methods=["POST"]) def run_pipeline_cleanup(): d = request.json or {} team_id = d.get("team_id", g.team_id) result = _get_ms().run_auto_cleanup(team_id) if result: return _ok(result) return _ok({"message": "No cleanup rules configured"}) # -- Scenario Aggregation -- @api.route("/lifecycle/aggregate-scenes", methods=["POST"]) def aggregate_scenes(): """Aggregate personal memories into scenario blocks using LLM. Optional body: {"max_memories": 30} """ d = request.json or {} agent_id = d.get("agent_id", "") if not agent_id: return _err("agent_id required", code="VALIDATION_ERROR") try: result = _get_ms().aggregate_scenarios( agent_id=agent_id, team_id=g.team_id, max_memories=d.get("max_memories", 50), ) except Exception as e: logger.exception("aggregate-scenes failed") return _err(str(e), 500, code="INTERNAL_ERROR") if "error" in result: return _err(result["error"]) return _ok(result) @api.route("/lifecycle/scenarios/", methods=["GET"]) def get_scenes(agent_id): """Get stored scenarios for an agent.""" scenarios = _get_ms().get_scenarios(agent_id) return _ok(scenarios) @api.route("/lifecycle/scenarios/", methods=["DELETE"]) def delete_scene(scenario_id): """Delete a scenario.""" ok = _get_ms().delete_scenario(scenario_id, g.team_id) if ok: return _ok({"deleted": scenario_id}) return _err("Scenario not found or access denied", 404, code="NOT_FOUND") # -- Persona Generation -- @api.route("/lifecycle/generate-persona", methods=["POST"]) def generate_persona(): """Generate user persona from memories and scenarios using LLM. Body: {"agent_id": "...", "max_items": 30} """ d = request.json or {} agent_id = d.get("agent_id", "") if not agent_id: return _err("agent_id required", code="VALIDATION_ERROR") try: result = _get_ms().generate_persona( agent_id=agent_id, team_id=g.team_id, max_items=d.get("max_items", 30), ) except Exception as e: logger.exception("generate-persona failed") return _err(str(e), 500, code="INTERNAL_ERROR") if "error" in result: return _err(result["error"]) return _ok(result) @api.route("/lifecycle/persona/", methods=["GET"]) def get_persona(agent_id): """Get the latest persona for an agent.""" persona = _get_ms().get_persona(agent_id) if persona: return _ok(persona) return _ok({"message": "No persona found", "agent_id": agent_id}) @api.route("/lifecycle/persona/", methods=["DELETE"]) def delete_persona(persona_id): """Delete a persona.""" ok = _get_ms().delete_persona(persona_id, g.team_id) if ok: return _ok({"deleted": persona_id}) return _err("Persona not found or access denied", 404, code="NOT_FOUND") # -- Atomic Fact Extraction -- @api.route("/lifecycle/extract-facts", methods=["POST"]) def extract_facts(): """Extract structured atomic facts from working memory using LLM. Body: {"agent_id": "...", "max_memories": 20, "delete_after": false} Returns: {"facts": [{"id": "...", "content": "...", "importance": 0.8, "category": "..."}]} """ d = request.json or {} agent_id = d.get("agent_id", "") if not agent_id: return _err("agent_id required", code="VALIDATION_ERROR") try: result = _get_ms().extract_facts( agent_id=agent_id, team_id=g.team_id, max_memories=d.get("max_memories", 20), delete_after=d.get("delete_after", False), ) except Exception as e: logger.exception("extract-facts failed") return _err(str(e), 500, code="INTERNAL_ERROR") if "error" in result: return _err(result["error"]) return _ok(result)