Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/en/api/05-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,93 @@ If the archive does not exist, is incomplete, or does not belong to the session,

---

### preview_memory_extraction()

Preview which memories would be extracted from the current live session without writing any memory files.

This endpoint is intended for debugging and observability:
- inspect the candidate memories produced by the extraction LLM
- verify category distribution before calling `commit_session()`
- inspect the archive summary preview that would be generated for the current messages

This endpoint returns:
- `session_id`: the target session ID
- `message_count`: how many live messages are currently in the session
- `estimated_message_tokens`: estimated token count of current live messages
- `latest_archive_overview`: overview of the latest completed archive, if any
- `archive_summary_preview`: summary preview for the current live messages
- `counts_by_category`: candidate counts grouped by memory category
- `candidates`: extracted candidate memories, not yet deduplicated or persisted

**Parameters**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| session_id | str | Yes | - | Session ID |

**Python SDK (Embedded / HTTP)**

```python
preview = await client.preview_memory_extraction("a1b2c3d4")
print(preview["counts_by_category"])
print(preview["archive_summary_preview"])
print(preview["candidates"][0]["abstract"])
```

**HTTP API**

```
POST /api/v1/sessions/{session_id}/extract-preview
```

```bash
curl -X POST "http://localhost:1933/api/v1/sessions/a1b2c3d4/extract-preview" \
-H "X-API-Key: your-key"
```

**Response**

```json
{
"status": "ok",
"result": {
"session_id": "a1b2c3d4",
"message_count": 2,
"estimated_message_tokens": 21,
"latest_archive_overview": "",
"archive_summary_preview": "# Session Summary\n\n**Overview**: The user talked about Wangcai and coffee preferences.",
"counts_by_category": {
"entities": 1,
"preferences": 1,
"total": 2
},
"candidates": [
{
"category": "entities",
"abstract": "The user's dog is named Wangcai.",
"overview": "The user mentioned a dog named Wangcai.",
"content": "The user said their dog is named Wangcai.",
"language": "zh-CN"
},
{
"category": "preferences",
"abstract": "The user prefers pour-over coffee.",
"overview": "The user mentioned a preference for pour-over coffee.",
"content": "The user said they like pour-over coffee.",
"language": "zh-CN"
}
]
}
}
```

Notes:
- This endpoint does **not** write any memory files.
- Returned candidates are raw extraction outputs before deduplication and merge.
- When the session has no live messages, `candidates` is empty and `archive_summary_preview` is an empty string.

---

### delete_session()

Delete a session.
Expand Down
87 changes: 87 additions & 0 deletions docs/zh/api/05-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,93 @@ ov session get-session-archive a1b2c3d4 archive_002

---

### preview_memory_extraction()

预览当前 live session 会抽取出哪些 memory,但**不会**写入任何 memory 文件。

这个接口主要用于调试和可观察性:
- 查看 memory extraction LLM 实际产出的候选记忆
- 在调用 `commit_session()` 前先确认分类分布是否合理
- 查看当前 live messages 会生成什么 archive summary 预览

该接口返回:
- `session_id`:目标会话 ID
- `message_count`:当前 session 里 live message 数量
- `estimated_message_tokens`:当前 live messages 的估算 token 数
- `latest_archive_overview`:最近一个已完成 archive 的 overview(如果存在)
- `archive_summary_preview`:当前 live messages 对应的 summary 预览
- `counts_by_category`:按 memory category 聚合的候选数量
- `candidates`:抽取出的候选记忆,尚未去重,也未落盘

**参数**

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| session_id | str | 是 | - | 会话 ID |

**Python SDK (Embedded / HTTP)**

```python
preview = await client.preview_memory_extraction("a1b2c3d4")
print(preview["counts_by_category"])
print(preview["archive_summary_preview"])
print(preview["candidates"][0]["abstract"])
```

**HTTP API**

```
POST /api/v1/sessions/{session_id}/extract-preview
```

```bash
curl -X POST "http://localhost:1933/api/v1/sessions/a1b2c3d4/extract-preview" \
-H "X-API-Key: your-key"
```

**响应**

```json
{
"status": "ok",
"result": {
"session_id": "a1b2c3d4",
"message_count": 2,
"estimated_message_tokens": 21,
"latest_archive_overview": "",
"archive_summary_preview": "# Session Summary\n\n**Overview**: 用户提到了旺财和手冲咖啡偏好。",
"counts_by_category": {
"entities": 1,
"preferences": 1,
"total": 2
},
"candidates": [
{
"category": "entities",
"abstract": "用户有一只叫旺财的狗。",
"overview": "用户提到自己的狗叫旺财。",
"content": "用户在对话中说明自己的狗叫旺财。",
"language": "zh-CN"
},
{
"category": "preferences",
"abstract": "用户偏好手冲咖啡。",
"overview": "用户表达了对手冲咖啡的偏好。",
"content": "用户提到自己喜欢手冲咖啡。",
"language": "zh-CN"
}
]
}
}
```

说明:
- 这个接口**不会**写入任何 memory 文件。
- 返回的 `candidates` 是去重、合并、落盘之前的原始抽取结果。
- 当 session 里没有 live messages 时,`candidates` 为空,`archive_summary_preview` 为空字符串。

---

### delete_session()

删除会话。
Expand Down
5 changes: 5 additions & 0 deletions openviking/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[st
await self._ensure_initialized()
return await self._client.get_session_archive(session_id, archive_id)

async def preview_memory_extraction(self, session_id: str) -> Dict[str, Any]:
"""Preview extracted memories for a session without persisting them."""
await self._ensure_initialized()
return await self._client.preview_memory_extraction(session_id)

async def delete_session(self, session_id: str) -> None:
"""Delete a session."""
await self._ensure_initialized()
Expand Down
5 changes: 5 additions & 0 deletions openviking/client/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[st
result = await session.get_session_archive(archive_id)
return _to_jsonable(result)

async def preview_memory_extraction(self, session_id: str) -> Dict[str, Any]:
"""Preview extracted memories for a session without persisting them."""
result = await self._service.sessions.preview_extract(session_id, self._ctx)
return _to_jsonable(result)

async def delete_session(self, session_id: str) -> None:
"""Delete a session."""
await self._service.sessions.delete(session_id, self._ctx)
Expand Down
12 changes: 11 additions & 1 deletion openviking/server/routers/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""Sessions endpoints for OpenViking HTTP Server."""

import logging
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional

from fastapi import APIRouter, Depends, Path, Query
Expand Down Expand Up @@ -230,6 +229,17 @@ async def extract_session(
return Response(status="ok", result=_to_jsonable(result))


@router.post("/{session_id}/extract-preview")
async def preview_extract_session(
session_id: str = Path(..., description="Session ID"),
_ctx: RequestContext = Depends(get_request_context),
):
"""Preview extracted memories for a session without persisting them."""
service = get_service()
result = await service.sessions.preview_extract(session_id, _ctx)
return Response(status="ok", result=_to_jsonable(result))


@router.post("/{session_id}/messages")
async def add_message(
request: AddMessageRequest,
Expand Down
80 changes: 79 additions & 1 deletion openviking/service/session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
Provides session management operations: session, sessions, add_message, commit, delete.
"""

from collections import defaultdict
from typing import Any, Dict, List, Optional

from openviking.server.identity import RequestContext
from openviking.service.task_tracker import get_task_tracker
from openviking.session import Session
from openviking.session import Session, ToolSkillCandidateMemory
from openviking.session.compressor import SessionCompressor
from openviking.storage import VikingDBManager
from openviking.storage.viking_fs import VikingFS
Expand Down Expand Up @@ -280,3 +281,80 @@ async def extract(self, session_id: str, ctx: RequestContext) -> List[Any]:
except Exception:
pass
return memories

@staticmethod
def _serialize_preview_candidate(candidate: Any) -> Dict[str, Any]:
"""Convert a candidate-memory dataclass into a stable preview payload."""
category = getattr(candidate, "category", "")
category_value = getattr(category, "value", category) or ""

payload = {
"category": str(category_value),
"abstract": getattr(candidate, "abstract", "") or "",
"overview": getattr(candidate, "overview", "") or "",
"content": getattr(candidate, "content", "") or "",
"language": getattr(candidate, "language", "") or "",
}

if isinstance(candidate, ToolSkillCandidateMemory):
payload.update(
{
"tool_name": candidate.tool_name,
"skill_name": candidate.skill_name,
"call_time": candidate.call_time,
"success_time": candidate.success_time,
"duration_ms": candidate.duration_ms,
"prompt_tokens": candidate.prompt_tokens,
"completion_tokens": candidate.completion_tokens,
"best_for": candidate.best_for,
"optimal_params": candidate.optimal_params,
"recommended_flow": candidate.recommended_flow,
"key_dependencies": candidate.key_dependencies,
"common_failures": candidate.common_failures,
"recommendation": candidate.recommendation,
}
)

return payload

async def preview_extract(self, session_id: str, ctx: RequestContext) -> Dict[str, Any]:
"""Preview memory extraction results without persisting any memories."""
self._ensure_initialized()
if not self._session_compressor:
raise NotInitializedError("SessionCompressor")

session = await self.get(session_id, ctx)
messages = list(session.messages)
latest_archive_overview = await session._get_latest_completed_archive_overview()
archive_summary_preview = ""
if messages:
archive_summary_preview = await session._generate_archive_summary_async(
messages,
latest_archive_overview=latest_archive_overview,
)

candidates = await self._session_compressor.preview_long_term_memories(
messages=messages,
user=ctx.user,
session_id=session_id,
latest_archive_overview=latest_archive_overview,
)
serialized_candidates = [
self._serialize_preview_candidate(candidate) for candidate in candidates
]

counts = defaultdict(int)
for candidate in serialized_candidates:
category = candidate.get("category", "") or "unknown"
counts[category] += 1
counts["total"] = len(serialized_candidates)

return {
"session_id": session_id,
"message_count": len(messages),
"estimated_message_tokens": sum(msg.estimated_tokens for msg in messages),
"latest_archive_overview": latest_archive_overview,
"archive_summary_preview": archive_summary_preview,
"counts_by_category": dict(counts),
"candidates": serialized_candidates,
}
22 changes: 22 additions & 0 deletions openviking/session/compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,28 @@ async def extract_long_term_memories(
self._pending_semantic_changes.clear()
raise

async def preview_long_term_memories(
self,
messages: List[Message],
user: Optional["UserIdentifier"] = None,
session_id: Optional[str] = None,
latest_archive_overview: str = "",
) -> List[CandidateMemory]:
"""Preview extracted memory candidates without writing any memory files.

This mirrors the extraction stage used by session commit, but stops
before deduplication, merge, indexing, or persistence so callers can
inspect the candidate memories that the LLM produced.
"""
if not messages or not user or not session_id:
return []

context = {
"messages": messages,
"summary": latest_archive_overview or "",
}
return await self.extractor.extract(context, user, session_id)

def _extract_tool_parts(self, messages: List[Message]) -> List:
"""Extract all ToolPart from messages."""
from openviking.message.part import ToolPart
Expand Down
4 changes: 4 additions & 0 deletions openviking/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any
"""Get one completed archive for a session."""
return run_async(self._async_client.get_session_archive(session_id, archive_id))

def preview_memory_extraction(self, session_id: str) -> Dict[str, Any]:
"""Preview extracted memories for a session without persisting them."""
return run_async(self._async_client.preview_memory_extraction(session_id))

def delete_session(self, session_id: str) -> None:
"""Delete a session."""
run_async(self._async_client.delete_session(session_id))
Expand Down
5 changes: 5 additions & 0 deletions openviking_cli/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[st
"""Get one completed archive for a session."""
...

@abstractmethod
async def preview_memory_extraction(self, session_id: str) -> Dict[str, Any]:
"""Preview extracted memories for a session without persisting them."""
...

@abstractmethod
async def delete_session(self, session_id: str) -> None:
"""Delete a session."""
Expand Down
7 changes: 7 additions & 0 deletions openviking_cli/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,13 @@ async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[st
)
return self._handle_response(response)

async def preview_memory_extraction(self, session_id: str) -> Dict[str, Any]:
"""Preview extracted memories for a session without persisting them."""
response = await self._http.post(
f"/api/v1/sessions/{session_id}/extract-preview",
)
return self._handle_response(response)

async def delete_session(self, session_id: str) -> None:
"""Delete a session."""
response = await self._http.delete(f"/api/v1/sessions/{session_id}")
Expand Down
Loading