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
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,18 +705,21 @@ viking://
│ │ └── src/
│ └── ...
├── user/ # User: personal preferences, habits, etc.
│ └── memories/
│ ├── preferences/
│ │ ├── writing_style
│ │ └── coding_habits
│ └── ...
└── agent/ # Agent: skills, instructions, task memories, etc.
├── skills/
│ ├── search_code
│ ├── analyze_data
│ └── ...
├── memories/
└── instructions/
│ └── {user_id}/
│ ├── memories/
│ │ ├── preferences/
│ │ │ ├── writing_style
│ │ │ └── coding_habits
│ │ └── ...
│ ├── resources/
│ │ └── private_project/
│ ├── skills/
│ │ ├── search_code
│ │ └── analyze_data
│ └── peers/
│ └── web-visitor-alice/
│ ├── memories/
│ └── resources/
```

### 2. Tiered Context Loading → Reduces Token Consumption
Expand Down
27 changes: 15 additions & 12 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,18 +749,21 @@ viking://
│ │ └── src/
│ └── ...
├── user/ # 用户:个人偏好、习惯等
│ └── memories/
│ ├── preferences/
│ │ ├── writing_style
│ │ └── coding_habits
│ └── ...
└── agent/ # 智能体:技能、指令、任务记忆等
├── skills/
│ ├── search_code
│ ├── analyze_data
│ └── ...
├── memories/
└── instructions/
│ └── {user_id}/
│ ├── memories/
│ │ ├── preferences/
│ │ │ ├── writing_style
│ │ │ └── coding_habits
│ │ └── ...
│ ├── resources/
│ │ └── private_project/
│ ├── skills/
│ │ ├── search_code
│ │ └── analyze_data
│ └── peers/
│ └── web-visitor-alice/
│ ├── memories/
│ └── resources/
```

### 2. 分层上下文加载 → 降低 Token 消耗
Expand Down
42 changes: 25 additions & 17 deletions bot/vikingbot/openviking_mount/ov_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import base64
import json
import re
import uuid
Expand All @@ -7,6 +8,7 @@
from loguru import logger

import openviking as ov
from openviking.core.peer_id import normalize_peer_id
from vikingbot.config.loader import load_config
from vikingbot.openviking_mount.user_apikey_manager import UserApiKeyManager

Expand All @@ -17,12 +19,21 @@ def _is_session_key(agent_id: Optional[str]) -> bool:
return agent_id is not None and "__" in agent_id


def _safe_peer_id(peer_id: Optional[str]) -> Optional[str]:
def _peer_id_from_external_id(peer_id: Optional[str]) -> Optional[str]:
if not peer_id:
return None
if "/" in peer_id or "\\" in peer_id:
raw_peer_id = str(peer_id).strip()
if not raw_peer_id:
return None
return peer_id
if "/" in raw_peer_id or "\\" in raw_peer_id:
return None
try:
return normalize_peer_id(raw_peer_id)
except ValueError:
pass

encoded = base64.urlsafe_b64encode(raw_peer_id.encode("utf-8")).decode("ascii").rstrip("=")
return normalize_peer_id(f"ext-{encoded}")


class VikingClient:
Expand Down Expand Up @@ -239,7 +250,7 @@ def default_memory_policy() -> Dict[str, Dict[str, bool]]:

@staticmethod
def _peer_id(value: Optional[str]) -> Optional[str]:
return _safe_peer_id(str(value)) if value is not None else None
return _peer_id_from_external_id(str(value)) if value is not None else None

async def _load_namespace_policy(self) -> None:
if self._namespace_policy_loaded:
Expand Down Expand Up @@ -331,9 +342,9 @@ def build_current_memory_target_uris(

normalized_peer_ids = self._dedupe_strings(
[
safe_peer_id
for safe_peer_id in (self._peer_id(peer_id) for peer_id in (peer_ids or []))
if safe_peer_id
pid
for pid in (self._peer_id(peer_id) for peer_id in (peer_ids or []))
if pid
]
)
for peer_id in normalized_peer_ids:
Expand Down Expand Up @@ -368,9 +379,9 @@ def build_memory_search_requests(
)
normalized_peer_ids = self._dedupe_strings(
[
safe_peer_id
for safe_peer_id in (self._peer_id(peer_id) for peer_id in (peer_ids or []))
if safe_peer_id
pid
for pid in (self._peer_id(peer_id) for peer_id in (peer_ids or []))
if pid
]
)
effective_owner_user_id = self._effective_user_id(owner_user_id) if owner_user_id else None
Expand Down Expand Up @@ -420,9 +431,6 @@ def build_memory_search_requests(
def _skill_memory_uri(self, skill_name: str, user_id: Optional[str] = None) -> str:
return f"{self._memory_target_uri(user_id)}skills/{skill_name}.md"

def should_sender_fanout(self) -> bool:
return self._is_root_key_mode()

async def find(
self,
query: str,
Expand Down Expand Up @@ -695,9 +703,9 @@ def _extract_memories(result: Any) -> list[Any]:
peer_values.append(peer_id)
normalized_peer_ids = self._dedupe_strings(
[
safe_peer_id
for safe_peer_id in (self._peer_id(peer_value) for peer_value in peer_values)
if safe_peer_id
pid
for pid in (self._peer_id(peer_value) for peer_value in peer_values)
if pid
]
)
effective_owner_user_id = self._effective_user_id(owner_user_id) if owner_user_id else None
Expand Down Expand Up @@ -1019,7 +1027,7 @@ async def commit(
appended = await self.append_messages(
session_id,
messages,
default_user_peer_id=peer_id,
default_user_peer_id=self._peer_id(peer_id),
session_user_id=session_user_id,
)
commit_result = await self.commit_session(
Expand Down
16 changes: 9 additions & 7 deletions bot/vikingbot/tests/unit/test_openviking_peer_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
sys.modules.setdefault("vikingbot.config", config_module)
sys.modules.setdefault("vikingbot.config.loader", loader_module)

from vikingbot.openviking_mount.ov_server import VikingClient
from vikingbot.openviking_mount.ov_server import VikingClient # noqa: E402

TELEGRAM_ALICE_PEER_ID = "ext-dGVsZWdyYW06YWxpY2U"


def _client(api_key_type: str = "user") -> VikingClient:
Expand All @@ -35,12 +37,12 @@ def test_normalize_session_messages_maps_sender_to_peer_only_for_user_messages()

messages = [
{"role": "user", "content": "hello", "sender_id": "telegram:alice"},
{"role": "assistant", "content": "hi", "sender_id": "telegram:alice"},
{"role": "assistant", "content": "hi", "sender_id": "agent-1"},
]

normalized = client._normalize_session_messages(messages)

assert normalized[0]["peer_id"] == "telegram:alice"
assert normalized[0]["peer_id"] == TELEGRAM_ALICE_PEER_ID
assert "peer_id" not in normalized[1]


Expand Down Expand Up @@ -68,7 +70,7 @@ async def fake_read_content(uri, level="read"):
profile = await client.read_peer_profile("telegram:alice")

assert profile == "Alice profile"
assert calls == [("viking://user/peers/telegram:alice/memories/profile.md", "read")]
assert calls == [(f"viking://user/peers/{TELEGRAM_ALICE_PEER_ID}/memories/profile.md", "read")]


@pytest.mark.asyncio
Expand All @@ -86,7 +88,7 @@ async def fake_read_content(uri, level="read"):

assert profile == "Alice profile"
assert calls == [
("viking://user/bot-user/peers/telegram:alice/memories/profile.md", "read")
(f"viking://user/bot-user/peers/{TELEGRAM_ALICE_PEER_ID}/memories/profile.md", "read")
]


Expand Down Expand Up @@ -147,7 +149,7 @@ async def fake_commit_session(
)

assert calls["append"]["session_user_id"] is None
assert calls["append"]["default_user_peer_id"] == "telegram:alice"
assert calls["append"]["default_user_peer_id"] == TELEGRAM_ALICE_PEER_ID
assert calls["commit"]["user_id"] is None
assert calls["commit"]["memory_policy"] is None

Expand Down Expand Up @@ -185,7 +187,7 @@ async def fake_commit_session(
)

assert calls["append"]["session_user_id"] == "bot-user"
assert calls["append"]["default_user_peer_id"] == "telegram:alice"
assert calls["append"]["default_user_peer_id"] == TELEGRAM_ALICE_PEER_ID
assert calls["commit"]["user_id"] == "bot-user"
assert calls["commit"]["memory_policy"] is None

Expand Down
8 changes: 4 additions & 4 deletions crates/ov_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2598,12 +2598,12 @@ mod tests {

#[test]
fn cli_parses_find_peer_id() {
let cli = Cli::try_parse_from(["ov", "find", "invoice", "--peer-id", "web:visitor:alice"])
let cli = Cli::try_parse_from(["ov", "find", "invoice", "--peer-id", "web-visitor-alice"])
.expect("find peer id should parse");

match cli.command {
Commands::Find { peer_id, .. } => {
assert_eq!(peer_id.as_deref(), Some("web:visitor:alice"));
assert_eq!(peer_id.as_deref(), Some("web-visitor-alice"));
}
_ => panic!("expected find command"),
}
Expand All @@ -2612,12 +2612,12 @@ mod tests {
#[test]
fn cli_parses_search_peer_id() {
let cli =
Cli::try_parse_from(["ov", "search", "invoice", "--peer-id", "web:visitor:alice"])
Cli::try_parse_from(["ov", "search", "invoice", "--peer-id", "web-visitor-alice"])
.expect("search peer id should parse");

match cli.command {
Commands::Search { peer_id, .. } => {
assert_eq!(peer_id.as_deref(), Some("web:visitor:alice"));
assert_eq!(peer_id.as_deref(), Some("web-visitor-alice"));
}
_ => panic!("expected search command"),
}
Expand Down
47 changes: 39 additions & 8 deletions docs/en/api/02-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ This endpoint is the core entry point for resource management, supporting adding

**Additional Notes**:
- `to` and `parent` cannot be specified together. Use `create_parent=true` with `parent` when the parent directory should be created automatically.
- Resource targets may use public `viking://resources/...`, current-user shorthand `viking://user/resources/...`, explicit user `viking://user/{user_id}/resources/...`, or peer `viking://user/{user_id}/peers/{peer_id}/resources/...` paths. Current-user shorthand is canonicalized with the authenticated request identity.
- `user_id` and `peer_id` path segments must be safe single-segment identifiers, for example `alice` or `web-visitor-alice`. Values with path separators, `.`, `..`, `:`, or `+` are rejected.
- `path` and `temp_file_id` cannot be specified together
- Raw HTTP calls for local files require first uploading via [temp_upload](#temp_upload) to obtain `temp_file_id`
- When `to` is specified and the target already exists, triggers incremental update
Expand Down Expand Up @@ -206,6 +208,16 @@ curl -X POST http://localhost:1933/api/v1/resources \
\"to\": \"viking://resources/guide.md\",
\"reason\": \"User guide\"
}"

# Add to the current user's private resource root
curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\",
\"parent\": \"viking://user/resources/docs\",
\"create_parent\": true
}"
```

**Python SDK**
Expand Down Expand Up @@ -235,6 +247,13 @@ result = client.add_resource(
reason="External API documentation"
)

# Add to the current user's private resource root
result = client.add_resource(
"./documents/guide.md",
parent="viking://user/resources/docs",
create_parent=True,
)

# Wait for processing to complete
client.wait_processed()

Expand Down Expand Up @@ -270,6 +289,13 @@ ov add-resource https://github.com/example/repo.git --to viking://resources/guid
# Add with parent directory (parent must exist)
ov add-resource ./documents/guide.md --parent viking://resources/docs

# Add under the current user's private resource root
ov add-resource ./documents/guide.md --parent viking://user/resources/docs

# Add under a specific peer's private resource root
ov add-resource ./documents/guide.md \
--parent viking://user/alice/peers/web-visitor-alice/resources/docs

# Add with parent directory (auto-create parent if it doesn't exist)
ov add-resource ./documents/guide.md -p viking://resources/docs/2026/05/07
# Or using full flag
Expand Down Expand Up @@ -488,6 +514,11 @@ Skills are special resources used to define operations or tools that agents can
| timeout | float | No | None | Timeout in seconds, only effective when `wait=True` |
| telemetry | TelemetryRequest | No | False | Whether to return telemetry data |

Skills are always installed under the current user's skills root. The public short form
`viking://user/skills` is accepted for filesystem/search operations and resolves to
`viking://user/{user_id}/skills`; `add_skill` does not accept `to`, `parent`,
`root_uri`, or peer-scoped skill targets.

#### 3. Usage Examples

**HTTP API**
Expand Down Expand Up @@ -560,8 +591,8 @@ ov add-skill ./skills/my-skill.json --wait
"status": "ok",
"result": {
"status": "success",
"root_uri": "viking://user/skills/my-skill",
"uri": "viking://user/skills/my-skill",
"root_uri": "viking://user/alice/skills/my-skill",
"uri": "viking://user/alice/skills/my-skill",
"name": "my-skill",
"auxiliary_files": 2,
"queue_status": {
Expand All @@ -582,8 +613,8 @@ ov add-skill ./skills/my-skill.json --wait
Note: Skill is being processed in the background.
Use 'ov wait' to wait for completion, or 'ov observer queue' to check status.
status success
root_uri viking://user/skills/my-skill
uri viking://user/skills/my-skill
root_uri viking://user/alice/skills/my-skill
uri viking://user/alice/skills/my-skill
name my-skill
auxiliary_files 2
```
Expand All @@ -593,8 +624,8 @@ auxiliary_files 2
```json
{
"status": "success",
"root_uri": "viking://user/skills/my-skill",
"uri": "viking://user/skills/my-skill",
"root_uri": "viking://user/alice/skills/my-skill",
"uri": "viking://user/alice/skills/my-skill",
"name": "my-skill",
"auxiliary_files": 2
}
Expand All @@ -605,8 +636,8 @@ auxiliary_files 2
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Processing status: "success" or "error" |
| `root_uri` | string | Final URI of the skill in OpenViking (same as `uri`) |
| `uri` | string | Final URI of the skill in OpenViking (same as `root_uri`) |
| `root_uri` | string | Canonical final URI of the skill in OpenViking (same as `uri`) |
| `uri` | string | Canonical final URI of the skill in OpenViking (same as `root_uri`) |
| `name` | string | Skill name |
| `auxiliary_files` | number | Number of auxiliary files attached to the skill |
| `queue_status` | object | (Optional, only when `wait=true`) Queue processing status with `pending`, `processing`, `completed` counts |
Expand Down
Loading
Loading