langchain 1.0.0a13__py3-none-any.whl → 1.0.0a14__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.
Potentially problematic release.
This version of langchain might be problematic. Click here for more details.
- langchain/agents/factory.py +107 -12
- langchain/agents/middleware/pii.py +6 -8
- langchain/agents/middleware/types.py +152 -13
- langchain/embeddings/__init__.py +0 -2
- langchain/messages/__init__.py +32 -0
- langchain/tools/__init__.py +2 -6
- langchain/tools/tool_node.py +30 -11
- {langchain-1.0.0a13.dist-info → langchain-1.0.0a14.dist-info}/METADATA +1 -1
- {langchain-1.0.0a13.dist-info → langchain-1.0.0a14.dist-info}/RECORD +11 -17
- langchain/documents/__init__.py +0 -7
- langchain/embeddings/cache.py +0 -361
- langchain/storage/__init__.py +0 -22
- langchain/storage/encoder_backed.py +0 -122
- langchain/storage/exceptions.py +0 -5
- langchain/storage/in_memory.py +0 -13
- {langchain-1.0.0a13.dist-info → langchain-1.0.0a14.dist-info}/WHEEL +0 -0
- {langchain-1.0.0a13.dist-info → langchain-1.0.0a14.dist-info}/licenses/LICENSE +0 -0
langchain/agents/factory.py
CHANGED
|
@@ -13,9 +13,6 @@ from typing import (
|
|
|
13
13
|
get_type_hints,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from collections.abc import Awaitable
|
|
18
|
-
|
|
19
16
|
from langchain_core.language_models.chat_models import BaseChatModel
|
|
20
17
|
from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage
|
|
21
18
|
from langchain_core.tools import BaseTool
|
|
@@ -47,11 +44,10 @@ from langchain.agents.structured_output import (
|
|
|
47
44
|
ToolStrategy,
|
|
48
45
|
)
|
|
49
46
|
from langchain.chat_models import init_chat_model
|
|
50
|
-
from langchain.tools import
|
|
51
|
-
from langchain.tools.tool_node import ToolCallWithContext
|
|
47
|
+
from langchain.tools.tool_node import ToolCallWithContext, _ToolNode
|
|
52
48
|
|
|
53
49
|
if TYPE_CHECKING:
|
|
54
|
-
from collections.abc import Callable, Sequence
|
|
50
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
55
51
|
|
|
56
52
|
from langchain_core.runnables import Runnable
|
|
57
53
|
from langgraph.cache.base import BaseCache
|
|
@@ -449,6 +445,70 @@ def _chain_tool_call_wrappers(
|
|
|
449
445
|
return result
|
|
450
446
|
|
|
451
447
|
|
|
448
|
+
def _chain_async_tool_call_wrappers(
|
|
449
|
+
wrappers: Sequence[
|
|
450
|
+
Callable[
|
|
451
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
452
|
+
Awaitable[ToolMessage | Command],
|
|
453
|
+
]
|
|
454
|
+
],
|
|
455
|
+
) -> (
|
|
456
|
+
Callable[
|
|
457
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
458
|
+
Awaitable[ToolMessage | Command],
|
|
459
|
+
]
|
|
460
|
+
| None
|
|
461
|
+
):
|
|
462
|
+
"""Compose async wrappers into middleware stack (first = outermost).
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
wrappers: Async wrappers in middleware order.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Composed async wrapper, or None if empty.
|
|
469
|
+
"""
|
|
470
|
+
if not wrappers:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
if len(wrappers) == 1:
|
|
474
|
+
return wrappers[0]
|
|
475
|
+
|
|
476
|
+
def compose_two(
|
|
477
|
+
outer: Callable[
|
|
478
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
479
|
+
Awaitable[ToolMessage | Command],
|
|
480
|
+
],
|
|
481
|
+
inner: Callable[
|
|
482
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
483
|
+
Awaitable[ToolMessage | Command],
|
|
484
|
+
],
|
|
485
|
+
) -> Callable[
|
|
486
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
487
|
+
Awaitable[ToolMessage | Command],
|
|
488
|
+
]:
|
|
489
|
+
"""Compose two async wrappers where outer wraps inner."""
|
|
490
|
+
|
|
491
|
+
async def composed(
|
|
492
|
+
request: ToolCallRequest,
|
|
493
|
+
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
494
|
+
) -> ToolMessage | Command:
|
|
495
|
+
# Create an async callable that invokes inner with the original execute
|
|
496
|
+
async def call_inner(req: ToolCallRequest) -> ToolMessage | Command:
|
|
497
|
+
return await inner(req, execute)
|
|
498
|
+
|
|
499
|
+
# Outer can call call_inner multiple times
|
|
500
|
+
return await outer(request, call_inner)
|
|
501
|
+
|
|
502
|
+
return composed
|
|
503
|
+
|
|
504
|
+
# Chain all wrappers: first -> second -> ... -> last
|
|
505
|
+
result = wrappers[-1]
|
|
506
|
+
for wrapper in reversed(wrappers[:-1]):
|
|
507
|
+
result = compose_two(wrapper, result)
|
|
508
|
+
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
|
|
452
512
|
def create_agent( # noqa: PLR0915
|
|
453
513
|
model: str | BaseChatModel,
|
|
454
514
|
tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
|
|
@@ -576,9 +636,14 @@ def create_agent( # noqa: PLR0915
|
|
|
576
636
|
structured_output_tools[structured_tool_info.tool.name] = structured_tool_info
|
|
577
637
|
middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]
|
|
578
638
|
|
|
579
|
-
# Collect middleware with wrap_tool_call hooks
|
|
639
|
+
# Collect middleware with wrap_tool_call or awrap_tool_call hooks
|
|
640
|
+
# Include middleware with either implementation to ensure NotImplementedError is raised
|
|
641
|
+
# when middleware doesn't support the execution path
|
|
580
642
|
middleware_w_wrap_tool_call = [
|
|
581
|
-
m
|
|
643
|
+
m
|
|
644
|
+
for m in middleware
|
|
645
|
+
if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
|
|
646
|
+
or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
|
|
582
647
|
]
|
|
583
648
|
|
|
584
649
|
# Chain all wrap_tool_call handlers into a single composed handler
|
|
@@ -587,8 +652,24 @@ def create_agent( # noqa: PLR0915
|
|
|
587
652
|
wrappers = [m.wrap_tool_call for m in middleware_w_wrap_tool_call]
|
|
588
653
|
wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)
|
|
589
654
|
|
|
655
|
+
# Collect middleware with awrap_tool_call or wrap_tool_call hooks
|
|
656
|
+
# Include middleware with either implementation to ensure NotImplementedError is raised
|
|
657
|
+
# when middleware doesn't support the execution path
|
|
658
|
+
middleware_w_awrap_tool_call = [
|
|
659
|
+
m
|
|
660
|
+
for m in middleware
|
|
661
|
+
if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
|
|
662
|
+
or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
|
|
663
|
+
]
|
|
664
|
+
|
|
665
|
+
# Chain all awrap_tool_call handlers into a single composed async handler
|
|
666
|
+
awrap_tool_call_wrapper = None
|
|
667
|
+
if middleware_w_awrap_tool_call:
|
|
668
|
+
async_wrappers = [m.awrap_tool_call for m in middleware_w_awrap_tool_call]
|
|
669
|
+
awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)
|
|
670
|
+
|
|
590
671
|
# Setup tools
|
|
591
|
-
tool_node:
|
|
672
|
+
tool_node: _ToolNode | None = None
|
|
592
673
|
# Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)
|
|
593
674
|
built_in_tools = [t for t in tools if isinstance(t, dict)]
|
|
594
675
|
regular_tools = [t for t in tools if not isinstance(t, dict)]
|
|
@@ -598,7 +679,11 @@ def create_agent( # noqa: PLR0915
|
|
|
598
679
|
|
|
599
680
|
# Only create ToolNode if we have client-side tools
|
|
600
681
|
tool_node = (
|
|
601
|
-
|
|
682
|
+
_ToolNode(
|
|
683
|
+
tools=available_tools,
|
|
684
|
+
wrap_tool_call=wrap_tool_call_wrapper,
|
|
685
|
+
awrap_tool_call=awrap_tool_call_wrapper,
|
|
686
|
+
)
|
|
602
687
|
if available_tools
|
|
603
688
|
else None
|
|
604
689
|
)
|
|
@@ -640,13 +725,23 @@ def create_agent( # noqa: PLR0915
|
|
|
640
725
|
if m.__class__.after_agent is not AgentMiddleware.after_agent
|
|
641
726
|
or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent
|
|
642
727
|
]
|
|
728
|
+
# Collect middleware with wrap_model_call or awrap_model_call hooks
|
|
729
|
+
# Include middleware with either implementation to ensure NotImplementedError is raised
|
|
730
|
+
# when middleware doesn't support the execution path
|
|
643
731
|
middleware_w_wrap_model_call = [
|
|
644
|
-
m
|
|
732
|
+
m
|
|
733
|
+
for m in middleware
|
|
734
|
+
if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
|
|
735
|
+
or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
|
|
645
736
|
]
|
|
737
|
+
# Collect middleware with awrap_model_call or wrap_model_call hooks
|
|
738
|
+
# Include middleware with either implementation to ensure NotImplementedError is raised
|
|
739
|
+
# when middleware doesn't support the execution path
|
|
646
740
|
middleware_w_awrap_model_call = [
|
|
647
741
|
m
|
|
648
742
|
for m in middleware
|
|
649
743
|
if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
|
|
744
|
+
or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
|
|
650
745
|
]
|
|
651
746
|
|
|
652
747
|
# Compose wrap_model_call handlers into a single middleware stack (sync)
|
|
@@ -1378,7 +1473,7 @@ def _make_model_to_model_edge(
|
|
|
1378
1473
|
|
|
1379
1474
|
def _make_tools_to_model_edge(
|
|
1380
1475
|
*,
|
|
1381
|
-
tool_node:
|
|
1476
|
+
tool_node: _ToolNode,
|
|
1382
1477
|
model_destination: str,
|
|
1383
1478
|
structured_output_tools: dict[str, OutputToolBinding],
|
|
1384
1479
|
end_destination: str,
|
|
@@ -431,14 +431,12 @@ class PIIMiddleware(AgentMiddleware):
|
|
|
431
431
|
|
|
432
432
|
Strategy Selection Guide:
|
|
433
433
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
`
|
|
438
|
-
`
|
|
439
|
-
`
|
|
440
|
-
`hash` Yes (pseudonymous) Analytics, debugging
|
|
441
|
-
======== =================== =======================================
|
|
434
|
+
| Strategy | Preserves Identity? | Best For |
|
|
435
|
+
| -------- | ------------------- | --------------------------------------- |
|
|
436
|
+
| `block` | N/A | Avoid PII completely |
|
|
437
|
+
| `redact` | No | General compliance, log sanitization |
|
|
438
|
+
| `mask` | No | Human readability, customer service UIs |
|
|
439
|
+
| `hash` | Yes (pseudonymous) | Analytics, debugging |
|
|
442
440
|
|
|
443
441
|
Example:
|
|
444
442
|
```python
|
|
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
|
|
22
22
|
from langchain.tools.tool_node import ToolCallRequest
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# Needed as top level import for Pydantic schema generation on AgentState
|
|
25
25
|
from typing import TypeAlias
|
|
26
26
|
|
|
27
27
|
from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, ToolMessage # noqa: TC002
|
|
@@ -263,18 +263,35 @@ class AgentMiddleware(Generic[StateT, ContextT]):
|
|
|
263
263
|
return AIMessage(content="Simplified response")
|
|
264
264
|
```
|
|
265
265
|
"""
|
|
266
|
-
|
|
266
|
+
msg = (
|
|
267
|
+
"Synchronous implementation of wrap_model_call is not available. "
|
|
268
|
+
"You are likely encountering this error because you defined only the async version "
|
|
269
|
+
"(awrap_model_call) and invoked your agent in a synchronous context "
|
|
270
|
+
"(e.g., using `stream()` or `invoke()`). "
|
|
271
|
+
"To resolve this, either: "
|
|
272
|
+
"(1) subclass AgentMiddleware and implement the synchronous wrap_model_call method, "
|
|
273
|
+
"(2) use the @wrap_model_call decorator on a standalone sync function, or "
|
|
274
|
+
"(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
|
|
275
|
+
)
|
|
276
|
+
raise NotImplementedError(msg)
|
|
267
277
|
|
|
268
278
|
async def awrap_model_call(
|
|
269
279
|
self,
|
|
270
280
|
request: ModelRequest,
|
|
271
281
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
272
282
|
) -> ModelCallResult:
|
|
273
|
-
"""
|
|
283
|
+
"""Intercept and control async model execution via handler callback.
|
|
284
|
+
|
|
285
|
+
The handler callback executes the model request and returns a ModelResponse.
|
|
286
|
+
Middleware can call the handler multiple times for retry logic, skip calling
|
|
287
|
+
it to short-circuit, or modify the request/response. Multiple middleware
|
|
288
|
+
compose with first in list as outermost layer.
|
|
274
289
|
|
|
275
290
|
Args:
|
|
276
291
|
request: Model request to execute (includes state and runtime).
|
|
277
|
-
handler: Async callback that executes the model request.
|
|
292
|
+
handler: Async callback that executes the model request and returns ModelResponse.
|
|
293
|
+
Call this to execute the model. Can be called multiple times
|
|
294
|
+
for retry logic. Can skip calling it to short-circuit.
|
|
278
295
|
|
|
279
296
|
Returns:
|
|
280
297
|
ModelCallResult
|
|
@@ -291,7 +308,17 @@ class AgentMiddleware(Generic[StateT, ContextT]):
|
|
|
291
308
|
raise
|
|
292
309
|
```
|
|
293
310
|
"""
|
|
294
|
-
|
|
311
|
+
msg = (
|
|
312
|
+
"Asynchronous implementation of awrap_model_call is not available. "
|
|
313
|
+
"You are likely encountering this error because you defined only the sync version "
|
|
314
|
+
"(wrap_model_call) and invoked your agent in an asynchronous context "
|
|
315
|
+
"(e.g., using `astream()` or `ainvoke()`). "
|
|
316
|
+
"To resolve this, either: "
|
|
317
|
+
"(1) subclass AgentMiddleware and implement the asynchronous awrap_model_call method, "
|
|
318
|
+
"(2) use the @wrap_model_call decorator on a standalone async function, or "
|
|
319
|
+
"(3) invoke your agent synchronously using `stream()` or `invoke()`."
|
|
320
|
+
)
|
|
321
|
+
raise NotImplementedError(msg)
|
|
295
322
|
|
|
296
323
|
def after_agent(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
|
|
297
324
|
"""Logic to run after the agent execution completes."""
|
|
@@ -353,7 +380,77 @@ class AgentMiddleware(Generic[StateT, ContextT]):
|
|
|
353
380
|
continue
|
|
354
381
|
return result
|
|
355
382
|
"""
|
|
356
|
-
|
|
383
|
+
msg = (
|
|
384
|
+
"Synchronous implementation of wrap_tool_call is not available. "
|
|
385
|
+
"You are likely encountering this error because you defined only the async version "
|
|
386
|
+
"(awrap_tool_call) and invoked your agent in a synchronous context "
|
|
387
|
+
"(e.g., using `stream()` or `invoke()`). "
|
|
388
|
+
"To resolve this, either: "
|
|
389
|
+
"(1) subclass AgentMiddleware and implement the synchronous wrap_tool_call method, "
|
|
390
|
+
"(2) use the @wrap_tool_call decorator on a standalone sync function, or "
|
|
391
|
+
"(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
|
|
392
|
+
)
|
|
393
|
+
raise NotImplementedError(msg)
|
|
394
|
+
|
|
395
|
+
async def awrap_tool_call(
|
|
396
|
+
self,
|
|
397
|
+
request: ToolCallRequest,
|
|
398
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
399
|
+
) -> ToolMessage | Command:
|
|
400
|
+
"""Intercept and control async tool execution via handler callback.
|
|
401
|
+
|
|
402
|
+
The handler callback executes the tool call and returns a ToolMessage or Command.
|
|
403
|
+
Middleware can call the handler multiple times for retry logic, skip calling
|
|
404
|
+
it to short-circuit, or modify the request/response. Multiple middleware
|
|
405
|
+
compose with first in list as outermost layer.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
request: Tool call request with call dict, BaseTool, state, and runtime.
|
|
409
|
+
Access state via request.state and runtime via request.runtime.
|
|
410
|
+
handler: Async callable to execute the tool and returns ToolMessage or Command.
|
|
411
|
+
Call this to execute the tool. Can be called multiple times
|
|
412
|
+
for retry logic. Can skip calling it to short-circuit.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
ToolMessage or Command (the final result).
|
|
416
|
+
|
|
417
|
+
The handler callable can be invoked multiple times for retry logic.
|
|
418
|
+
Each call to handler is independent and stateless.
|
|
419
|
+
|
|
420
|
+
Examples:
|
|
421
|
+
Async retry on error:
|
|
422
|
+
```python
|
|
423
|
+
async def awrap_tool_call(self, request, handler):
|
|
424
|
+
for attempt in range(3):
|
|
425
|
+
try:
|
|
426
|
+
result = await handler(request)
|
|
427
|
+
if is_valid(result):
|
|
428
|
+
return result
|
|
429
|
+
except Exception:
|
|
430
|
+
if attempt == 2:
|
|
431
|
+
raise
|
|
432
|
+
return result
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def awrap_tool_call(self, request, handler):
|
|
437
|
+
if cached := await get_cache_async(request):
|
|
438
|
+
return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
|
|
439
|
+
result = await handler(request)
|
|
440
|
+
await save_cache_async(request, result)
|
|
441
|
+
return result
|
|
442
|
+
"""
|
|
443
|
+
msg = (
|
|
444
|
+
"Asynchronous implementation of awrap_tool_call is not available. "
|
|
445
|
+
"You are likely encountering this error because you defined only the sync version "
|
|
446
|
+
"(wrap_tool_call) and invoked your agent in an asynchronous context "
|
|
447
|
+
"(e.g., using `astream()` or `ainvoke()`). "
|
|
448
|
+
"To resolve this, either: "
|
|
449
|
+
"(1) subclass AgentMiddleware and implement the asynchronous awrap_tool_call method, "
|
|
450
|
+
"(2) use the @wrap_tool_call decorator on a standalone async function, or "
|
|
451
|
+
"(3) invoke your agent synchronously using `stream()` or `invoke()`."
|
|
452
|
+
)
|
|
453
|
+
raise NotImplementedError(msg)
|
|
357
454
|
|
|
358
455
|
|
|
359
456
|
class _CallableWithStateAndRuntime(Protocol[StateT_contra, ContextT]):
|
|
@@ -1104,6 +1201,16 @@ def dynamic_prompt(
|
|
|
1104
1201
|
request.system_prompt = prompt
|
|
1105
1202
|
return handler(request)
|
|
1106
1203
|
|
|
1204
|
+
async def async_wrapped_from_sync(
|
|
1205
|
+
self: AgentMiddleware[StateT, ContextT], # noqa: ARG001
|
|
1206
|
+
request: ModelRequest,
|
|
1207
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
1208
|
+
) -> ModelCallResult:
|
|
1209
|
+
# Delegate to sync function
|
|
1210
|
+
prompt = cast("str", func(request))
|
|
1211
|
+
request.system_prompt = prompt
|
|
1212
|
+
return await handler(request)
|
|
1213
|
+
|
|
1107
1214
|
middleware_name = cast("str", getattr(func, "__name__", "DynamicPromptMiddleware"))
|
|
1108
1215
|
|
|
1109
1216
|
return type(
|
|
@@ -1113,6 +1220,7 @@ def dynamic_prompt(
|
|
|
1113
1220
|
"state_schema": AgentState,
|
|
1114
1221
|
"tools": [],
|
|
1115
1222
|
"wrap_model_call": wrapped,
|
|
1223
|
+
"awrap_model_call": async_wrapped_from_sync,
|
|
1116
1224
|
},
|
|
1117
1225
|
)()
|
|
1118
1226
|
|
|
@@ -1309,6 +1417,7 @@ def wrap_tool_call(
|
|
|
1309
1417
|
Args:
|
|
1310
1418
|
func: Function accepting (request, handler) that calls
|
|
1311
1419
|
handler(request) to execute the tool and returns final ToolMessage or Command.
|
|
1420
|
+
Can be sync or async.
|
|
1312
1421
|
tools: Additional tools to register with this middleware.
|
|
1313
1422
|
name: Middleware class name. Defaults to function name.
|
|
1314
1423
|
|
|
@@ -1316,13 +1425,6 @@ def wrap_tool_call(
|
|
|
1316
1425
|
AgentMiddleware instance if func provided, otherwise a decorator.
|
|
1317
1426
|
|
|
1318
1427
|
Examples:
|
|
1319
|
-
Basic passthrough:
|
|
1320
|
-
```python
|
|
1321
|
-
@wrap_tool_call
|
|
1322
|
-
def passthrough(request, handler):
|
|
1323
|
-
return handler(request)
|
|
1324
|
-
```
|
|
1325
|
-
|
|
1326
1428
|
Retry logic:
|
|
1327
1429
|
```python
|
|
1328
1430
|
@wrap_tool_call
|
|
@@ -1336,6 +1438,18 @@ def wrap_tool_call(
|
|
|
1336
1438
|
raise
|
|
1337
1439
|
```
|
|
1338
1440
|
|
|
1441
|
+
Async retry logic:
|
|
1442
|
+
```python
|
|
1443
|
+
@wrap_tool_call
|
|
1444
|
+
async def async_retry(request, handler):
|
|
1445
|
+
for attempt in range(3):
|
|
1446
|
+
try:
|
|
1447
|
+
return await handler(request)
|
|
1448
|
+
except Exception:
|
|
1449
|
+
if attempt == 2:
|
|
1450
|
+
raise
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1339
1453
|
Modify request:
|
|
1340
1454
|
```python
|
|
1341
1455
|
@wrap_tool_call
|
|
@@ -1359,6 +1473,31 @@ def wrap_tool_call(
|
|
|
1359
1473
|
def decorator(
|
|
1360
1474
|
func: _CallableReturningToolResponse,
|
|
1361
1475
|
) -> AgentMiddleware:
|
|
1476
|
+
is_async = iscoroutinefunction(func)
|
|
1477
|
+
|
|
1478
|
+
if is_async:
|
|
1479
|
+
|
|
1480
|
+
async def async_wrapped(
|
|
1481
|
+
self: AgentMiddleware, # noqa: ARG001
|
|
1482
|
+
request: ToolCallRequest,
|
|
1483
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
1484
|
+
) -> ToolMessage | Command:
|
|
1485
|
+
return await func(request, handler) # type: ignore[arg-type,misc]
|
|
1486
|
+
|
|
1487
|
+
middleware_name = name or cast(
|
|
1488
|
+
"str", getattr(func, "__name__", "WrapToolCallMiddleware")
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
return type(
|
|
1492
|
+
middleware_name,
|
|
1493
|
+
(AgentMiddleware,),
|
|
1494
|
+
{
|
|
1495
|
+
"state_schema": AgentState,
|
|
1496
|
+
"tools": tools or [],
|
|
1497
|
+
"awrap_tool_call": async_wrapped,
|
|
1498
|
+
},
|
|
1499
|
+
)()
|
|
1500
|
+
|
|
1362
1501
|
def wrapped(
|
|
1363
1502
|
self: AgentMiddleware, # noqa: ARG001
|
|
1364
1503
|
request: ToolCallRequest,
|
langchain/embeddings/__init__.py
CHANGED
langchain/messages/__init__.py
CHANGED
|
@@ -3,29 +3,61 @@
|
|
|
3
3
|
from langchain_core.messages import (
|
|
4
4
|
AIMessage,
|
|
5
5
|
AIMessageChunk,
|
|
6
|
+
Annotation,
|
|
6
7
|
AnyMessage,
|
|
8
|
+
AudioContentBlock,
|
|
9
|
+
Citation,
|
|
10
|
+
ContentBlock,
|
|
11
|
+
DataContentBlock,
|
|
12
|
+
FileContentBlock,
|
|
7
13
|
HumanMessage,
|
|
14
|
+
ImageContentBlock,
|
|
8
15
|
InvalidToolCall,
|
|
9
16
|
MessageLikeRepresentation,
|
|
17
|
+
NonStandardAnnotation,
|
|
18
|
+
NonStandardContentBlock,
|
|
19
|
+
PlainTextContentBlock,
|
|
20
|
+
ReasoningContentBlock,
|
|
10
21
|
RemoveMessage,
|
|
22
|
+
ServerToolCall,
|
|
23
|
+
ServerToolCallChunk,
|
|
24
|
+
ServerToolResult,
|
|
11
25
|
SystemMessage,
|
|
26
|
+
TextContentBlock,
|
|
12
27
|
ToolCall,
|
|
13
28
|
ToolCallChunk,
|
|
14
29
|
ToolMessage,
|
|
30
|
+
VideoContentBlock,
|
|
15
31
|
trim_messages,
|
|
16
32
|
)
|
|
17
33
|
|
|
18
34
|
__all__ = [
|
|
19
35
|
"AIMessage",
|
|
20
36
|
"AIMessageChunk",
|
|
37
|
+
"Annotation",
|
|
21
38
|
"AnyMessage",
|
|
39
|
+
"AudioContentBlock",
|
|
40
|
+
"Citation",
|
|
41
|
+
"ContentBlock",
|
|
42
|
+
"DataContentBlock",
|
|
43
|
+
"FileContentBlock",
|
|
22
44
|
"HumanMessage",
|
|
45
|
+
"ImageContentBlock",
|
|
23
46
|
"InvalidToolCall",
|
|
24
47
|
"MessageLikeRepresentation",
|
|
48
|
+
"NonStandardAnnotation",
|
|
49
|
+
"NonStandardContentBlock",
|
|
50
|
+
"PlainTextContentBlock",
|
|
51
|
+
"ReasoningContentBlock",
|
|
25
52
|
"RemoveMessage",
|
|
53
|
+
"ServerToolCall",
|
|
54
|
+
"ServerToolCallChunk",
|
|
55
|
+
"ServerToolResult",
|
|
26
56
|
"SystemMessage",
|
|
57
|
+
"TextContentBlock",
|
|
27
58
|
"ToolCall",
|
|
28
59
|
"ToolCallChunk",
|
|
29
60
|
"ToolMessage",
|
|
61
|
+
"VideoContentBlock",
|
|
30
62
|
"trim_messages",
|
|
31
63
|
]
|
langchain/tools/__init__.py
CHANGED
|
@@ -8,11 +8,7 @@ from langchain_core.tools import (
|
|
|
8
8
|
tool,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
from langchain.tools.tool_node import
|
|
12
|
-
InjectedState,
|
|
13
|
-
InjectedStore,
|
|
14
|
-
ToolNode,
|
|
15
|
-
)
|
|
11
|
+
from langchain.tools.tool_node import InjectedState, InjectedStore, ToolInvocationError
|
|
16
12
|
|
|
17
13
|
__all__ = [
|
|
18
14
|
"BaseTool",
|
|
@@ -21,6 +17,6 @@ __all__ = [
|
|
|
21
17
|
"InjectedToolArg",
|
|
22
18
|
"InjectedToolCallId",
|
|
23
19
|
"ToolException",
|
|
24
|
-
"
|
|
20
|
+
"ToolInvocationError",
|
|
25
21
|
"tool",
|
|
26
22
|
]
|
langchain/tools/tool_node.py
CHANGED
|
@@ -38,7 +38,7 @@ from __future__ import annotations
|
|
|
38
38
|
import asyncio
|
|
39
39
|
import inspect
|
|
40
40
|
import json
|
|
41
|
-
from collections.abc import Callable
|
|
41
|
+
from collections.abc import Awaitable, Callable
|
|
42
42
|
from copy import copy, deepcopy
|
|
43
43
|
from dataclasses import dataclass, replace
|
|
44
44
|
from types import UnionType
|
|
@@ -72,6 +72,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg
|
|
|
72
72
|
from langchain_core.tools import tool as create_tool
|
|
73
73
|
from langchain_core.tools.base import (
|
|
74
74
|
TOOL_MESSAGE_BLOCK_TYPES,
|
|
75
|
+
ToolException,
|
|
75
76
|
get_all_basemodel_annotations,
|
|
76
77
|
)
|
|
77
78
|
from langgraph._internal._runnable import RunnableCallable
|
|
@@ -188,6 +189,12 @@ Examples:
|
|
|
188
189
|
return result
|
|
189
190
|
"""
|
|
190
191
|
|
|
192
|
+
AsyncToolCallWrapper = Callable[
|
|
193
|
+
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
|
|
194
|
+
Awaitable[ToolMessage | Command],
|
|
195
|
+
]
|
|
196
|
+
"""Async wrapper for tool call execution with multi-call support."""
|
|
197
|
+
|
|
191
198
|
|
|
192
199
|
class ToolCallWithContext(TypedDict):
|
|
193
200
|
"""ToolCall with additional context for graph state.
|
|
@@ -239,8 +246,11 @@ def msg_content_output(output: Any) -> str | list[dict]:
|
|
|
239
246
|
return str(output)
|
|
240
247
|
|
|
241
248
|
|
|
242
|
-
class ToolInvocationError(
|
|
243
|
-
"""
|
|
249
|
+
class ToolInvocationError(ToolException):
|
|
250
|
+
"""An error occurred while invoking a tool due to invalid arguments.
|
|
251
|
+
|
|
252
|
+
This exception is only raised when invoking a tool using the ToolNode!
|
|
253
|
+
"""
|
|
244
254
|
|
|
245
255
|
def __init__(
|
|
246
256
|
self, tool_name: str, source: ValidationError, tool_kwargs: dict[str, Any]
|
|
@@ -382,7 +392,7 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
|
|
|
382
392
|
return (Exception,)
|
|
383
393
|
|
|
384
394
|
|
|
385
|
-
class
|
|
395
|
+
class _ToolNode(RunnableCallable):
|
|
386
396
|
"""A node for executing tools in LangGraph workflows.
|
|
387
397
|
|
|
388
398
|
Handles tool execution patterns including function calls, state injection,
|
|
@@ -500,6 +510,7 @@ class ToolNode(RunnableCallable):
|
|
|
500
510
|
| tuple[type[Exception], ...] = _default_handle_tool_errors,
|
|
501
511
|
messages_key: str = "messages",
|
|
502
512
|
wrap_tool_call: ToolCallWrapper | None = None,
|
|
513
|
+
awrap_tool_call: AsyncToolCallWrapper | None = None,
|
|
503
514
|
) -> None:
|
|
504
515
|
"""Initialize ToolNode with tools and configuration.
|
|
505
516
|
|
|
@@ -509,9 +520,11 @@ class ToolNode(RunnableCallable):
|
|
|
509
520
|
tags: Optional metadata tags.
|
|
510
521
|
handle_tool_errors: Error handling configuration.
|
|
511
522
|
messages_key: State key containing messages.
|
|
512
|
-
wrap_tool_call:
|
|
523
|
+
wrap_tool_call: Sync wrapper function to intercept tool execution. Receives
|
|
513
524
|
ToolCallRequest and execute callable, returns ToolMessage or Command.
|
|
514
525
|
Enables retries, caching, request modification, and control flow.
|
|
526
|
+
awrap_tool_call: Async wrapper function to intercept tool execution.
|
|
527
|
+
If not provided, falls back to wrap_tool_call for async execution.
|
|
515
528
|
"""
|
|
516
529
|
super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
|
|
517
530
|
self._tools_by_name: dict[str, BaseTool] = {}
|
|
@@ -520,6 +533,7 @@ class ToolNode(RunnableCallable):
|
|
|
520
533
|
self._handle_tool_errors = handle_tool_errors
|
|
521
534
|
self._messages_key = messages_key
|
|
522
535
|
self._wrap_tool_call = wrap_tool_call
|
|
536
|
+
self._awrap_tool_call = awrap_tool_call
|
|
523
537
|
for tool in tools:
|
|
524
538
|
if not isinstance(tool, BaseTool):
|
|
525
539
|
tool_ = create_tool(cast("type[BaseTool]", tool))
|
|
@@ -855,7 +869,7 @@ class ToolNode(RunnableCallable):
|
|
|
855
869
|
input: list[AnyMessage] | dict[str, Any] | BaseModel,
|
|
856
870
|
runtime: Any,
|
|
857
871
|
) -> ToolMessage | Command:
|
|
858
|
-
"""Execute single tool call asynchronously with
|
|
872
|
+
"""Execute single tool call asynchronously with awrap_tool_call wrapper if configured.
|
|
859
873
|
|
|
860
874
|
Args:
|
|
861
875
|
call: Tool call dict.
|
|
@@ -883,7 +897,7 @@ class ToolNode(RunnableCallable):
|
|
|
883
897
|
runtime=runtime,
|
|
884
898
|
)
|
|
885
899
|
|
|
886
|
-
if self._wrap_tool_call is None:
|
|
900
|
+
if self._awrap_tool_call is None and self._wrap_tool_call is None:
|
|
887
901
|
# No wrapper - execute directly
|
|
888
902
|
return await self._execute_tool_async(tool_request, input_type, config)
|
|
889
903
|
|
|
@@ -892,12 +906,17 @@ class ToolNode(RunnableCallable):
|
|
|
892
906
|
"""Execute tool with given request. Can be called multiple times."""
|
|
893
907
|
return await self._execute_tool_async(req, input_type, config)
|
|
894
908
|
|
|
909
|
+
def _sync_execute(req: ToolCallRequest) -> ToolMessage | Command:
|
|
910
|
+
"""Sync execute fallback for sync wrapper."""
|
|
911
|
+
return self._execute_tool_sync(req, input_type, config)
|
|
912
|
+
|
|
895
913
|
# Call wrapper with request and execute callable
|
|
896
|
-
# Note: wrapper is sync, but execute callable is async
|
|
897
914
|
try:
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
915
|
+
if self._awrap_tool_call is not None:
|
|
916
|
+
return await self._awrap_tool_call(tool_request, execute)
|
|
917
|
+
# None check was performed above already
|
|
918
|
+
self._wrap_tool_call = cast("ToolCallWrapper", self._wrap_tool_call)
|
|
919
|
+
return self._wrap_tool_call(tool_request, _sync_execute)
|
|
901
920
|
except Exception as e:
|
|
902
921
|
# Wrapper threw an exception
|
|
903
922
|
if not self._handle_tool_errors:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a14
|
|
4
4
|
Summary: Building applications with LLMs through composability
|
|
5
5
|
Project-URL: homepage, https://docs.langchain.com/
|
|
6
6
|
Project-URL: repository, https://github.com/langchain-ai/langchain/tree/master/libs/langchain
|
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
langchain/__init__.py,sha256=rED92FbyWFRmks07cFlRTuz5ZtaPKxYq6BcsxW5KhrE,64
|
|
2
2
|
langchain/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
langchain/agents/__init__.py,sha256=x85V7MqddVSrraoirGHplPMzEz9Lha-vL9fKjXCS7lA,258
|
|
4
|
-
langchain/agents/factory.py,sha256=
|
|
4
|
+
langchain/agents/factory.py,sha256=e6xjbw_qTFvfP-AHXZtykXGrX-2GoX6hFaW_97WHiEw,61245
|
|
5
5
|
langchain/agents/structured_output.py,sha256=msf-ClqDnMfJ-oGHqjwEyth860tMnx58GLTvqJijqg8,13686
|
|
6
6
|
langchain/agents/middleware/__init__.py,sha256=FDwjEGYtxPgyFa9iiLAWT5M2W8c-NDYfGz6_y8cEqPI,1568
|
|
7
7
|
langchain/agents/middleware/context_editing.py,sha256=6ca6Qed-F59JD1rAlrIuxlrBbDVIKQmCpfvZaIFbBy8,7691
|
|
8
8
|
langchain/agents/middleware/human_in_the_loop.py,sha256=Bs4_Hgjuy9l0-AMUHvU9wlr_rL2Z1rUwL_VcfFLhhUM,12666
|
|
9
9
|
langchain/agents/middleware/model_call_limit.py,sha256=H3lJL2cLv3u0uF0kJsRagFt1rBmHHgn5SFsfnmcyQdA,6703
|
|
10
10
|
langchain/agents/middleware/model_fallback.py,sha256=pdKRSO9JD6MdMYHFl7IK4yj6LEwQfyzfTJiBE8uJ2pE,3118
|
|
11
|
-
langchain/agents/middleware/pii.py,sha256=
|
|
11
|
+
langchain/agents/middleware/pii.py,sha256=7hTBxnpcG_hSZd29TCg-4tbiLFO9IJb-wwnujCRMrv4,24780
|
|
12
12
|
langchain/agents/middleware/planning.py,sha256=59Q6-4aALytjssIZ5a4hZkx5THxIG-RTeUHuDP1LGDA,9319
|
|
13
13
|
langchain/agents/middleware/prompt_caching.py,sha256=cMvIJ_dpSsn4_cqCvZBBKjtw5GpcVkc8Lgf_VEPzM1w,3225
|
|
14
14
|
langchain/agents/middleware/summarization.py,sha256=H1VxRkkbauw4p4sMMKyc_uZGbJhtqoVvOF7y_5JBXTc,10329
|
|
15
15
|
langchain/agents/middleware/tool_call_limit.py,sha256=6cWviwPRzaf7UUcp9zlXwk6RJBBoWVaVSBc1NaVT2fI,9729
|
|
16
16
|
langchain/agents/middleware/tool_emulator.py,sha256=5qJFPfTSiVukNclDeUo7_c7-PjGEVWyefbPC-zpYSlI,7115
|
|
17
17
|
langchain/agents/middleware/tool_selection.py,sha256=6RYdgkg6aSNx1w-YxRyL2Hct7UPnMRgGg6YVZVtW5TU,11638
|
|
18
|
-
langchain/agents/middleware/types.py,sha256=
|
|
18
|
+
langchain/agents/middleware/types.py,sha256=JqTdwFru-nqs8RlamYPqEM0cnak9WBPEp__dtsve3g4,53868
|
|
19
19
|
langchain/chat_models/__init__.py,sha256=PTq9qskQEbqXYAcUUxUXDsugOcwISgFhv4w40JgkbgU,181
|
|
20
20
|
langchain/chat_models/base.py,sha256=HPlD0QaLOGXRJAY1Qq6ojr1WcteBlgVO--_GoSqpxXE,34560
|
|
21
|
-
langchain/
|
|
22
|
-
langchain/embeddings/__init__.py,sha256=sJZEfZ4ovEFU5JJnoVNCJIjtSCLT1w9r9uFw1hieRZ8,269
|
|
21
|
+
langchain/embeddings/__init__.py,sha256=kfLfu342i9bTrA0WC8yA6IJE2bgY4ZynWBi-_cMUg8E,179
|
|
23
22
|
langchain/embeddings/base.py,sha256=o77Z1TrXoUZN1SdYY9nZCNehm7cZzC-TNqc5NIzWtww,7327
|
|
24
|
-
langchain/
|
|
25
|
-
langchain/messages/__init__.py,sha256=rXBO7H-OOL1uknFjNdaRi8p1ryRVMMGdJLjSY8y1iGY,557
|
|
23
|
+
langchain/messages/__init__.py,sha256=X5-dRewJP-jtehdC6oDbs21j9bxGDUbI5WlcNrO_bHk,1309
|
|
26
24
|
langchain/rate_limiters/__init__.py,sha256=5490xUNhet37N2nX6kbJlDgf8u1DX-C1Cs_r7etXn8A,351
|
|
27
|
-
langchain/
|
|
28
|
-
langchain/
|
|
29
|
-
langchain/
|
|
30
|
-
langchain/
|
|
31
|
-
langchain/
|
|
32
|
-
langchain/
|
|
33
|
-
langchain-1.0.0a13.dist-info/METADATA,sha256=usSvQLeqH64jSkqTl15cqX4RGhj5q3sQpcQeP0uir6E,6118
|
|
34
|
-
langchain-1.0.0a13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
35
|
-
langchain-1.0.0a13.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
|
|
36
|
-
langchain-1.0.0a13.dist-info/RECORD,,
|
|
25
|
+
langchain/tools/__init__.py,sha256=C0GW8HPluAgnVmGneHXY-ibwbl3kXixBtZS88PtnXSI,410
|
|
26
|
+
langchain/tools/tool_node.py,sha256=p9NO3R8dgA9QhjCuGb-INebjizjzKj21tIsnoKSBkA8,57917
|
|
27
|
+
langchain-1.0.0a14.dist-info/METADATA,sha256=5-_c3FrZ93AM_AtuB9-PkYjoYDLgfMFtfij0SSHjvJE,6118
|
|
28
|
+
langchain-1.0.0a14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
29
|
+
langchain-1.0.0a14.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
|
|
30
|
+
langchain-1.0.0a14.dist-info/RECORD,,
|
langchain/documents/__init__.py
DELETED
langchain/embeddings/cache.py
DELETED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
"""Module contains code for a cache backed embedder.
|
|
2
|
-
|
|
3
|
-
The cache backed embedder is a wrapper around an embedder that caches
|
|
4
|
-
embeddings in a key-value store. The cache is used to avoid recomputing
|
|
5
|
-
embeddings for the same text.
|
|
6
|
-
|
|
7
|
-
The text is hashed and the hash is used as the key in the cache.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import hashlib
|
|
13
|
-
import json
|
|
14
|
-
import uuid
|
|
15
|
-
import warnings
|
|
16
|
-
from typing import TYPE_CHECKING, Literal, cast
|
|
17
|
-
|
|
18
|
-
from langchain_core.embeddings import Embeddings
|
|
19
|
-
from langchain_core.utils.iter import batch_iterate
|
|
20
|
-
|
|
21
|
-
from langchain.storage.encoder_backed import EncoderBackedStore
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Callable, Sequence
|
|
25
|
-
|
|
26
|
-
from langchain_core.stores import BaseStore, ByteStore
|
|
27
|
-
|
|
28
|
-
NAMESPACE_UUID = uuid.UUID(int=1985)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _sha1_hash_to_uuid(text: str) -> uuid.UUID:
|
|
32
|
-
"""Return a UUID derived from *text* using SHA-1 (deterministic).
|
|
33
|
-
|
|
34
|
-
Deterministic and fast, **but not collision-resistant**.
|
|
35
|
-
|
|
36
|
-
A malicious attacker could try to create two different texts that hash to the same
|
|
37
|
-
UUID. This may not necessarily be an issue in the context of caching embeddings,
|
|
38
|
-
but new applications should swap this out for a stronger hash function like
|
|
39
|
-
xxHash, BLAKE2 or SHA-256, which are collision-resistant.
|
|
40
|
-
"""
|
|
41
|
-
sha1_hex = hashlib.sha1(text.encode("utf-8"), usedforsecurity=False).hexdigest()
|
|
42
|
-
# Embed the hex string in `uuid5` to obtain a valid UUID.
|
|
43
|
-
return uuid.uuid5(NAMESPACE_UUID, sha1_hex)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _make_default_key_encoder(namespace: str, algorithm: str) -> Callable[[str], str]:
|
|
47
|
-
"""Create a default key encoder function.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
namespace: Prefix that segregates keys from different embedding models.
|
|
51
|
-
algorithm:
|
|
52
|
-
* `'sha1'` - fast but not collision-resistant
|
|
53
|
-
* `'blake2b'` - cryptographically strong, faster than SHA-1
|
|
54
|
-
* `'sha256'` - cryptographically strong, slower than SHA-1
|
|
55
|
-
* `'sha512'` - cryptographically strong, slower than SHA-1
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
A function that encodes a key using the specified algorithm.
|
|
59
|
-
"""
|
|
60
|
-
if algorithm == "sha1":
|
|
61
|
-
_warn_about_sha1_encoder()
|
|
62
|
-
|
|
63
|
-
def _key_encoder(key: str) -> str:
|
|
64
|
-
"""Encode a key using the specified algorithm."""
|
|
65
|
-
if algorithm == "sha1":
|
|
66
|
-
return f"{namespace}{_sha1_hash_to_uuid(key)}"
|
|
67
|
-
if algorithm == "blake2b":
|
|
68
|
-
return f"{namespace}{hashlib.blake2b(key.encode('utf-8')).hexdigest()}"
|
|
69
|
-
if algorithm == "sha256":
|
|
70
|
-
return f"{namespace}{hashlib.sha256(key.encode('utf-8')).hexdigest()}"
|
|
71
|
-
if algorithm == "sha512":
|
|
72
|
-
return f"{namespace}{hashlib.sha512(key.encode('utf-8')).hexdigest()}"
|
|
73
|
-
msg = f"Unsupported algorithm: {algorithm}"
|
|
74
|
-
raise ValueError(msg)
|
|
75
|
-
|
|
76
|
-
return _key_encoder
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _value_serializer(value: Sequence[float]) -> bytes:
|
|
80
|
-
"""Serialize a value."""
|
|
81
|
-
return json.dumps(value).encode()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _value_deserializer(serialized_value: bytes) -> list[float]:
|
|
85
|
-
"""Deserialize a value."""
|
|
86
|
-
return cast("list[float]", json.loads(serialized_value.decode()))
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# The warning is global; track emission, so it appears only once.
|
|
90
|
-
_warned_about_sha1: bool = False
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _warn_about_sha1_encoder() -> None:
|
|
94
|
-
"""Emit a one-time warning about SHA-1 collision weaknesses."""
|
|
95
|
-
global _warned_about_sha1 # noqa: PLW0603
|
|
96
|
-
if not _warned_about_sha1:
|
|
97
|
-
warnings.warn(
|
|
98
|
-
"Using default key encoder: SHA-1 is *not* collision-resistant. "
|
|
99
|
-
"While acceptable for most cache scenarios, a motivated attacker "
|
|
100
|
-
"can craft two different payloads that map to the same cache key. "
|
|
101
|
-
"If that risk matters in your environment, supply a stronger "
|
|
102
|
-
"encoder (e.g. SHA-256 or BLAKE2) via the `key_encoder` argument. "
|
|
103
|
-
"If you change the key encoder, consider also creating a new cache, "
|
|
104
|
-
"to avoid (the potential for) collisions with existing keys.",
|
|
105
|
-
category=UserWarning,
|
|
106
|
-
stacklevel=2,
|
|
107
|
-
)
|
|
108
|
-
_warned_about_sha1 = True
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class CacheBackedEmbeddings(Embeddings):
|
|
112
|
-
"""Interface for caching results from embedding models.
|
|
113
|
-
|
|
114
|
-
The interface allows works with any store that implements
|
|
115
|
-
the abstract store interface accepting keys of type str and values of list of
|
|
116
|
-
floats.
|
|
117
|
-
|
|
118
|
-
If need be, the interface can be extended to accept other implementations
|
|
119
|
-
of the value serializer and deserializer, as well as the key encoder.
|
|
120
|
-
|
|
121
|
-
Note that by default only document embeddings are cached. To cache query
|
|
122
|
-
embeddings too, pass in a query_embedding_store to constructor.
|
|
123
|
-
|
|
124
|
-
Examples:
|
|
125
|
-
```python
|
|
126
|
-
from langchain.embeddings import CacheBackedEmbeddings
|
|
127
|
-
from langchain.storage import LocalFileStore
|
|
128
|
-
from langchain_community.embeddings import OpenAIEmbeddings
|
|
129
|
-
|
|
130
|
-
store = LocalFileStore("./my_cache")
|
|
131
|
-
|
|
132
|
-
underlying_embedder = OpenAIEmbeddings()
|
|
133
|
-
embedder = CacheBackedEmbeddings.from_bytes_store(
|
|
134
|
-
underlying_embedder, store, namespace=underlying_embedder.model
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Embedding is computed and cached
|
|
138
|
-
embeddings = embedder.embed_documents(["hello", "goodbye"])
|
|
139
|
-
|
|
140
|
-
# Embeddings are retrieved from the cache, no computation is done
|
|
141
|
-
embeddings = embedder.embed_documents(["hello", "goodbye"])
|
|
142
|
-
```
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
def __init__(
|
|
146
|
-
self,
|
|
147
|
-
underlying_embeddings: Embeddings,
|
|
148
|
-
document_embedding_store: BaseStore[str, list[float]],
|
|
149
|
-
*,
|
|
150
|
-
batch_size: int | None = None,
|
|
151
|
-
query_embedding_store: BaseStore[str, list[float]] | None = None,
|
|
152
|
-
) -> None:
|
|
153
|
-
"""Initialize the embedder.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
underlying_embeddings: the embedder to use for computing embeddings.
|
|
157
|
-
document_embedding_store: The store to use for caching document embeddings.
|
|
158
|
-
batch_size: The number of documents to embed between store updates.
|
|
159
|
-
query_embedding_store: The store to use for caching query embeddings.
|
|
160
|
-
If `None`, query embeddings are not cached.
|
|
161
|
-
"""
|
|
162
|
-
super().__init__()
|
|
163
|
-
self.document_embedding_store = document_embedding_store
|
|
164
|
-
self.query_embedding_store = query_embedding_store
|
|
165
|
-
self.underlying_embeddings = underlying_embeddings
|
|
166
|
-
self.batch_size = batch_size
|
|
167
|
-
|
|
168
|
-
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
169
|
-
"""Embed a list of texts.
|
|
170
|
-
|
|
171
|
-
The method first checks the cache for the embeddings.
|
|
172
|
-
If the embeddings are not found, the method uses the underlying embedder
|
|
173
|
-
to embed the documents and stores the results in the cache.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
texts: A list of texts to embed.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
A list of embeddings for the given texts.
|
|
180
|
-
"""
|
|
181
|
-
vectors: list[list[float] | None] = self.document_embedding_store.mget(
|
|
182
|
-
texts,
|
|
183
|
-
)
|
|
184
|
-
all_missing_indices: list[int] = [i for i, vector in enumerate(vectors) if vector is None]
|
|
185
|
-
|
|
186
|
-
for missing_indices in batch_iterate(self.batch_size, all_missing_indices):
|
|
187
|
-
missing_texts = [texts[i] for i in missing_indices]
|
|
188
|
-
missing_vectors = self.underlying_embeddings.embed_documents(missing_texts)
|
|
189
|
-
self.document_embedding_store.mset(
|
|
190
|
-
list(zip(missing_texts, missing_vectors, strict=False)),
|
|
191
|
-
)
|
|
192
|
-
for index, updated_vector in zip(missing_indices, missing_vectors, strict=False):
|
|
193
|
-
vectors[index] = updated_vector
|
|
194
|
-
|
|
195
|
-
return cast(
|
|
196
|
-
"list[list[float]]",
|
|
197
|
-
vectors,
|
|
198
|
-
) # Nones should have been resolved by now
|
|
199
|
-
|
|
200
|
-
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
201
|
-
"""Embed a list of texts.
|
|
202
|
-
|
|
203
|
-
The method first checks the cache for the embeddings.
|
|
204
|
-
If the embeddings are not found, the method uses the underlying embedder
|
|
205
|
-
to embed the documents and stores the results in the cache.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
texts: A list of texts to embed.
|
|
209
|
-
|
|
210
|
-
Returns:
|
|
211
|
-
A list of embeddings for the given texts.
|
|
212
|
-
"""
|
|
213
|
-
vectors: list[list[float] | None] = await self.document_embedding_store.amget(texts)
|
|
214
|
-
all_missing_indices: list[int] = [i for i, vector in enumerate(vectors) if vector is None]
|
|
215
|
-
|
|
216
|
-
# batch_iterate supports None batch_size which returns all elements at once
|
|
217
|
-
# as a single batch.
|
|
218
|
-
for missing_indices in batch_iterate(self.batch_size, all_missing_indices):
|
|
219
|
-
missing_texts = [texts[i] for i in missing_indices]
|
|
220
|
-
missing_vectors = await self.underlying_embeddings.aembed_documents(
|
|
221
|
-
missing_texts,
|
|
222
|
-
)
|
|
223
|
-
await self.document_embedding_store.amset(
|
|
224
|
-
list(zip(missing_texts, missing_vectors, strict=False)),
|
|
225
|
-
)
|
|
226
|
-
for index, updated_vector in zip(missing_indices, missing_vectors, strict=False):
|
|
227
|
-
vectors[index] = updated_vector
|
|
228
|
-
|
|
229
|
-
return cast(
|
|
230
|
-
"list[list[float]]",
|
|
231
|
-
vectors,
|
|
232
|
-
) # Nones should have been resolved by now
|
|
233
|
-
|
|
234
|
-
def embed_query(self, text: str) -> list[float]:
|
|
235
|
-
"""Embed query text.
|
|
236
|
-
|
|
237
|
-
By default, this method does not cache queries. To enable caching, set the
|
|
238
|
-
`cache_query` parameter to `True` when initializing the embedder.
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
text: The text to embed.
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
The embedding for the given text.
|
|
245
|
-
"""
|
|
246
|
-
if not self.query_embedding_store:
|
|
247
|
-
return self.underlying_embeddings.embed_query(text)
|
|
248
|
-
|
|
249
|
-
(cached,) = self.query_embedding_store.mget([text])
|
|
250
|
-
if cached is not None:
|
|
251
|
-
return cached
|
|
252
|
-
|
|
253
|
-
vector = self.underlying_embeddings.embed_query(text)
|
|
254
|
-
self.query_embedding_store.mset([(text, vector)])
|
|
255
|
-
return vector
|
|
256
|
-
|
|
257
|
-
async def aembed_query(self, text: str) -> list[float]:
|
|
258
|
-
"""Embed query text.
|
|
259
|
-
|
|
260
|
-
By default, this method does not cache queries. To enable caching, set the
|
|
261
|
-
`cache_query` parameter to `True` when initializing the embedder.
|
|
262
|
-
|
|
263
|
-
Args:
|
|
264
|
-
text: The text to embed.
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
The embedding for the given text.
|
|
268
|
-
"""
|
|
269
|
-
if not self.query_embedding_store:
|
|
270
|
-
return await self.underlying_embeddings.aembed_query(text)
|
|
271
|
-
|
|
272
|
-
(cached,) = await self.query_embedding_store.amget([text])
|
|
273
|
-
if cached is not None:
|
|
274
|
-
return cached
|
|
275
|
-
|
|
276
|
-
vector = await self.underlying_embeddings.aembed_query(text)
|
|
277
|
-
await self.query_embedding_store.amset([(text, vector)])
|
|
278
|
-
return vector
|
|
279
|
-
|
|
280
|
-
@classmethod
|
|
281
|
-
def from_bytes_store(
|
|
282
|
-
cls,
|
|
283
|
-
underlying_embeddings: Embeddings,
|
|
284
|
-
document_embedding_cache: ByteStore,
|
|
285
|
-
*,
|
|
286
|
-
namespace: str = "",
|
|
287
|
-
batch_size: int | None = None,
|
|
288
|
-
query_embedding_cache: bool | ByteStore = False,
|
|
289
|
-
key_encoder: Callable[[str], str] | Literal["sha1", "blake2b", "sha256", "sha512"] = "sha1",
|
|
290
|
-
) -> CacheBackedEmbeddings:
|
|
291
|
-
"""On-ramp that adds the necessary serialization and encoding to the store.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
underlying_embeddings: The embedder to use for embedding.
|
|
295
|
-
document_embedding_cache: The cache to use for storing document embeddings.
|
|
296
|
-
namespace: The namespace to use for document cache.
|
|
297
|
-
This namespace is used to avoid collisions with other caches.
|
|
298
|
-
For example, set it to the name of the embedding model used.
|
|
299
|
-
batch_size: The number of documents to embed between store updates.
|
|
300
|
-
query_embedding_cache: The cache to use for storing query embeddings.
|
|
301
|
-
True to use the same cache as document embeddings.
|
|
302
|
-
False to not cache query embeddings.
|
|
303
|
-
key_encoder: Optional callable to encode keys. If not provided,
|
|
304
|
-
a default encoder using SHA-1 will be used. SHA-1 is not
|
|
305
|
-
collision-resistant, and a motivated attacker could craft two
|
|
306
|
-
different texts that hash to the same cache key.
|
|
307
|
-
|
|
308
|
-
New applications should use one of the alternative encoders
|
|
309
|
-
or provide a custom and strong key encoder function to avoid this risk.
|
|
310
|
-
|
|
311
|
-
If you change a key encoder in an existing cache, consider
|
|
312
|
-
just creating a new cache, to avoid (the potential for)
|
|
313
|
-
collisions with existing keys or having duplicate keys
|
|
314
|
-
for the same text in the cache.
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
An instance of CacheBackedEmbeddings that uses the provided cache.
|
|
318
|
-
"""
|
|
319
|
-
if isinstance(key_encoder, str):
|
|
320
|
-
key_encoder = _make_default_key_encoder(namespace, key_encoder)
|
|
321
|
-
elif callable(key_encoder):
|
|
322
|
-
# If a custom key encoder is provided, it should not be used with a
|
|
323
|
-
# namespace.
|
|
324
|
-
# A user can handle namespacing in directly their custom key encoder.
|
|
325
|
-
if namespace:
|
|
326
|
-
msg = (
|
|
327
|
-
"Do not supply `namespace` when using a custom key_encoder; "
|
|
328
|
-
"add any prefixing inside the encoder itself."
|
|
329
|
-
)
|
|
330
|
-
raise ValueError(msg)
|
|
331
|
-
else:
|
|
332
|
-
msg = (
|
|
333
|
-
"key_encoder must be either 'blake2b', 'sha1', 'sha256', 'sha512' "
|
|
334
|
-
"or a callable that encodes keys."
|
|
335
|
-
)
|
|
336
|
-
raise ValueError(msg) # noqa: TRY004
|
|
337
|
-
|
|
338
|
-
document_embedding_store = EncoderBackedStore[str, list[float]](
|
|
339
|
-
document_embedding_cache,
|
|
340
|
-
key_encoder,
|
|
341
|
-
_value_serializer,
|
|
342
|
-
_value_deserializer,
|
|
343
|
-
)
|
|
344
|
-
if query_embedding_cache is True:
|
|
345
|
-
query_embedding_store = document_embedding_store
|
|
346
|
-
elif query_embedding_cache is False:
|
|
347
|
-
query_embedding_store = None
|
|
348
|
-
else:
|
|
349
|
-
query_embedding_store = EncoderBackedStore[str, list[float]](
|
|
350
|
-
query_embedding_cache,
|
|
351
|
-
key_encoder,
|
|
352
|
-
_value_serializer,
|
|
353
|
-
_value_deserializer,
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
return cls(
|
|
357
|
-
underlying_embeddings,
|
|
358
|
-
document_embedding_store,
|
|
359
|
-
batch_size=batch_size,
|
|
360
|
-
query_embedding_store=query_embedding_store,
|
|
361
|
-
)
|
langchain/storage/__init__.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"""Implementations of key-value stores and storage helpers.
|
|
2
|
-
|
|
3
|
-
Module provides implementations of various key-value stores that conform
|
|
4
|
-
to a simple key-value interface.
|
|
5
|
-
|
|
6
|
-
The primary goal of these storages is to support implementation of caching.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from langchain_core.stores import (
|
|
10
|
-
InMemoryByteStore,
|
|
11
|
-
InMemoryStore,
|
|
12
|
-
InvalidKeyException,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
from langchain.storage.encoder_backed import EncoderBackedStore
|
|
16
|
-
|
|
17
|
-
__all__ = [
|
|
18
|
-
"EncoderBackedStore",
|
|
19
|
-
"InMemoryByteStore",
|
|
20
|
-
"InMemoryStore",
|
|
21
|
-
"InvalidKeyException",
|
|
22
|
-
]
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""Encoder-backed store implementation."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
|
4
|
-
from typing import (
|
|
5
|
-
Any,
|
|
6
|
-
TypeVar,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
from langchain_core.stores import BaseStore
|
|
10
|
-
|
|
11
|
-
K = TypeVar("K")
|
|
12
|
-
V = TypeVar("V")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class EncoderBackedStore(BaseStore[K, V]):
|
|
16
|
-
"""Wraps a store with key and value encoders/decoders.
|
|
17
|
-
|
|
18
|
-
Examples that uses JSON for encoding/decoding:
|
|
19
|
-
|
|
20
|
-
```python
|
|
21
|
-
import json
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def key_encoder(key: int) -> str:
|
|
25
|
-
return json.dumps(key)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def value_serializer(value: float) -> str:
|
|
29
|
-
return json.dumps(value)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def value_deserializer(serialized_value: str) -> float:
|
|
33
|
-
return json.loads(serialized_value)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Create an instance of the abstract store
|
|
37
|
-
abstract_store = MyCustomStore()
|
|
38
|
-
|
|
39
|
-
# Create an instance of the encoder-backed store
|
|
40
|
-
store = EncoderBackedStore(
|
|
41
|
-
store=abstract_store,
|
|
42
|
-
key_encoder=key_encoder,
|
|
43
|
-
value_serializer=value_serializer,
|
|
44
|
-
value_deserializer=value_deserializer,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# Use the encoder-backed store methods
|
|
48
|
-
store.mset([(1, 3.14), (2, 2.718)])
|
|
49
|
-
values = store.mget([1, 2]) # Retrieves [3.14, 2.718]
|
|
50
|
-
store.mdelete([1, 2]) # Deletes the keys 1 and 2
|
|
51
|
-
```
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
store: BaseStore[str, Any],
|
|
57
|
-
key_encoder: Callable[[K], str],
|
|
58
|
-
value_serializer: Callable[[V], bytes],
|
|
59
|
-
value_deserializer: Callable[[Any], V],
|
|
60
|
-
) -> None:
|
|
61
|
-
"""Initialize an EncodedStore."""
|
|
62
|
-
self.store = store
|
|
63
|
-
self.key_encoder = key_encoder
|
|
64
|
-
self.value_serializer = value_serializer
|
|
65
|
-
self.value_deserializer = value_deserializer
|
|
66
|
-
|
|
67
|
-
def mget(self, keys: Sequence[K]) -> list[V | None]:
|
|
68
|
-
"""Get the values associated with the given keys."""
|
|
69
|
-
encoded_keys: list[str] = [self.key_encoder(key) for key in keys]
|
|
70
|
-
values = self.store.mget(encoded_keys)
|
|
71
|
-
return [self.value_deserializer(value) if value is not None else value for value in values]
|
|
72
|
-
|
|
73
|
-
async def amget(self, keys: Sequence[K]) -> list[V | None]:
|
|
74
|
-
"""Get the values associated with the given keys."""
|
|
75
|
-
encoded_keys: list[str] = [self.key_encoder(key) for key in keys]
|
|
76
|
-
values = await self.store.amget(encoded_keys)
|
|
77
|
-
return [self.value_deserializer(value) if value is not None else value for value in values]
|
|
78
|
-
|
|
79
|
-
def mset(self, key_value_pairs: Sequence[tuple[K, V]]) -> None:
|
|
80
|
-
"""Set the values for the given keys."""
|
|
81
|
-
encoded_pairs = [
|
|
82
|
-
(self.key_encoder(key), self.value_serializer(value)) for key, value in key_value_pairs
|
|
83
|
-
]
|
|
84
|
-
self.store.mset(encoded_pairs)
|
|
85
|
-
|
|
86
|
-
async def amset(self, key_value_pairs: Sequence[tuple[K, V]]) -> None:
|
|
87
|
-
"""Set the values for the given keys."""
|
|
88
|
-
encoded_pairs = [
|
|
89
|
-
(self.key_encoder(key), self.value_serializer(value)) for key, value in key_value_pairs
|
|
90
|
-
]
|
|
91
|
-
await self.store.amset(encoded_pairs)
|
|
92
|
-
|
|
93
|
-
def mdelete(self, keys: Sequence[K]) -> None:
|
|
94
|
-
"""Delete the given keys and their associated values."""
|
|
95
|
-
encoded_keys = [self.key_encoder(key) for key in keys]
|
|
96
|
-
self.store.mdelete(encoded_keys)
|
|
97
|
-
|
|
98
|
-
async def amdelete(self, keys: Sequence[K]) -> None:
|
|
99
|
-
"""Delete the given keys and their associated values."""
|
|
100
|
-
encoded_keys = [self.key_encoder(key) for key in keys]
|
|
101
|
-
await self.store.amdelete(encoded_keys)
|
|
102
|
-
|
|
103
|
-
def yield_keys(
|
|
104
|
-
self,
|
|
105
|
-
*,
|
|
106
|
-
prefix: str | None = None,
|
|
107
|
-
) -> Iterator[K] | Iterator[str]:
|
|
108
|
-
"""Get an iterator over keys that match the given prefix."""
|
|
109
|
-
# For the time being this does not return K, but str
|
|
110
|
-
# it's for debugging purposes. Should fix this.
|
|
111
|
-
yield from self.store.yield_keys(prefix=prefix)
|
|
112
|
-
|
|
113
|
-
async def ayield_keys(
|
|
114
|
-
self,
|
|
115
|
-
*,
|
|
116
|
-
prefix: str | None = None,
|
|
117
|
-
) -> AsyncIterator[K] | AsyncIterator[str]:
|
|
118
|
-
"""Get an iterator over keys that match the given prefix."""
|
|
119
|
-
# For the time being this does not return K, but str
|
|
120
|
-
# it's for debugging purposes. Should fix this.
|
|
121
|
-
async for key in self.store.ayield_keys(prefix=prefix):
|
|
122
|
-
yield key
|
langchain/storage/exceptions.py
DELETED
langchain/storage/in_memory.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""In memory store that is not thread safe and has no eviction policy.
|
|
2
|
-
|
|
3
|
-
This is a simple implementation of the BaseStore using a dictionary that is useful
|
|
4
|
-
primarily for unit testing purposes.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from langchain_core.stores import InMemoryBaseStore, InMemoryByteStore, InMemoryStore
|
|
8
|
-
|
|
9
|
-
__all__ = [
|
|
10
|
-
"InMemoryBaseStore",
|
|
11
|
-
"InMemoryByteStore",
|
|
12
|
-
"InMemoryStore",
|
|
13
|
-
]
|
|
File without changes
|
|
File without changes
|