Merge pull request #1644 from A-Dawn/dev

为记忆系统补全情节记忆/人物画像/纠错功能,并修复docker的依赖安装问题
This commit is contained in:
SengokuCola
2026-05-06 13:37:30 +08:00
committed by GitHub
10 changed files with 1990 additions and 33 deletions

View File

@@ -197,6 +197,9 @@ def _setup_static_files(app: FastAPI):
@app.get("/{full_path:path}", include_in_schema=False)
async def serve_spa(full_path: str):
if full_path == "api" or full_path.startswith("api/"):
raise HTTPException(status_code=404, detail=t("core.not_found"))
if not full_path or full_path == "/":
response = FileResponse(static_path / "index.html", media_type="text/html")
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"

View File

@@ -9,8 +9,12 @@ from typing import Any, Optional
import tomlkit
from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import BaseModel, Field
from sqlmodel import col, select
from src.A_memorix.host_service import a_memorix_host_service
from src.common.database.database import get_db_session
from src.common.database.database_model import PersonInfo
from src.person_info.person_info import resolve_person_id_for_memory
from src.services.memory_service import MemorySearchResult, memory_service
from src.webui.dependencies import require_auth
@@ -297,22 +301,49 @@ async def _episode_list(
limit: int,
source: str,
person_id: str,
platform: str,
user_id: str,
time_start: float | None,
time_end: float | None,
) -> dict:
return await memory_service.episode_admin(
clean_person_id = str(person_id or "").strip()
if not clean_person_id and str(platform or "").strip() and str(user_id or "").strip():
clean_person_id = resolve_person_id_for_memory(
platform=str(platform or "").strip(),
user_id=str(user_id or "").strip(),
strict_known=False,
)
payload = await memory_service.episode_admin(
action="list",
query=query,
limit=limit,
source=source,
person_id=person_id,
person_id=clean_person_id,
time_start=time_start,
time_end=time_end,
)
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
return payload
items = []
for item in payload["items"]:
if not isinstance(item, dict):
items.append(item)
continue
items.append(_enrich_episode_person_name(item))
payload = dict(payload)
payload["items"] = items
return payload
async def _episode_get(episode_id: str) -> dict:
return await memory_service.episode_admin(action="get", episode_id=episode_id)
payload = await memory_service.episode_admin(action="get", episode_id=episode_id)
if isinstance(payload, dict) and isinstance(payload.get("episode"), dict):
payload = dict(payload)
payload["episode"] = _enrich_episode_person_name(payload["episode"])
return payload
async def _episode_rebuild(payload: EpisodeRebuildRequest) -> dict:
@@ -336,18 +367,143 @@ async def _episode_process_pending(payload: EpisodeProcessPendingRequest) -> dic
)
async def _profile_query(*, person_id: str, person_keyword: str, limit: int, force_refresh: bool) -> dict:
async def _profile_query(
*,
person_id: str,
person_keyword: str,
platform: str,
user_id: str,
limit: int,
force_refresh: bool,
) -> dict:
clean_person_id = str(person_id or "").strip()
if not clean_person_id and str(platform or "").strip() and str(user_id or "").strip():
clean_person_id = resolve_person_id_for_memory(
platform=str(platform or "").strip(),
user_id=str(user_id or "").strip(),
strict_known=False,
)
return await memory_service.profile_admin(
action="query",
person_id=person_id,
person_id=clean_person_id,
person_keyword=person_keyword,
limit=limit,
force_refresh=force_refresh,
)
def _get_person_name_for_person_id(person_id: str) -> str:
clean_person_id = str(person_id or "").strip()
if not clean_person_id:
return ""
try:
with get_db_session(auto_commit=False) as session:
statement = select(PersonInfo.person_name).where(col(PersonInfo.person_id) == clean_person_id).limit(1)
person_name = session.exec(statement).first()
return str(person_name or "").strip()
except Exception:
return ""
def _enrich_episode_person_name(item: dict) -> dict:
enriched = dict(item)
item_person_id = str(enriched.get("person_id", "") or "").strip()
participants = enriched.get("participants")
if not item_person_id and isinstance(participants, list):
for participant in participants:
if isinstance(participant, dict):
candidate = str(participant.get("person_id", "") or participant.get("id", "") or "").strip()
else:
candidate = str(participant or "").strip()
if candidate:
item_person_id = candidate
break
enriched["person_id"] = item_person_id
enriched["person_name"] = _get_person_name_for_person_id(item_person_id)
return enriched
async def _profile_list(limit: int) -> dict:
return await memory_service.profile_admin(action="list", limit=limit)
payload = await memory_service.profile_admin(action="list", limit=limit)
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
return payload
items = []
for item in payload["items"]:
if not isinstance(item, dict):
items.append(item)
continue
enriched = dict(item)
person_id = str(enriched.get("person_id", "") or "").strip()
enriched["person_name"] = _get_person_name_for_person_id(person_id)
items.append(enriched)
payload = dict(payload)
payload["items"] = items
return payload
async def _profile_search(
*,
person_id: str,
person_keyword: str,
platform: str,
user_id: str,
limit: int,
) -> dict:
clean_person_id = str(person_id or "").strip()
if not clean_person_id and str(platform or "").strip() and str(user_id or "").strip():
clean_person_id = resolve_person_id_for_memory(
platform=str(platform or "").strip(),
user_id=str(user_id or "").strip(),
strict_known=False,
)
payload = await _profile_list(max(limit, 200))
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list):
return payload
keyword = str(person_keyword or "").strip().lower()
def _matches(item: dict) -> bool:
if clean_person_id and str(item.get("person_id", "") or "").strip() != clean_person_id:
return False
if not keyword:
return True
override = item.get("manual_override")
override_text = ""
if isinstance(override, dict):
override_text = str(override.get("override_text", "") or override.get("text", "") or "")
elif isinstance(override, str):
override_text = override
haystack = "\n".join(
[
str(item.get("person_id", "") or ""),
str(item.get("person_name", "") or ""),
str(item.get("profile_text", "") or ""),
str(item.get("source_note", "") or ""),
override_text,
]
).lower()
return keyword in haystack
items = [item for item in payload["items"] if isinstance(item, dict) and _matches(item)]
items = items[:limit]
return {
"success": True,
"items": items,
"count": len(items),
"query": {
"person_id": clean_person_id,
"person_keyword": person_keyword,
"platform": platform,
"user_id": user_id,
},
}
async def _profile_set_override(payload: ProfileOverrideRequest) -> dict:
@@ -797,6 +953,8 @@ async def list_memory_episodes(
limit: int = Query(20, ge=1, le=200),
source: str = Query(""),
person_id: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
time_start: float | None = Query(None),
time_end: float | None = Query(None),
):
@@ -805,11 +963,18 @@ async def list_memory_episodes(
limit=limit,
source=source,
person_id=person_id,
platform=platform,
user_id=user_id,
time_start=time_start,
time_end=time_end,
)
@router.get("/episodes/status")
async def get_memory_episode_status(limit: int = Query(20, ge=1, le=200)):
return await _episode_status(limit)
@router.get("/episodes/{episode_id}")
async def get_memory_episode(episode_id: str):
return await _episode_get(episode_id)
@@ -820,11 +985,6 @@ async def rebuild_memory_episodes(payload: EpisodeRebuildRequest):
return await _episode_rebuild(payload)
@router.get("/episodes/status")
async def get_memory_episode_status(limit: int = Query(20, ge=1, le=200)):
return await _episode_status(limit)
@router.post("/episodes/process-pending")
async def process_memory_episode_pending(payload: EpisodeProcessPendingRequest):
return await _episode_process_pending(payload)
@@ -834,12 +994,16 @@ async def process_memory_episode_pending(payload: EpisodeProcessPendingRequest):
async def query_memory_profile(
person_id: str = Query(""),
person_keyword: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
limit: int = Query(12, ge=1, le=100),
force_refresh: bool = Query(False),
):
return await _profile_query(
person_id=person_id,
person_keyword=person_keyword,
platform=platform,
user_id=user_id,
limit=limit,
force_refresh=force_refresh,
)
@@ -850,6 +1014,23 @@ async def list_memory_profiles(limit: int = Query(50, ge=1, le=200)):
return await _profile_list(limit)
@router.get("/profiles/search")
async def search_memory_profiles(
person_id: str = Query(""),
person_keyword: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
limit: int = Query(50, ge=1, le=200),
):
return await _profile_search(
person_id=person_id,
person_keyword=person_keyword,
platform=platform,
user_id=user_id,
limit=limit,
)
@router.post("/profiles/override")
async def set_memory_profile_override(payload: ProfileOverrideRequest):
return await _profile_set_override(payload)
@@ -1269,6 +1450,8 @@ async def compat_list_episodes(
limit: int = Query(20, ge=1, le=200),
source: str = Query(""),
person_id: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
time_start: float | None = Query(None),
time_end: float | None = Query(None),
):
@@ -1277,11 +1460,18 @@ async def compat_list_episodes(
limit=limit,
source=source,
person_id=person_id,
platform=platform,
user_id=user_id,
time_start=time_start,
time_end=time_end,
)
@compat_router.get("/episodes/status")
async def compat_episode_status(limit: int = Query(20, ge=1, le=200)):
return await _episode_status(limit)
@compat_router.get("/episodes/{episode_id}")
async def compat_get_episode(episode_id: str):
return await _episode_get(episode_id)
@@ -1292,11 +1482,6 @@ async def compat_rebuild_episodes(payload: EpisodeRebuildRequest):
return await _episode_rebuild(payload)
@compat_router.get("/episodes/status")
async def compat_episode_status(limit: int = Query(20, ge=1, le=200)):
return await _episode_status(limit)
@compat_router.post("/episodes/process_pending")
async def compat_process_episode_pending(payload: EpisodeProcessPendingRequest):
return await _episode_process_pending(payload)
@@ -1306,12 +1491,16 @@ async def compat_process_episode_pending(payload: EpisodeProcessPendingRequest):
async def compat_profile_query(
person_id: str = Query(""),
person_keyword: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
limit: int = Query(12, ge=1, le=100),
force_refresh: bool = Query(False),
):
return await _profile_query(
person_id=person_id,
person_keyword=person_keyword,
platform=platform,
user_id=user_id,
limit=limit,
force_refresh=force_refresh,
)
@@ -1322,6 +1511,23 @@ async def compat_profile_list(limit: int = Query(50, ge=1, le=200)):
return await _profile_list(limit)
@compat_router.get("/person_profile/search")
async def compat_profile_search(
person_id: str = Query(""),
person_keyword: str = Query(""),
platform: str = Query(""),
user_id: str = Query(""),
limit: int = Query(50, ge=1, le=200),
):
return await _profile_search(
person_id=person_id,
person_keyword=person_keyword,
platform=platform,
user_id=user_id,
limit=limit,
)
@compat_router.post("/person_profile/override")
async def compat_set_profile_override(payload: ProfileOverrideRequest):
return await _profile_set_override(payload)