langchain-openviking 0.1.dev1885__py3-none-any.whl
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.
- langchain_openviking/__init__.py +106 -0
- langchain_openviking/_async_client_cache.py +124 -0
- langchain_openviking/_testing_utils.py +62 -0
- langchain_openviking/_uri.py +61 -0
- langchain_openviking/actor_peer.py +48 -0
- langchain_openviking/client.py +872 -0
- langchain_openviking/context.py +944 -0
- langchain_openviking/history.py +292 -0
- langchain_openviking/messages.py +206 -0
- langchain_openviking/middleware.py +598 -0
- langchain_openviking/py.typed +1 -0
- langchain_openviking/recording.py +697 -0
- langchain_openviking/retrievers.py +309 -0
- langchain_openviking/store.py +599 -0
- langchain_openviking/testing.py +409 -0
- langchain_openviking/tools.py +878 -0
- langchain_openviking-0.1.dev1885.dist-info/METADATA +96 -0
- langchain_openviking-0.1.dev1885.dist-info/RECORD +20 -0
- langchain_openviking-0.1.dev1885.dist-info/WHEEL +5 -0
- langchain_openviking-0.1.dev1885.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0
|
|
3
|
+
"""Compatibility bridge for request-scoped actor peers in openviking-sdk."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from contextlib import AbstractContextManager
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from typing import Callable, cast
|
|
10
|
+
|
|
11
|
+
_openviking_sdk = import_module("openviking_sdk")
|
|
12
|
+
_actor_peer_getter = getattr(_openviking_sdk, "get_actor_peer_id", None)
|
|
13
|
+
_actor_peer_scope = getattr(_openviking_sdk, "use_actor_peer", None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_request_actor_peer_support() -> bool:
|
|
17
|
+
"""Return whether the installed SDK supports request-scoped actor peers."""
|
|
18
|
+
|
|
19
|
+
return _actor_peer_getter is not None and _actor_peer_scope is not None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def require_request_actor_peer_support() -> None:
|
|
23
|
+
"""Raise an actionable error when request-scoped actor peers are unavailable."""
|
|
24
|
+
|
|
25
|
+
if not has_request_actor_peer_support():
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"Runtime actor peers require an openviking-sdk release with "
|
|
28
|
+
"request-scoped actor-peer support. Upgrade openviking-sdk and retry."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_actor_peer_id() -> str | None:
|
|
33
|
+
"""Return the active SDK actor peer, tolerating older SDKs when unused."""
|
|
34
|
+
|
|
35
|
+
if _actor_peer_getter is None:
|
|
36
|
+
return None
|
|
37
|
+
return cast(Callable[[], str | None], _actor_peer_getter)()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def use_actor_peer(actor_peer_id: str | None) -> AbstractContextManager[None]:
|
|
41
|
+
"""Apply an SDK actor-peer scope after confirming SDK support."""
|
|
42
|
+
|
|
43
|
+
require_request_actor_peer_support()
|
|
44
|
+
scope_factory = cast(
|
|
45
|
+
Callable[[str | None], AbstractContextManager[None]],
|
|
46
|
+
_actor_peer_scope,
|
|
47
|
+
)
|
|
48
|
+
return scope_factory(actor_peer_id)
|