Files
zqf b1f93588f1 初始化:记忆系统源代码上传(已脱敏)
- 排除 .env / *.bak / 内部运维文档(README_INTERNAL.html)
- config.py 默认密码已替换为占位符 CHANGE_ME_*
- init_db.sql 移除生产数据库用户 GRANT 段
- README.html 数据库用户名已脱敏
- 保留:源码 + 公网 API 文档 + 建表 SQL(无授权语句)
2026-08-03 04:53:48 +08:00

490 lines
16 KiB
Python

"""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/<team_id>", 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/<team_id>", 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/<team_id>", 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/<team_id>/<agent_id>", 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/<team_id>/<agent_id>", 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/<agent_id>", 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/<memory_id>", 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/<memory_id>", 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/<memory_id>", 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/<agent_id>", 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/<agent_id>", 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/<memory_id>", 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/<memory_id>", 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/<memory_id>", 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/<agent_id>", 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/<scenario_id>", 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/<agent_id>", 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/<persona_id>", 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)