From 76d3d420c480105fb9bc4d67fafed54b7184cf9a Mon Sep 17 00:00:00 2001 From: frf12 Date: Thu, 10 Sep 2026 03:46:41 +0800 Subject: [PATCH 1/5] POWE-120: add Topic Memory dashboard --- src/powercontext/server/dashboard/api.py | 23 ++++++++++++ src/powercontext/server/dashboard/content.py | 37 +++++++++++++++++++ .../server/dashboard/labels.en.json | 13 +++++++ src/powercontext/server/dashboard/labels.json | 13 +++++++ src/powercontext/server/dashboard/routes.py | 10 ++++- .../templates/components/navigation.html | 2 +- .../server/dashboard/templates/macros.html | 2 +- .../server/dashboard/templates/topics.html | 27 ++++++++++++++ tests/test_dashboard.py | 8 ++++ 9 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/powercontext/server/dashboard/templates/topics.html diff --git a/src/powercontext/server/dashboard/api.py b/src/powercontext/server/dashboard/api.py index 6bed0ecf5..f7e26a8ec 100644 --- a/src/powercontext/server/dashboard/api.py +++ b/src/powercontext/server/dashboard/api.py @@ -50,6 +50,7 @@ class DashboardAPI: """Use an in-process HTTP transport with the incoming user's credentials.""" def __init__(self, request: Request) -> None: + self.app = request.app headers = authentication_headers(request.scope) headers = {key: value for key, value in headers.items() if key in {"authorization", "cookie"}} self.client = httpx.AsyncClient( @@ -143,3 +144,25 @@ async def load(item: dict[str, Any]) -> dict[str, Any]: "items": list(await asyncio.gather(*(load(item) for item in page["items"]))), "next_cursor": page["next_cursor"], } + + async def topic_memory_search(self, scope: str, query: str) -> dict[str, Any]: + return await self.read("/v1/topic-memory/search", {"scope_id": scope, "query": query}) + + async def topic_memory_get(self, scope: str, artifact: dict[str, Any]) -> dict[str, Any]: + return await self.read("/v1/topic-memory/get", {"scope_id": scope, "artifact": artifact}) + + async def topic_memory_browse(self, scope: str, *, limit: int = 50) -> dict[str, Any]: + application = getattr(self.app.state, "application", None) + if application is None: + raise ReadError(503, "service_unavailable") + items = await application.topic_memory.for_scope(scope).browse(limit=limit) + return { + "items": [ + { + **item.model_dump(mode="json"), + "artifact": item.artifact_ref.model_dump(mode="json"), + } + for item in items + ], + "next_cursor": None, + } diff --git a/src/powercontext/server/dashboard/content.py b/src/powercontext/server/dashboard/content.py index 6e9dff421..0c5b66c78 100644 --- a/src/powercontext/server/dashboard/content.py +++ b/src/powercontext/server/dashboard/content.py @@ -159,5 +159,42 @@ async def load_content(api: DashboardAPI, request: Request, ctx: dict[str, Any]) await load_collection(api, request, ctx, ctx["method_kind"]) elif page == "usage": await load_stats(api, ctx) + elif page == "topics": + await load_topics(api, ctx) elif page in RECORDS: await load_record(api, request, ctx) + + +async def load_topics(api: DashboardAPI, ctx: dict[str, Any]) -> None: + """Load Topic Memory browse/search results and an exact selected revision.""" + query = ctx["artifact_query"] + if query: + try: + result = await api.topic_memory_search(ctx["scope"], query) + for hit in result["hits"]: + try: + record = await api.topic_memory_get(ctx["scope"], hit["artifact"]) + ctx["data"]["topic_memory"].append({**hit, **record, "is_current": True}) + except ReadError as error: + ctx["errors"].setdefault("topic_memory", error) + except ReadError as error: + ctx["errors"]["topic_memory"] = error + else: + try: + ctx["data"]["topic_memory"] = (await api.topic_memory_browse(ctx["scope"]))["items"] + except ReadError as error: + ctx["errors"]["topic_memory"] = error + if ctx["topic_artifact"] and ctx["topic_revision"]: + try: + ctx["data"]["topic_memory_selected"] = await api.topic_memory_get( + ctx["scope"], + { + "family": "topic-memory", + "artifact_id": ctx["topic_artifact"], + "revision": int(ctx["topic_revision"]), + }, + ) + except ValueError: + ctx["errors"]["topic_memory_selected"] = ReadError(422, "invalid_request") + except ReadError as error: + ctx["errors"]["topic_memory_selected"] = error diff --git a/src/powercontext/server/dashboard/labels.en.json b/src/powercontext/server/dashboard/labels.en.json index 4d355fa4f..b1490a225 100644 --- a/src/powercontext/server/dashboard/labels.en.json +++ b/src/powercontext/server/dashboard/labels.en.json @@ -4,6 +4,19 @@ "notes": "Memories", "methods": "Experiences & Skills", "usage": "Usage", + "topics": "Topic Memory", + "topic_memory": "Topic Memory", + "topics_subtitle": "Browse durable Topic Memory and inspect an exact revision", + "topic_inventory": "Topic Memory inventory", + "select_topic": "Select a topic to inspect its exact revision", + "select_topic_hint": "Full detail, publication state and source references appear here.", + "current_revision": "Current revision", + "full_detail": "Full detail", + "source_references": "Source references", + "no_sources": "No direct Source references", + "search_topic_memory": "Search Topic Memory", + "topic_memory_empty": "No matching topics", + "topic_memory_empty_body": "No active Topic Memory matched this query.", "experience": "Experience", "skill": "Skill", "other_scopes": "Other scopes", diff --git a/src/powercontext/server/dashboard/labels.json b/src/powercontext/server/dashboard/labels.json index 6faa4f7b5..1f554026f 100644 --- a/src/powercontext/server/dashboard/labels.json +++ b/src/powercontext/server/dashboard/labels.json @@ -4,6 +4,19 @@ "notes": "记忆", "methods": "经验与技能", "usage": "用量", + "topics": "主题记忆", + "topic_memory": "主题记忆", + "topics_subtitle": "浏览持久主题记忆并查看精确版本", + "topic_inventory": "主题记忆列表", + "select_topic": "请选择一个主题以查看其精确版本", + "select_topic_hint": "这里会显示完整详情、发布状态和来源引用。", + "current_revision": "当前版本", + "full_detail": "完整详情", + "source_references": "来源引用", + "no_sources": "没有直接来源引用", + "search_topic_memory": "搜索主题记忆", + "topic_memory_empty": "没有匹配的主题", + "topic_memory_empty_body": "当前内容范围没有匹配此查询的主题记忆。", "experience": "经验", "skill": "技能", "other_scopes": "其他范围", diff --git a/src/powercontext/server/dashboard/routes.py b/src/powercontext/server/dashboard/routes.py index 833919885..6afcff23b 100644 --- a/src/powercontext/server/dashboard/routes.py +++ b/src/powercontext/server/dashboard/routes.py @@ -32,7 +32,7 @@ ROOT = Path(__file__).parent LABELS = CATALOGS["zh"] PARENTS = {"handoff-detail": "handoff", "experience": "methods", "skill": "methods"} -PAGES = {"home", "handoff", "notes", "methods", "usage", "entry", *RECORDS} +PAGES = {"home", "handoff", "notes", "methods", "topics", "usage", "entry", *RECORDS} ENV = Environment( loader=FileSystemLoader(ROOT / "templates"), autoescape=select_autoescape(), undefined=StrictUndefined ) @@ -80,6 +80,9 @@ def link(destination: str | None = None, fragment: str = "", **params: Any) -> s "experience_cursor", "skill_cursor", "q", + "topic_q", + "topic_artifact", + "topic_revision", "notes_page", "skill_page", "experience_history", @@ -129,6 +132,9 @@ def initial_context(request: Request, page: str) -> dict[str, Any]: "period": request.query_params.get("period", "7d"), "method_kind": method_kind, "search_query": request.query_params.get("q", "").strip() or None, + "artifact_query": request.query_params.get("topic_q", "").strip() or None, + "topic_artifact": request.query_params.get("topic_artifact"), + "topic_revision": request.query_params.get("topic_revision"), "search_limited": False, "data": { "title": "PowerContext", @@ -137,6 +143,8 @@ def initial_context(request: Request, page: str) -> dict[str, Any]: "handoff": None, "experience": None, "skill": None, + "topic_memory": [], + "topic_memory_selected": None, }, "scopes": [], "scope_descriptor": None, diff --git a/src/powercontext/server/dashboard/templates/components/navigation.html b/src/powercontext/server/dashboard/templates/components/navigation.html index 0c3fcc4ce..e50cb5c99 100644 --- a/src/powercontext/server/dashboard/templates/components/navigation.html +++ b/src/powercontext/server/dashboard/templates/components/navigation.html @@ -34,7 +34,7 @@ {% if errors.get('scopes') %}

{{ t.scope_list_unavailable }}

{% endif %}