初始化:记忆系统源代码上传(已脱敏)
- 排除 .env / *.bak / 内部运维文档(README_INTERNAL.html) - config.py 默认密码已替换为占位符 CHANGE_ME_* - init_db.sql 移除生产数据库用户 GRANT 段 - README.html 数据库用户名已脱敏 - 保留:源码 + 公网 API 文档 + 建表 SQL(无授权语句)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# API module
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user