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.
@@ -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)