langchain-openviking 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. langchain_openviking-0.1.0/PKG-INFO +96 -0
  2. langchain_openviking-0.1.0/README.md +60 -0
  3. langchain_openviking-0.1.0/README_CN.md +47 -0
  4. langchain_openviking-0.1.0/pyproject.toml +98 -0
  5. langchain_openviking-0.1.0/setup.cfg +4 -0
  6. langchain_openviking-0.1.0/src/langchain_openviking/__init__.py +106 -0
  7. langchain_openviking-0.1.0/src/langchain_openviking/_async_client_cache.py +124 -0
  8. langchain_openviking-0.1.0/src/langchain_openviking/_testing_utils.py +62 -0
  9. langchain_openviking-0.1.0/src/langchain_openviking/_uri.py +61 -0
  10. langchain_openviking-0.1.0/src/langchain_openviking/actor_peer.py +48 -0
  11. langchain_openviking-0.1.0/src/langchain_openviking/client.py +872 -0
  12. langchain_openviking-0.1.0/src/langchain_openviking/context.py +944 -0
  13. langchain_openviking-0.1.0/src/langchain_openviking/history.py +292 -0
  14. langchain_openviking-0.1.0/src/langchain_openviking/messages.py +206 -0
  15. langchain_openviking-0.1.0/src/langchain_openviking/middleware.py +598 -0
  16. langchain_openviking-0.1.0/src/langchain_openviking/py.typed +1 -0
  17. langchain_openviking-0.1.0/src/langchain_openviking/recording.py +697 -0
  18. langchain_openviking-0.1.0/src/langchain_openviking/retrievers.py +309 -0
  19. langchain_openviking-0.1.0/src/langchain_openviking/store.py +599 -0
  20. langchain_openviking-0.1.0/src/langchain_openviking/testing.py +409 -0
  21. langchain_openviking-0.1.0/src/langchain_openviking/tools.py +878 -0
  22. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/PKG-INFO +96 -0
  23. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/SOURCES.txt +28 -0
  24. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/dependency_links.txt +1 -0
  25. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/requires.txt +17 -0
  26. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/scm_file_list.json +25 -0
  27. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/scm_version.json +8 -0
  28. langchain_openviking-0.1.0/src/langchain_openviking.egg-info/top_level.txt +1 -0
  29. langchain_openviking-0.1.0/tests/installed_base_smoke.py +45 -0
  30. langchain_openviking-0.1.0/tests/installed_smoke.py +59 -0
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-openviking
3
+ Version: 0.1.0
4
+ Summary: OpenViking integrations for LangChain and LangGraph
5
+ Author-email: ByteDance <noreply@bytedance.com>
6
+ License-Expression: AGPL-3.0
7
+ Project-URL: Homepage, https://github.com/volcengine/openviking
8
+ Project-URL: Documentation, https://openviking.ai
9
+ Project-URL: Repository, https://github.com/volcengine/openviking
10
+ Project-URL: Issues, https://github.com/volcengine/openviking/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: langchain-core<2.0.0,>=1.0.0
23
+ Requires-Dist: openviking-sdk>=0.1.1
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: langgraph
26
+ Requires-Dist: langchain<2.0.0,>=1.0.0; extra == "langgraph"
27
+ Requires-Dist: langgraph<2.0.0,>=1.0.0; extra == "langgraph"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=7.0.0; extra == "test"
30
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: twine>=5.0.0; extra == "dev"
36
+
37
+ # langchain-openviking
38
+
39
+ `langchain-openviking` is the official OpenViking integration package for
40
+ LangChain and LangGraph applications. It keeps framework-specific adapters
41
+ separate from the OpenViking server and communicates with remote OpenViking
42
+ instances through the lightweight `openviking-sdk` package.
43
+
44
+ ## Installation
45
+
46
+ For LangChain retrievers, tools, message history, and context wrappers:
47
+
48
+ ```bash
49
+ pip install langchain-openviking
50
+ ```
51
+
52
+ For the LangGraph store and middleware:
53
+
54
+ ```bash
55
+ pip install "langchain-openviking[langgraph]"
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ from langchain_openviking import OpenVikingRetriever
62
+ from openviking_sdk import SyncHTTPClient
63
+
64
+ client = SyncHTTPClient(
65
+ url="http://127.0.0.1:1933",
66
+ api_key="your-user-api-key",
67
+ )
68
+ client.initialize()
69
+ retriever = OpenVikingRetriever(
70
+ client=client,
71
+ target_uri="viking://user/memories",
72
+ )
73
+
74
+ try:
75
+ documents = retriever.invoke("What deployment preferences should I remember?")
76
+ finally:
77
+ client.close()
78
+ ```
79
+
80
+ The package also provides `OpenVikingSessionRecorder`,
81
+ `OpenVikingContextMiddleware`, `OpenVikingStore`,
82
+ `OpenVikingChatMessageHistory`, and `create_openviking_tools`.
83
+
84
+ ## Client ownership
85
+
86
+ - A client supplied through `client=` or `async_client=` remains caller-owned.
87
+ - Clients created from `url=` are managed by the adapter and can be closed with
88
+ `close()` or `aclose()` as documented by each adapter.
89
+ - Embedded `path=` mode requires the full `openviking` package. It is kept for
90
+ compatibility, while the standalone package's default boundary is HTTP.
91
+
92
+ The previous `openviking.integrations.langchain` import path remains available
93
+ from the full `openviking` distribution as a compatibility shim.
94
+
95
+ See the [OpenViking documentation](https://openviking.ai) and the repository's
96
+ `examples/langchain-langgraph` directory for complete examples.
@@ -0,0 +1,60 @@
1
+ # langchain-openviking
2
+
3
+ `langchain-openviking` is the official OpenViking integration package for
4
+ LangChain and LangGraph applications. It keeps framework-specific adapters
5
+ separate from the OpenViking server and communicates with remote OpenViking
6
+ instances through the lightweight `openviking-sdk` package.
7
+
8
+ ## Installation
9
+
10
+ For LangChain retrievers, tools, message history, and context wrappers:
11
+
12
+ ```bash
13
+ pip install langchain-openviking
14
+ ```
15
+
16
+ For the LangGraph store and middleware:
17
+
18
+ ```bash
19
+ pip install "langchain-openviking[langgraph]"
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```python
25
+ from langchain_openviking import OpenVikingRetriever
26
+ from openviking_sdk import SyncHTTPClient
27
+
28
+ client = SyncHTTPClient(
29
+ url="http://127.0.0.1:1933",
30
+ api_key="your-user-api-key",
31
+ )
32
+ client.initialize()
33
+ retriever = OpenVikingRetriever(
34
+ client=client,
35
+ target_uri="viking://user/memories",
36
+ )
37
+
38
+ try:
39
+ documents = retriever.invoke("What deployment preferences should I remember?")
40
+ finally:
41
+ client.close()
42
+ ```
43
+
44
+ The package also provides `OpenVikingSessionRecorder`,
45
+ `OpenVikingContextMiddleware`, `OpenVikingStore`,
46
+ `OpenVikingChatMessageHistory`, and `create_openviking_tools`.
47
+
48
+ ## Client ownership
49
+
50
+ - A client supplied through `client=` or `async_client=` remains caller-owned.
51
+ - Clients created from `url=` are managed by the adapter and can be closed with
52
+ `close()` or `aclose()` as documented by each adapter.
53
+ - Embedded `path=` mode requires the full `openviking` package. It is kept for
54
+ compatibility, while the standalone package's default boundary is HTTP.
55
+
56
+ The previous `openviking.integrations.langchain` import path remains available
57
+ from the full `openviking` distribution as a compatibility shim.
58
+
59
+ See the [OpenViking documentation](https://openviking.ai) and the repository's
60
+ `examples/langchain-langgraph` directory for complete examples.
@@ -0,0 +1,47 @@
1
+ # langchain-openviking
2
+
3
+ `langchain-openviking` 是 OpenViking 官方维护的 LangChain 和 LangGraph
4
+ 集成包。框架适配逻辑不再依赖 OpenViking 服务端实现,远程访问统一通过轻量的
5
+ `openviking-sdk` 完成。
6
+
7
+ ## 安装
8
+
9
+ LangChain Retriever、Tools、Message History 和 Context Wrapper:
10
+
11
+ ```bash
12
+ pip install langchain-openviking
13
+ ```
14
+
15
+ LangGraph Store 和 Middleware:
16
+
17
+ ```bash
18
+ pip install "langchain-openviking[langgraph]"
19
+ ```
20
+
21
+ ## 快速开始
22
+
23
+ ```python
24
+ from langchain_openviking import OpenVikingRetriever
25
+ from openviking_sdk import SyncHTTPClient
26
+
27
+ client = SyncHTTPClient(
28
+ url="http://127.0.0.1:1933",
29
+ api_key="your-user-api-key",
30
+ )
31
+ client.initialize()
32
+ retriever = OpenVikingRetriever(
33
+ client=client,
34
+ target_uri="viking://user/memories",
35
+ )
36
+
37
+ try:
38
+ documents = retriever.invoke("需要记住哪些部署偏好?")
39
+ finally:
40
+ client.close()
41
+ ```
42
+
43
+ 外部传入的 client 仍由调用方管理。通过 `url=` 创建的 client 由适配器管理。
44
+ `path=` 嵌入模式需要另外安装完整的 `openviking` 包。
45
+
46
+ 完整 `openviking` 包会继续保留原有的
47
+ `openviking.integrations.langchain` 导入路径,并转发到本包,方便现有应用平滑迁移。
@@ -0,0 +1,98 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "setuptools-scm>=8.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "langchain-openviking"
7
+ dynamic = ["version"]
8
+ description = "OpenViking integrations for LangChain and LangGraph"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ {name = "ByteDance", email = "noreply@bytedance.com"},
13
+ ]
14
+ license = "AGPL-3.0"
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ ]
26
+ dependencies = [
27
+ "langchain-core>=1.0.0,<2.0.0",
28
+ "openviking-sdk>=0.1.1",
29
+ "pydantic>=2.0.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ langgraph = [
34
+ "langchain>=1.0.0,<2.0.0",
35
+ "langgraph>=1.0.0,<2.0.0",
36
+ ]
37
+ test = [
38
+ "pytest>=7.0.0",
39
+ "pytest-asyncio>=0.21.0",
40
+ ]
41
+ dev = [
42
+ "build>=1.2.0",
43
+ "mypy>=1.0.0",
44
+ "ruff>=0.1.0",
45
+ "twine>=5.0.0",
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/volcengine/openviking"
50
+ Documentation = "https://openviking.ai"
51
+ Repository = "https://github.com/volcengine/openviking"
52
+ Issues = "https://github.com/volcengine/openviking/issues"
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+
57
+ [tool.setuptools.packages.find]
58
+ where = ["src"]
59
+ include = ["langchain_openviking*"]
60
+
61
+ [tool.setuptools.package-data]
62
+ langchain_openviking = ["py.typed"]
63
+
64
+ [tool.setuptools_scm]
65
+ root = "../.."
66
+ local_scheme = "no-local-version"
67
+ tag_regex = "^langchain-openviking@(?P<version>[0-9]+(?:\\.[0-9]+)*)$"
68
+ git_describe_command = "git describe --dirty --tags --long --match langchain-openviking@*"
69
+
70
+ [tool.mypy]
71
+ python_version = "3.10"
72
+ warn_return_any = false
73
+ warn_unused_configs = true
74
+ check_untyped_defs = true
75
+ no_implicit_optional = false
76
+ warn_redundant_casts = true
77
+ warn_unused_ignores = true
78
+ ignore_missing_imports = true
79
+
80
+ [tool.pytest.ini_options]
81
+ asyncio_mode = "auto"
82
+
83
+ [tool.ruff]
84
+ line-length = 100
85
+ target-version = "py310"
86
+
87
+ [tool.ruff.lint]
88
+ select = ["E", "W", "F", "I", "C", "B"]
89
+ ignore = ["E501", "B008", "C901", "B006", "B904", "E741", "E722", "B027"]
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "__init__.py" = ["F401"]
93
+
94
+ [tool.ruff.format]
95
+ quote-style = "double"
96
+ indent-style = "space"
97
+ skip-magic-trailing-comma = false
98
+ line-ending = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,106 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: AGPL-3.0
3
+ """LangChain and LangGraph integrations for OpenViking.
4
+
5
+ The base distribution depends on ``langchain-core``. LangGraph middleware is
6
+ loaded only when the ``langgraph`` extra (or compatible dependencies) is
7
+ installed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib.util import find_spec
13
+ from typing import Any
14
+
15
+ from langchain_openviking.actor_peer import (
16
+ has_request_actor_peer_support,
17
+ )
18
+
19
+ __all__ = [
20
+ "InMemoryOpenVikingClient",
21
+ "OpenVikingChatMessageHistory",
22
+ "OpenVikingCancellationProgress",
23
+ "OpenVikingCommitPolicy",
24
+ "OpenVikingContextRunnable",
25
+ "OpenVikingPartialWriteError",
26
+ "OpenVikingRecordResult",
27
+ "OpenVikingRetriever",
28
+ "OpenVikingSessionContextAssembler",
29
+ "OpenVikingSessionRecorder",
30
+ "OpenVikingStore",
31
+ "create_openviking_tools",
32
+ "get_openviking_cancellation_progress",
33
+ "has_request_actor_peer_support",
34
+ "with_openviking_context",
35
+ ]
36
+
37
+ if find_spec("langchain") is not None:
38
+ __all__.append("OpenVikingContextMiddleware")
39
+
40
+
41
+ def __getattr__(name: str) -> Any:
42
+ if name == "OpenVikingRetriever":
43
+ from langchain_openviking.retrievers import OpenVikingRetriever
44
+
45
+ return OpenVikingRetriever
46
+ if name == "create_openviking_tools":
47
+ from langchain_openviking.tools import create_openviking_tools
48
+
49
+ return create_openviking_tools
50
+ if name == "OpenVikingStore":
51
+ from langchain_openviking.store import OpenVikingStore
52
+
53
+ return OpenVikingStore
54
+ if name == "OpenVikingChatMessageHistory":
55
+ from langchain_openviking.history import OpenVikingChatMessageHistory
56
+
57
+ return OpenVikingChatMessageHistory
58
+ if name == "OpenVikingSessionContextAssembler":
59
+ from langchain_openviking.context import OpenVikingSessionContextAssembler
60
+
61
+ return OpenVikingSessionContextAssembler
62
+ if name == "OpenVikingContextRunnable":
63
+ from langchain_openviking.context import OpenVikingContextRunnable
64
+
65
+ return OpenVikingContextRunnable
66
+ if name == "OpenVikingSessionRecorder":
67
+ from langchain_openviking.recording import OpenVikingSessionRecorder
68
+
69
+ return OpenVikingSessionRecorder
70
+ if name == "OpenVikingPartialWriteError":
71
+ from langchain_openviking.recording import OpenVikingPartialWriteError
72
+
73
+ return OpenVikingPartialWriteError
74
+ if name == "OpenVikingRecordResult":
75
+ from langchain_openviking.recording import OpenVikingRecordResult
76
+
77
+ return OpenVikingRecordResult
78
+ if name == "OpenVikingCancellationProgress":
79
+ from langchain_openviking.recording import (
80
+ OpenVikingCancellationProgress,
81
+ )
82
+
83
+ return OpenVikingCancellationProgress
84
+ if name == "get_openviking_cancellation_progress":
85
+ from langchain_openviking.recording import (
86
+ get_openviking_cancellation_progress,
87
+ )
88
+
89
+ return get_openviking_cancellation_progress
90
+ if name == "OpenVikingCommitPolicy":
91
+ from langchain_openviking.client import OpenVikingCommitPolicy
92
+
93
+ return OpenVikingCommitPolicy
94
+ if name == "with_openviking_context":
95
+ from langchain_openviking.context import with_openviking_context
96
+
97
+ return with_openviking_context
98
+ if name == "OpenVikingContextMiddleware":
99
+ from langchain_openviking.middleware import OpenVikingContextMiddleware
100
+
101
+ return OpenVikingContextMiddleware
102
+ if name == "InMemoryOpenVikingClient":
103
+ from langchain_openviking.testing import InMemoryOpenVikingClient
104
+
105
+ return InMemoryOpenVikingClient
106
+ raise AttributeError(name)
@@ -0,0 +1,124 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: AGPL-3.0
3
+ """Private event-loop scoped cache for integration-owned async clients."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import inspect
9
+ import threading
10
+ import weakref
11
+ from collections.abc import Callable
12
+ from typing import Any, Protocol
13
+
14
+
15
+ class _Closeable(Protocol):
16
+ def close(self) -> Any: ...
17
+
18
+
19
+ class _AsyncCloseable(Protocol):
20
+ def aclose(self) -> Any: ...
21
+
22
+
23
+ def _close_client(client: _Closeable) -> Any:
24
+ return client.close()
25
+
26
+
27
+ def _aclose_client(client: _AsyncCloseable) -> Any:
28
+ return client.aclose()
29
+
30
+
31
+ class LoopScopedAsyncClientCache:
32
+ """Cache integration-owned async clients per running event loop.
33
+
34
+ HTTP clients may bind internal asyncio primitives to the loop that first
35
+ uses them. Keeping this small cache private lets the adapter own that
36
+ lifecycle without importing OpenViking server utilities.
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ self._clients_by_loop: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = (
41
+ weakref.WeakKeyDictionary()
42
+ )
43
+ self._fallback_client: Any = None
44
+ self._lock = threading.Lock()
45
+
46
+ def __copy__(self) -> LoopScopedAsyncClientCache:
47
+ """Return an empty cache instead of sharing live async clients."""
48
+
49
+ return type(self)()
50
+
51
+ def __deepcopy__(self, memo: dict[int, Any]) -> LoopScopedAsyncClientCache:
52
+ """Return an empty cache instead of copying loop-bound clients."""
53
+
54
+ copied = type(self)()
55
+ memo[id(self)] = copied
56
+ return copied
57
+
58
+ def get(self, factory: Callable[[], Any]) -> Any:
59
+ """Return the client for the current loop, creating it if necessary."""
60
+
61
+ try:
62
+ loop = asyncio.get_running_loop()
63
+ except RuntimeError:
64
+ with self._lock:
65
+ if self._fallback_client is None:
66
+ self._fallback_client = factory()
67
+ return self._fallback_client
68
+
69
+ with self._lock:
70
+ client = self._clients_by_loop.get(loop)
71
+ if client is None:
72
+ client = factory()
73
+ self._clients_by_loop[loop] = client
74
+ return client
75
+
76
+ def has_clients(self) -> bool:
77
+ """Return whether the cache currently retains any clients."""
78
+
79
+ with self._lock:
80
+ return bool(self._clients_by_loop) or self._fallback_client is not None
81
+
82
+ def pop_all(self) -> list[Any]:
83
+ """Remove and return every retained client."""
84
+
85
+ with self._lock:
86
+ clients = list(self._clients_by_loop.values())
87
+ self._clients_by_loop.clear()
88
+ if self._fallback_client is not None:
89
+ clients.append(self._fallback_client)
90
+ self._fallback_client = None
91
+ return clients
92
+
93
+ @staticmethod
94
+ async def _close_clients(clients: list[Any], close_client: Callable[[Any], Any]) -> None:
95
+ seen: set[int] = set()
96
+ for client in clients:
97
+ client_id = id(client)
98
+ if client_id in seen:
99
+ continue
100
+ seen.add(client_id)
101
+ result = close_client(client)
102
+ if inspect.isawaitable(result):
103
+ await result
104
+
105
+ def close_all(self, close_client: Callable[[Any], Any]) -> None:
106
+ """Close and clear all cached clients on a best-effort basis."""
107
+
108
+ clients = self.pop_all()
109
+ if not clients:
110
+ return
111
+ try:
112
+ loop = asyncio.get_running_loop()
113
+ except RuntimeError:
114
+ loop = None
115
+ if loop and loop.is_running():
116
+ loop.create_task(self._close_clients(clients, close_client))
117
+ else:
118
+ asyncio.run(self._close_clients(clients, close_client))
119
+
120
+ def close_all_with_close(self) -> None:
121
+ self.close_all(_close_client)
122
+
123
+ def close_all_with_aclose(self) -> None:
124
+ self.close_all(_aclose_client)
@@ -0,0 +1,62 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: AGPL-3.0
3
+ """Private deterministic helpers for the in-memory integration test client."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ import re
9
+
10
+ _IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z0-9_.@-]+$")
11
+
12
+
13
+ def normalize_peer_id(peer_id: str | None) -> str | None:
14
+ """Normalize a peer identifier using OpenViking's public wire constraints."""
15
+
16
+ if peer_id is None:
17
+ return None
18
+ if not isinstance(peer_id, str):
19
+ raise ValueError("Invalid peer_id: peer_id must be a string.")
20
+ normalized = peer_id.strip()
21
+ if not normalized:
22
+ return None
23
+ if normalized in {".", ".."}:
24
+ raise ValueError("Invalid peer_id: peer_id must not be '.' or '..'")
25
+ if not _IDENTIFIER_PATTERN.fullmatch(normalized):
26
+ raise ValueError("Invalid peer_id: peer_id must be alpha_numeric string.")
27
+ if normalized.count("@") > 1:
28
+ raise ValueError("Invalid peer_id: peer_id must have at most one @.")
29
+ return normalized
30
+
31
+
32
+ def _is_cjk_code_point(code_point: int) -> bool:
33
+ return (
34
+ 0x3400 <= code_point <= 0x4DBF
35
+ or 0x4E00 <= code_point <= 0x9FFF
36
+ or 0xF900 <= code_point <= 0xFAFF
37
+ or 0x20000 <= code_point <= 0x2EBEF
38
+ or 0x3040 <= code_point <= 0x30FF
39
+ or 0x31F0 <= code_point <= 0x31FF
40
+ or 0xAC00 <= code_point <= 0xD7AF
41
+ or 0x1100 <= code_point <= 0x11FF
42
+ or 0x3130 <= code_point <= 0x318F
43
+ or 0xFF00 <= code_point <= 0xFFEF
44
+ or 0x3000 <= code_point <= 0x303F
45
+ )
46
+
47
+
48
+ def estimate_text_tokens(text: str | None) -> int:
49
+ """Estimate tokens conservatively without depending on server internals."""
50
+
51
+ if not text:
52
+ return 0
53
+ weight = 0.0
54
+ for char in text:
55
+ code_point = ord(char)
56
+ if _is_cjk_code_point(code_point):
57
+ weight += 1.5
58
+ elif code_point > 0xFFFF:
59
+ weight += 2.0
60
+ else:
61
+ weight += 0.25
62
+ return math.ceil(weight)
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: AGPL-3.0
3
+ """Narrow Viking URI parsing used by the LangGraph store adapter."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+ _CONTENT_SEGMENTS = frozenset({"memories", "resources", "skills"})
10
+ _PEER_CONTENT_SEGMENTS = frozenset({"memories", "resources"})
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class UriClassification:
15
+ """Structural URI information needed to validate store record paths."""
16
+
17
+ parts: tuple[str, ...]
18
+ scope: str
19
+ content_index: int | None
20
+
21
+
22
+ def _uri_parts(uri: str) -> tuple[str, ...]:
23
+ value = str(uri or "").split("?", 1)[0].strip()
24
+ if not value:
25
+ return ()
26
+ if value == "/":
27
+ value = "viking://"
28
+ elif not value.startswith("viking://"):
29
+ value = f"viking://{value.lstrip('/')}"
30
+ if value.rstrip("/") == "viking:":
31
+ return ()
32
+ return tuple(part for part in value[len("viking://") :].split("/") if part)
33
+
34
+
35
+ def _content_segment_index(parts: tuple[str, ...]) -> int | None:
36
+ if len(parts) >= 2 and parts[:2] == ("agent", "skills"):
37
+ return 1
38
+ if len(parts) >= 3 and parts[0] == "agent" and parts[2] in _CONTENT_SEGMENTS:
39
+ return 2
40
+ if len(parts) < 2 or parts[0] != "user":
41
+ return None
42
+ if parts[1] in _CONTENT_SEGMENTS:
43
+ return 1
44
+ if len(parts) >= 4 and parts[1] == "peers" and parts[3] in _PEER_CONTENT_SEGMENTS:
45
+ return 3
46
+ if len(parts) >= 5 and parts[2] == "peers" and parts[4] in _PEER_CONTENT_SEGMENTS:
47
+ return 4
48
+ if len(parts) >= 3 and parts[2] in _CONTENT_SEGMENTS:
49
+ return 2
50
+ return None
51
+
52
+
53
+ def classify_uri(uri: str) -> UriClassification:
54
+ """Classify the user/agent namespace shape of a Viking URI."""
55
+
56
+ parts = _uri_parts(uri)
57
+ return UriClassification(
58
+ parts=parts,
59
+ scope=parts[0] if parts else "",
60
+ content_index=_content_segment_index(parts),
61
+ )