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.

@@ -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 ToolNode
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 for m in middleware if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
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: ToolNode | None = None
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
- ToolNode(tools=available_tools, wrap_tool_call=wrap_tool_call_wrapper)
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 for m in middleware if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
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: ToolNode,
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
- Strategy Preserves Identity? Best For
436
- ======== =================== =======================================
437
- `block` N/A Avoid PII completely
438
- `redact` No General compliance, log sanitization
439
- `mask` No Human readability, customer service UIs
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
- # needed as top level import for pydantic schema generation on AgentState
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
- raise NotImplementedError
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
- """Async version of wrap_model_call.
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
- raise NotImplementedError
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
- raise NotImplementedError
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,
@@ -3,10 +3,8 @@
3
3
  from langchain_core.embeddings import Embeddings
4
4
 
5
5
  from langchain.embeddings.base import init_embeddings
6
- from langchain.embeddings.cache import CacheBackedEmbeddings
7
6
 
8
7
  __all__ = [
9
- "CacheBackedEmbeddings",
10
8
  "Embeddings",
11
9
  "init_embeddings",
12
10
  ]
@@ -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
  ]
@@ -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
- "ToolNode",
20
+ "ToolInvocationError",
25
21
  "tool",
26
22
  ]
@@ -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(Exception):
243
- """Exception raised when a tool invocation fails due to invalid arguments."""
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 ToolNode(RunnableCallable):
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: Wrapper function to intercept tool execution. Receives
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 wrap_tool_call wrapper if configured.
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
- result = self._wrap_tool_call(tool_request, execute) # type: ignore[arg-type]
899
- # If result is a coroutine, await it (though wrapper should be sync)
900
- return await result if hasattr(result, "__await__") else result
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.0a13
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=WcuuMvL0g3QbzL--weJC8SFDabEaCuXOdF_tLA6DwAw,57539
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=6KOrK152nbV8ag68459JspXCCmM35N2LJ0PrDW5GLhU,24830
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=_dTVkDSXDhtuvfBOqO74WSFVTdIMAfuIC-gWu2_vh8w,47064
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/documents/__init__.py,sha256=DjuBCy1TQbem4Vz8SsCcGAbZeFwW5KgGPvDrA8e9oGA,94
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/embeddings/cache.py,sha256=znf9DBhrmSpqb64ugwqEqQQaU-7X62HYWxSL9L1fn8g,14267
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/storage/__init__.py,sha256=cvxc63N2nluqyVc7d9MeAj5mmO2iYl3GhcxMCpmqjUk,533
28
- langchain/storage/encoder_backed.py,sha256=z3CS_4kxt7IFv2q8TEfSjMr_EeBp25cloVBINu2KCpE,4179
29
- langchain/storage/exceptions.py,sha256=Fl_8tON3KmByBKwXtno5WSj0-c2RiZxnhw3gv5aS2T8,114
30
- langchain/storage/in_memory.py,sha256=ozrmu0EtaJJVSAzK_u7nzxWpr9OOscWkANHSg-qIVYQ,369
31
- langchain/tools/__init__.py,sha256=tWlUqT7jrnf1ouhMctuUkaYBWEuOPD3JQX4Y8uTHk5w,405
32
- langchain/tools/tool_node.py,sha256=ICsI7x0zkT3nJOCK3TGAEYgho9b8U_lAUsffbdB18Kc,57048
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,,
@@ -1,7 +0,0 @@
1
- """Document."""
2
-
3
- from langchain_core.documents import Document
4
-
5
- __all__ = [
6
- "Document",
7
- ]
@@ -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
- )
@@ -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
@@ -1,5 +0,0 @@
1
- """Store exceptions."""
2
-
3
- from langchain_core.stores import InvalidKeyException
4
-
5
- __all__ = ["InvalidKeyException"]
@@ -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
- ]