Add memory system session context API
This commit is contained in:
54
README.md
54
README.md
@ -151,6 +151,7 @@ http://127.0.0.1:1934/memory-system
|
|||||||
| `POST` | `/messages` | 写入一轮或半轮会话消息 | 需要 |
|
| `POST` | `/messages` | 写入一轮或半轮会话消息 | 需要 |
|
||||||
| `POST` | `/sessions/{session_id}/commit` | 提交会话,触发 OpenViking commit 和 EverOS flush | 需要 |
|
| `POST` | `/sessions/{session_id}/commit` | 提交会话,触发 OpenViking commit 和 EverOS flush | 需要 |
|
||||||
| `POST` | `/sessions/{session_id}/extract` | 立即触发 OpenViking extract | 需要 |
|
| `POST` | `/sessions/{session_id}/extract` | 立即触发 OpenViking extract | 需要 |
|
||||||
|
| `GET/POST` | `/sessions/{session_id}/context` | 查询 OpenViking 会话上下文,并用同一 query 搜索 EverOS 记忆 | 需要 |
|
||||||
| `GET` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 |
|
| `GET` | `/openviking/tasks/{task_id}` | 查询 OpenViking 后台任务状态 | 需要 |
|
||||||
| `POST` | `/search` | 同时搜索 OpenViking 和 EverOS 记忆 | 需要 |
|
| `POST` | `/search` | 同时搜索 OpenViking 和 EverOS 记忆 | 需要 |
|
||||||
| `GET` | `/users/{user_id}/profile` | 查询 EverOS profile | 需要 |
|
| `GET` | `/users/{user_id}/profile` | 查询 EverOS profile | 需要 |
|
||||||
@ -284,6 +285,59 @@ curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/extract
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `GET/POST /sessions/{session_id}/context`
|
||||||
|
|
||||||
|
查询一次会话的当前上下文。网关会调用 OpenViking:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/sessions/{session_id}/context
|
||||||
|
Authorization: Bearer <user_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
同时用同一个 `query` 调用 EverOS `/api/v1/memories/search`,返回相关 episodic/profile/raw message 记忆。适合在回答用户问题前,把“当前 session 工作记忆”和“EverOS 相关记忆”一起取回。
|
||||||
|
|
||||||
|
路径参数:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `session_id` | string | 要查询的会话 ID |
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必需 | 说明 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `user_id` | string | 是 | 用户 ID |
|
||||||
|
| `user_key` | string | 是 | `/users` 返回的 user key |
|
||||||
|
| `query` | string | 是 | 用于 EverOS search 的查询文本 |
|
||||||
|
| `limit` | int | 否 | EverOS 记忆返回条数,默认 10,范围 1 到 100 |
|
||||||
|
|
||||||
|
POST 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST http://127.0.0.1:1934/memory-system/sessions/sessionA1/context \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user_id": "userA",
|
||||||
|
"user_key": "'"$USER_KEY"'",
|
||||||
|
"query": "我喜欢喝什么?",
|
||||||
|
"limit": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
GET 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "http://127.0.0.1:1934/memory-system/sessions/sessionA1/context?user_id=userA&user_key=$USER_KEY&query=我喜欢喝什么?&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| `context` | OpenViking `result`,包含 `latest_archive_overview`、`pre_archive_abstracts`、`messages`、`stats` |
|
||||||
|
| `items` | EverOS 搜索命中的精简记忆结果,含 `source_backend: "everos"` |
|
||||||
|
| `backends` | 两个后端的精简诊断信息,不重复返回完整 OpenViking context 或 EverOS `original_data` |
|
||||||
|
|
||||||
### `GET /openviking/tasks/{task_id}`
|
### `GET /openviking/tasks/{task_id}`
|
||||||
|
|
||||||
查询 OpenViking 后台任务状态,例如 commit 返回的任务。
|
查询 OpenViking 后台任务状态,例如 commit 返回的任务。
|
||||||
|
|||||||
@ -4,7 +4,13 @@ from __future__ import annotations
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
from .auth import verify_api_key
|
from .auth import verify_api_key
|
||||||
from .schemas import MessageIngestRequest, SearchRequest, SessionUserRequest, UserCreateRequest
|
from .schemas import (
|
||||||
|
MessageIngestRequest,
|
||||||
|
SearchRequest,
|
||||||
|
SessionContextRequest,
|
||||||
|
SessionUserRequest,
|
||||||
|
UserCreateRequest,
|
||||||
|
)
|
||||||
from .service import MemorySystemService
|
from .service import MemorySystemService
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +76,34 @@ async def extract_session(
|
|||||||
raise user_auth_error(exc) from exc
|
raise user_auth_error(exc) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sessions/{session_id}/context")
|
||||||
|
async def get_session_context(
|
||||||
|
session_id: str,
|
||||||
|
request: SessionContextRequest,
|
||||||
|
service: MemorySystemService = Depends(get_service),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return await service.get_session_context(session_id, request)
|
||||||
|
except PermissionError as exc:
|
||||||
|
raise user_auth_error(exc) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions/{session_id}/context")
|
||||||
|
async def get_session_context_from_query(
|
||||||
|
session_id: str,
|
||||||
|
user_id: str = Query(min_length=1),
|
||||||
|
user_key: str = Query(min_length=1),
|
||||||
|
query: str = Query(min_length=1),
|
||||||
|
limit: int = Query(default=10, ge=1, le=100),
|
||||||
|
service: MemorySystemService = Depends(get_service),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
request = SessionContextRequest(user_id=user_id, user_key=user_key, query=query, limit=limit)
|
||||||
|
return await service.get_session_context(session_id, request)
|
||||||
|
except PermissionError as exc:
|
||||||
|
raise user_auth_error(exc) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get("/openviking/tasks/{task_id}")
|
@router.get("/openviking/tasks/{task_id}")
|
||||||
async def get_openviking_task(
|
async def get_openviking_task(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
|
|||||||
@ -180,6 +180,12 @@ class OpenVikingMemorySystemClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
async def get_session_context(self, credential: OpenVikingCredential | str, session_id: str) -> dict[str, Any]:
|
||||||
|
async with self._credential_client(credential) as client:
|
||||||
|
response = await client.get(f"/api/v1/sessions/{session_id}/context")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
def _credential_client(self, credential: OpenVikingCredential | str) -> httpx.AsyncClient:
|
def _credential_client(self, credential: OpenVikingCredential | str) -> httpx.AsyncClient:
|
||||||
if isinstance(credential, str):
|
if isinstance(credential, str):
|
||||||
return self._client(credential)
|
return self._client(credential)
|
||||||
|
|||||||
@ -33,6 +33,13 @@ class SearchRequest(BaseModel):
|
|||||||
limit: int = Field(default=10, ge=1, le=100)
|
limit: int = Field(default=10, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionContextRequest(BaseModel):
|
||||||
|
user_id: str = Field(min_length=1)
|
||||||
|
user_key: str = Field(min_length=1)
|
||||||
|
query: str = Field(min_length=1)
|
||||||
|
limit: int = Field(default=10, ge=1, le=100)
|
||||||
|
|
||||||
|
|
||||||
class BackendStatus(BaseModel):
|
class BackendStatus(BaseModel):
|
||||||
status: OperationStatus
|
status: OperationStatus
|
||||||
result: Any = None
|
result: Any = None
|
||||||
@ -71,6 +78,13 @@ class SearchResponse(BaseModel):
|
|||||||
backends: dict[str, BackendStatus]
|
backends: dict[str, BackendStatus]
|
||||||
|
|
||||||
|
|
||||||
|
class SessionContextResponse(BaseModel):
|
||||||
|
status: OperationStatus
|
||||||
|
context: dict[str, Any] | None = None
|
||||||
|
items: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
backends: dict[str, BackendStatus]
|
||||||
|
|
||||||
|
|
||||||
class ProfileResponse(BaseModel):
|
class ProfileResponse(BaseModel):
|
||||||
status: OperationStatus
|
status: OperationStatus
|
||||||
profile: Any = None
|
profile: Any = None
|
||||||
|
|||||||
@ -15,6 +15,8 @@ from .schemas import (
|
|||||||
ProfileResponse,
|
ProfileResponse,
|
||||||
SearchRequest,
|
SearchRequest,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
SessionContextRequest,
|
||||||
|
SessionContextResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -124,6 +126,40 @@ class MemorySystemService:
|
|||||||
backends=compact_backends,
|
backends=compact_backends,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_session_context(self, session_id: str, request: SessionContextRequest) -> SessionContextResponse:
|
||||||
|
credential = self.openviking.credential_for_user(
|
||||||
|
request.user_id,
|
||||||
|
request.user_key,
|
||||||
|
agent_id=session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def read_openviking_context() -> dict[str, Any]:
|
||||||
|
return await self.openviking.get_session_context(credential, session_id)
|
||||||
|
|
||||||
|
async def search_everos() -> dict[str, Any]:
|
||||||
|
return await self.everos.search(
|
||||||
|
request.user_id,
|
||||||
|
session_id,
|
||||||
|
request.query,
|
||||||
|
"hybrid",
|
||||||
|
request.limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
backends = await self._run_backends(openviking=read_openviking_context, everos=search_everos)
|
||||||
|
backends = self._remove_vectors_from_backends(backends)
|
||||||
|
context = self._context_from_openviking_result(backends["openviking"].result)
|
||||||
|
items = (
|
||||||
|
self._items_from_backend_result("everos", backends["everos"].result)[: request.limit]
|
||||||
|
if backends["everos"].status == "success"
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
return SessionContextResponse(
|
||||||
|
status=self._aggregate_status(backends),
|
||||||
|
context=context,
|
||||||
|
items=items,
|
||||||
|
backends=self._compact_session_context_backends(backends),
|
||||||
|
)
|
||||||
|
|
||||||
async def get_profile(self, user_id: str) -> ProfileResponse:
|
async def get_profile(self, user_id: str) -> ProfileResponse:
|
||||||
backends = {"everos": await self._capture(lambda: self.everos.get_profile(user_id))}
|
backends = {"everos": await self._capture(lambda: self.everos.get_profile(user_id))}
|
||||||
profile = backends["everos"].result if backends["everos"].status == "success" else None
|
profile = backends["everos"].result if backends["everos"].status == "success" else None
|
||||||
@ -260,6 +296,33 @@ class MemorySystemService:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _context_from_openviking_result(self, result: Any) -> dict[str, Any] | None:
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return None
|
||||||
|
data = result.get("result") if isinstance(result.get("result"), dict) else result
|
||||||
|
return data if isinstance(data, dict) else None
|
||||||
|
|
||||||
|
def _compact_session_context_backends(self, backends: dict[str, BackendStatus]) -> dict[str, BackendStatus]:
|
||||||
|
return {
|
||||||
|
name: backend.model_copy(update={"result": self._compact_session_context_backend_result(name, backend.result)})
|
||||||
|
for name, backend in backends.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compact_session_context_backend_result(self, backend_name: str, result: Any) -> Any:
|
||||||
|
if backend_name == "openviking":
|
||||||
|
data = self._context_from_openviking_result(result)
|
||||||
|
if data is None:
|
||||||
|
return result
|
||||||
|
compact = {
|
||||||
|
"status": result.get("status") if isinstance(result, dict) else None,
|
||||||
|
"estimatedTokens": data.get("estimatedTokens"),
|
||||||
|
"stats": data.get("stats"),
|
||||||
|
"has_latest_archive_overview": bool(data.get("latest_archive_overview")),
|
||||||
|
"message_count": len(data.get("messages") or []) if isinstance(data.get("messages"), list) else 0,
|
||||||
|
}
|
||||||
|
return {key: value for key, value in compact.items() if value is not None}
|
||||||
|
return self._compact_backend_result(backend_name, result)
|
||||||
|
|
||||||
def _remove_vectors_from_backends(self, backends: dict[str, BackendStatus]) -> dict[str, BackendStatus]:
|
def _remove_vectors_from_backends(self, backends: dict[str, BackendStatus]) -> dict[str, BackendStatus]:
|
||||||
return {
|
return {
|
||||||
name: backend.model_copy(update={"result": self._remove_vectors(backend.result)})
|
name: backend.model_copy(update={"result": self._remove_vectors(backend.result)})
|
||||||
|
|||||||
@ -59,6 +59,7 @@ Base path: `/memory-system`
|
|||||||
| `POST` | `/messages` | Write user/assistant messages to backends | Yes |
|
| `POST` | `/messages` | Write user/assistant messages to backends | Yes |
|
||||||
| `POST` | `/sessions/{session_id}/commit` | Commit OpenViking session and flush EverOS | Yes |
|
| `POST` | `/sessions/{session_id}/commit` | Commit OpenViking session and flush EverOS | Yes |
|
||||||
| `POST` | `/sessions/{session_id}/extract` | Trigger OpenViking extract only | Yes |
|
| `POST` | `/sessions/{session_id}/extract` | Trigger OpenViking extract only | Yes |
|
||||||
|
| `GET/POST` | `/sessions/{session_id}/context` | Read OpenViking session context plus EverOS recall | Yes |
|
||||||
| `GET` | `/openviking/tasks/{task_id}` | Poll OpenViking task status | Yes |
|
| `GET` | `/openviking/tasks/{task_id}` | Poll OpenViking task status | Yes |
|
||||||
| `POST` | `/search` | Search OpenViking and EverOS | Yes |
|
| `POST` | `/search` | Search OpenViking and EverOS | Yes |
|
||||||
| `GET` | `/users/{user_id}/profile` | Read EverOS profile | Yes |
|
| `GET` | `/users/{user_id}/profile` | Read EverOS profile | Yes |
|
||||||
@ -127,6 +128,25 @@ curl -sS -X POST "$BASE/memory-system/search" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Session context:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST "$BASE/memory-system/sessions/sessionA1/context" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user_id": "userA",
|
||||||
|
"user_key": "'"$USER_KEY"'",
|
||||||
|
"query": "我喜欢喝什么?",
|
||||||
|
"limit": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
GET with query parameters is also supported:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE/memory-system/sessions/sessionA1/context?user_id=userA&user_key=$USER_KEY&query=我喜欢喝什么?&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
## Response Handling
|
## Response Handling
|
||||||
|
|
||||||
Top-level `status` is one of:
|
Top-level `status` is one of:
|
||||||
@ -137,6 +157,8 @@ Top-level `status` is one of:
|
|||||||
|
|
||||||
Search responses include merged `items` and compact backend diagnostics under `backends`. Keep `source_backend` when using results. Fields named `vector` are stripped from returned payloads, and the raw EverOS `original_data` blob is not returned by search anymore.
|
Search responses include merged `items` and compact backend diagnostics under `backends`. Keep `source_backend` when using results. Fields named `vector` are stripped from returned payloads, and the raw EverOS `original_data` blob is not returned by search anymore.
|
||||||
|
|
||||||
|
Session context responses include OpenViking context under `context`, EverOS recall under `items`, and compact backend diagnostics under `backends`.
|
||||||
|
|
||||||
## Common Mistakes
|
## Common Mistakes
|
||||||
|
|
||||||
- Calling `/messages` before `/users`.
|
- Calling `/messages` before `/users`.
|
||||||
|
|||||||
@ -72,6 +72,68 @@ curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/memory-system/sessions/<SESSION_ID>/ext
|
|||||||
-d '{"user_id": "<USER_ID>", "user_key": "<USER_KEY>"}'
|
-d '{"user_id": "<USER_ID>", "user_key": "<USER_KEY>"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Session Context
|
||||||
|
|
||||||
|
Use this when the caller needs the OpenViking working-memory context for one session plus related EverOS recall for the same user/session.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST <MEMORY_SYSTEM_BASE_URL>/memory-system/sessions/<SESSION_ID>/context \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user_id": "<USER_ID>",
|
||||||
|
"user_key": "<USER_KEY>",
|
||||||
|
"query": "我喜欢喝什么?",
|
||||||
|
"limit": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent GET form:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "<MEMORY_SYSTEM_BASE_URL>/memory-system/sessions/<SESSION_ID>/context?user_id=<USER_ID>&user_key=<USER_KEY>&query=我喜欢喝什么?&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"context": {
|
||||||
|
"latest_archive_overview": "# Working Memory\n...",
|
||||||
|
"pre_archive_abstracts": [],
|
||||||
|
"messages": [],
|
||||||
|
"estimatedTokens": 342,
|
||||||
|
"stats": {"totalArchives": 3}
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"source_backend": "everos",
|
||||||
|
"memory_type": "episode",
|
||||||
|
"id": "episode-1",
|
||||||
|
"summary": "userB 在对话中表示自己喜欢拿铁。",
|
||||||
|
"score": 0.72
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backends": {
|
||||||
|
"openviking": {
|
||||||
|
"status": "success",
|
||||||
|
"result": {
|
||||||
|
"status": "ok",
|
||||||
|
"estimatedTokens": 342,
|
||||||
|
"has_latest_archive_overview": true,
|
||||||
|
"message_count": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"everos": {
|
||||||
|
"status": "success",
|
||||||
|
"result": {
|
||||||
|
"counts": {"episodes": 1, "profiles": 0, "raw_messages": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Search
|
## Search
|
||||||
|
|
||||||
Without LLM planning:
|
Without LLM planning:
|
||||||
|
|||||||
@ -255,6 +255,49 @@ def test_openviking_search_uses_session_target_uri():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_openviking_get_session_context_uses_user_key_auth():
|
||||||
|
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||||
|
calls = []
|
||||||
|
responses = [
|
||||||
|
FakeResponse(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"result": {
|
||||||
|
"latest_archive_overview": "# Working Memory",
|
||||||
|
"messages": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
client._client = lambda api_key, extra_headers=None: FakeAsyncClient( # type: ignore[method-assign]
|
||||||
|
calls,
|
||||||
|
responses,
|
||||||
|
api_key,
|
||||||
|
extra_headers or {},
|
||||||
|
)
|
||||||
|
credential = client.user_credential("tom-key", "tom", agent_id="sess-1")
|
||||||
|
|
||||||
|
result = asyncio.run(client.get_session_context(credential, "sess-1"))
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "ok",
|
||||||
|
"result": {
|
||||||
|
"latest_archive_overview": "# Working Memory",
|
||||||
|
"messages": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert calls == [
|
||||||
|
(
|
||||||
|
"get",
|
||||||
|
"tom-key",
|
||||||
|
{},
|
||||||
|
"/api/v1/sessions/sess-1/context",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_openviking_commit_keeps_no_recent_live_messages():
|
def test_openviking_commit_keeps_no_recent_live_messages():
|
||||||
client = OpenVikingMemorySystemClient(store=FakeStore())
|
client = OpenVikingMemorySystemClient(store=FakeStore())
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
@ -4,6 +4,14 @@ def test_memory_system_server_exposes_routes():
|
|||||||
paths = {route.path for route in app.routes}
|
paths = {route.path for route in app.routes}
|
||||||
assert "/memory-system/users" in paths
|
assert "/memory-system/users" in paths
|
||||||
assert "/memory-system/messages" in paths
|
assert "/memory-system/messages" in paths
|
||||||
|
assert "/memory-system/sessions/{session_id}/context" in paths
|
||||||
|
context_methods = {
|
||||||
|
method
|
||||||
|
for route in app.routes
|
||||||
|
if getattr(route, "path", "") == "/memory-system/sessions/{session_id}/context"
|
||||||
|
for method in getattr(route, "methods", set())
|
||||||
|
}
|
||||||
|
assert {"GET", "POST"} <= context_methods
|
||||||
assert "/memory-system/search" in paths
|
assert "/memory-system/search" in paths
|
||||||
assert "/memory-system/users/{user_id}/profile" in paths
|
assert "/memory-system/users/{user_id}/profile" in paths
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from memory_system_api.schemas import MessageIngestRequest, SearchRequest
|
from memory_system_api.schemas import MessageIngestRequest, SearchRequest, SessionContextRequest
|
||||||
from memory_system_api.service import MemorySystemService
|
from memory_system_api.service import MemorySystemService
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +44,19 @@ class FakeOpenViking:
|
|||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
return {"items": [{"source": "openviking-search"}]}
|
return {"items": [{"source": "openviking-search"}]}
|
||||||
|
|
||||||
|
async def get_session_context(self, user_key: str, session_id: str) -> dict:
|
||||||
|
self.calls.append(("get_session_context", user_key, session_id))
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"result": {
|
||||||
|
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
|
||||||
|
"pre_archive_abstracts": [],
|
||||||
|
"messages": [],
|
||||||
|
"estimatedTokens": 42,
|
||||||
|
"stats": {"totalArchives": 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
async def commit_session(self, user_key: str, session_id: str) -> dict:
|
async def commit_session(self, user_key: str, session_id: str) -> dict:
|
||||||
self.calls.append(("commit_session", user_key, session_id))
|
self.calls.append(("commit_session", user_key, session_id))
|
||||||
return {"status": "ok", "result": {"task_id": "task-1", "archive_uri": "archive-1"}}
|
return {"status": "ok", "result": {"task_id": "task-1", "archive_uri": "archive-1"}}
|
||||||
@ -203,6 +216,43 @@ def test_search_returns_compact_items_and_backend_diagnostics_without_duplicate_
|
|||||||
assert not _has_key(response.backends["everos"].result, "original_data")
|
assert not _has_key(response.backends["everos"].result, "original_data")
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_context_combines_openviking_context_and_everos_search_items():
|
||||||
|
openviking = FakeOpenViking()
|
||||||
|
everos = FakeEverOSVerbose()
|
||||||
|
service = MemorySystemService(openviking=openviking, everos=everos)
|
||||||
|
|
||||||
|
response = asyncio.run(
|
||||||
|
service.get_session_context(
|
||||||
|
"sess-1",
|
||||||
|
SessionContextRequest(user_id="tom", user_key="tom-key", query="我喜欢喝什么?", limit=5),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status == "success"
|
||||||
|
assert response.context == {
|
||||||
|
"latest_archive_overview": "# Working Memory\nUser likes coffee.",
|
||||||
|
"pre_archive_abstracts": [],
|
||||||
|
"messages": [],
|
||||||
|
"estimatedTokens": 42,
|
||||||
|
"stats": {"totalArchives": 1},
|
||||||
|
}
|
||||||
|
assert response.items == [
|
||||||
|
{
|
||||||
|
"source_backend": "everos",
|
||||||
|
"memory_type": "episode",
|
||||||
|
"id": "episode-1",
|
||||||
|
"user_id": "tom",
|
||||||
|
"session_id": "sess-1",
|
||||||
|
"timestamp": "2026-05-22T07:50:51.750000Z",
|
||||||
|
"summary": "userB 在对话中表示自己喜欢拿铁。",
|
||||||
|
"score": 0.72,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert ("credential_for_user", "tom", "tom-key", "sess-1") in openviking.calls
|
||||||
|
assert ("get_session_context", "key-tom", "sess-1") in openviking.calls
|
||||||
|
assert ("search", "tom", "sess-1", "我喜欢喝什么?", "hybrid", 5) in everos.calls
|
||||||
|
|
||||||
|
|
||||||
def _has_key(value, key: str) -> bool:
|
def _has_key(value, key: str) -> bool:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return key in value or any(_has_key(item, key) for item in value.values())
|
return key in value or any(_has_key(item, key) for item in value.values())
|
||||||
|
|||||||
Reference in New Issue
Block a user