langchain 1.0.0a10__py3-none-any.whl → 1.0.0a12__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.
Files changed (35) hide show
  1. langchain/__init__.py +1 -24
  2. langchain/_internal/_documents.py +1 -1
  3. langchain/_internal/_prompts.py +2 -2
  4. langchain/_internal/_typing.py +1 -1
  5. langchain/agents/__init__.py +2 -3
  6. langchain/agents/factory.py +1126 -0
  7. langchain/agents/middleware/__init__.py +38 -1
  8. langchain/agents/middleware/context_editing.py +245 -0
  9. langchain/agents/middleware/human_in_the_loop.py +61 -12
  10. langchain/agents/middleware/model_call_limit.py +177 -0
  11. langchain/agents/middleware/model_fallback.py +94 -0
  12. langchain/agents/middleware/pii.py +753 -0
  13. langchain/agents/middleware/planning.py +201 -0
  14. langchain/agents/middleware/prompt_caching.py +7 -4
  15. langchain/agents/middleware/summarization.py +2 -1
  16. langchain/agents/middleware/tool_call_limit.py +260 -0
  17. langchain/agents/middleware/tool_selection.py +306 -0
  18. langchain/agents/middleware/types.py +708 -127
  19. langchain/agents/structured_output.py +15 -1
  20. langchain/chat_models/base.py +22 -25
  21. langchain/embeddings/base.py +3 -4
  22. langchain/embeddings/cache.py +0 -1
  23. langchain/messages/__init__.py +29 -0
  24. langchain/rate_limiters/__init__.py +13 -0
  25. langchain/tools/tool_node.py +1 -1
  26. {langchain-1.0.0a10.dist-info → langchain-1.0.0a12.dist-info}/METADATA +29 -35
  27. langchain-1.0.0a12.dist-info/RECORD +43 -0
  28. {langchain-1.0.0a10.dist-info → langchain-1.0.0a12.dist-info}/WHEEL +1 -1
  29. langchain/agents/middleware_agent.py +0 -622
  30. langchain/agents/react_agent.py +0 -1229
  31. langchain/globals.py +0 -18
  32. langchain/text_splitter.py +0 -50
  33. langchain-1.0.0a10.dist-info/RECORD +0 -38
  34. langchain-1.0.0a10.dist-info/entry_points.txt +0 -4
  35. {langchain-1.0.0a10.dist-info → langchain-1.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -1,1229 +0,0 @@
1
- """React agent implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- import inspect
6
- from collections.abc import Awaitable, Callable, Sequence
7
- from dataclasses import asdict, is_dataclass
8
- from typing import (
9
- TYPE_CHECKING,
10
- Annotated,
11
- Any,
12
- Generic,
13
- Literal,
14
- cast,
15
- get_type_hints,
16
- )
17
- from warnings import warn
18
-
19
- from langchain_core.language_models import (
20
- BaseChatModel,
21
- LanguageModelInput,
22
- LanguageModelLike,
23
- )
24
- from langchain_core.messages import (
25
- AIMessage,
26
- AnyMessage,
27
- BaseMessage,
28
- SystemMessage,
29
- ToolCall,
30
- ToolMessage,
31
- )
32
- from langchain_core.runnables import (
33
- Runnable,
34
- RunnableConfig,
35
- )
36
- from langgraph._internal._runnable import RunnableCallable, RunnableLike
37
- from langgraph._internal._typing import MISSING
38
- from langgraph.errors import ErrorCode, create_error_message
39
- from langgraph.graph import END, StateGraph
40
- from langgraph.graph.message import add_messages
41
- from langgraph.managed import RemainingSteps # noqa: TC002
42
- from langgraph.types import Checkpointer, Command, Send
43
- from langgraph.typing import ContextT, StateT
44
- from pydantic import BaseModel
45
- from typing_extensions import NotRequired, TypedDict, TypeVar
46
-
47
- from langchain.agents.middleware_agent import create_agent as create_middleware_agent
48
- from langchain.agents.structured_output import (
49
- MultipleStructuredOutputsError,
50
- OutputToolBinding,
51
- ProviderStrategy,
52
- ProviderStrategyBinding,
53
- ResponseFormat,
54
- StructuredOutputValidationError,
55
- ToolStrategy,
56
- )
57
- from langchain.chat_models import init_chat_model
58
- from langchain.tools import ToolNode
59
-
60
- if TYPE_CHECKING:
61
- from langchain_core.tools import BaseTool
62
- from langgraph.graph.state import CompiledStateGraph
63
- from langgraph.runtime import Runtime
64
- from langgraph.store.base import BaseStore
65
-
66
- from langchain.agents._internal._typing import (
67
- SyncOrAsync,
68
- )
69
- from langchain.agents.types import AgentMiddleware
70
-
71
- StructuredResponseT = TypeVar("StructuredResponseT", default=None)
72
-
73
- STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
74
-
75
-
76
- class AgentState(TypedDict):
77
- """The state of the agent."""
78
-
79
- messages: Annotated[Sequence[BaseMessage], add_messages]
80
-
81
- remaining_steps: NotRequired[RemainingSteps]
82
-
83
-
84
- class AgentStatePydantic(BaseModel):
85
- """The state of the agent."""
86
-
87
- messages: Annotated[Sequence[BaseMessage], add_messages]
88
-
89
- remaining_steps: RemainingSteps = 25
90
-
91
-
92
- class AgentStateWithStructuredResponse(AgentState, Generic[StructuredResponseT]):
93
- """The state of the agent with a structured response."""
94
-
95
- structured_response: StructuredResponseT
96
-
97
-
98
- class AgentStateWithStructuredResponsePydantic(AgentStatePydantic, Generic[StructuredResponseT]):
99
- """The state of the agent with a structured response."""
100
-
101
- structured_response: StructuredResponseT
102
-
103
-
104
- PROMPT_RUNNABLE_NAME = "Prompt"
105
-
106
- Prompt = (
107
- SystemMessage
108
- | str
109
- | Callable[[StateT], LanguageModelInput]
110
- | Runnable[StateT, LanguageModelInput]
111
- )
112
-
113
-
114
- def _get_state_value(state: StateT, key: str, default: Any = None) -> Any:
115
- return state.get(key, default) if isinstance(state, dict) else getattr(state, key, default)
116
-
117
-
118
- def _get_prompt_runnable(prompt: Prompt | None) -> Runnable:
119
- prompt_runnable: Runnable
120
- if prompt is None:
121
- prompt_runnable = RunnableCallable(
122
- lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME
123
- )
124
- elif isinstance(prompt, str):
125
- _system_message: BaseMessage = SystemMessage(content=prompt)
126
- prompt_runnable = RunnableCallable(
127
- lambda state: [_system_message, *_get_state_value(state, "messages")],
128
- name=PROMPT_RUNNABLE_NAME,
129
- )
130
- elif isinstance(prompt, SystemMessage):
131
- prompt_runnable = RunnableCallable(
132
- lambda state: [prompt, *_get_state_value(state, "messages")],
133
- name=PROMPT_RUNNABLE_NAME,
134
- )
135
- elif inspect.iscoroutinefunction(prompt):
136
- prompt_runnable = RunnableCallable(
137
- None,
138
- prompt,
139
- name=PROMPT_RUNNABLE_NAME,
140
- )
141
- elif callable(prompt):
142
- prompt_runnable = RunnableCallable(
143
- prompt,
144
- name=PROMPT_RUNNABLE_NAME,
145
- )
146
- elif isinstance(prompt, Runnable):
147
- prompt_runnable = prompt
148
- else:
149
- msg = f"Got unexpected type for `prompt`: {type(prompt)}"
150
- raise ValueError(msg)
151
-
152
- return prompt_runnable
153
-
154
-
155
- def _validate_chat_history(
156
- messages: Sequence[BaseMessage],
157
- ) -> None:
158
- """Validate that all tool calls in AIMessages have a corresponding ToolMessage."""
159
- all_tool_calls = [
160
- tool_call
161
- for message in messages
162
- if isinstance(message, AIMessage)
163
- for tool_call in message.tool_calls
164
- ]
165
- tool_call_ids_with_results = {
166
- message.tool_call_id for message in messages if isinstance(message, ToolMessage)
167
- }
168
- tool_calls_without_results = [
169
- tool_call
170
- for tool_call in all_tool_calls
171
- if tool_call["id"] not in tool_call_ids_with_results
172
- ]
173
- if not tool_calls_without_results:
174
- return
175
-
176
- error_message = create_error_message(
177
- message="Found AIMessages with tool_calls that do not have a corresponding ToolMessage. "
178
- f"Here are the first few of those tool calls: {tool_calls_without_results[:3]}.\n\n"
179
- "Every tool call (LLM requesting to call a tool) in the message history "
180
- "MUST have a corresponding ToolMessage (result of a tool invocation to return to the LLM) -"
181
- " this is required by most LLM providers.",
182
- error_code=ErrorCode.INVALID_CHAT_HISTORY,
183
- )
184
- raise ValueError(error_message)
185
-
186
-
187
- class _AgentBuilder(Generic[StateT, ContextT, StructuredResponseT]):
188
- """Internal builder class for constructing and agent."""
189
-
190
- def __init__(
191
- self,
192
- model: str | BaseChatModel | SyncOrAsync[[StateT, Runtime[ContextT]], BaseChatModel],
193
- tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,
194
- *,
195
- prompt: Prompt | None = None,
196
- response_format: ResponseFormat[StructuredResponseT] | None = None,
197
- pre_model_hook: RunnableLike | None = None,
198
- post_model_hook: RunnableLike | None = None,
199
- state_schema: type[StateT] | None = None,
200
- context_schema: type[ContextT] | None = None,
201
- version: Literal["v1", "v2"] = "v2",
202
- name: str | None = None,
203
- store: BaseStore | None = None,
204
- ) -> None:
205
- self.model = model
206
- self.tools = tools
207
- self.prompt = prompt
208
- self.response_format = response_format
209
- self.pre_model_hook = pre_model_hook
210
- self.post_model_hook = post_model_hook
211
- self.state_schema = state_schema
212
- self.context_schema = context_schema
213
- self.version = version
214
- self.name = name
215
- self.store = store
216
-
217
- if isinstance(model, Runnable) and not isinstance(model, BaseChatModel):
218
- msg = (
219
- "Expected `model` to be a BaseChatModel or a string, got {type(model)}."
220
- "The `model` parameter should not have pre-bound tools, "
221
- "simply pass the model and tools separately."
222
- )
223
- raise ValueError(msg)
224
-
225
- self._setup_tools()
226
- self._setup_state_schema()
227
- self._setup_structured_output()
228
- self._setup_model()
229
-
230
- def _setup_tools(self) -> None:
231
- """Setup tool-related attributes."""
232
- if isinstance(self.tools, ToolNode):
233
- self._tool_classes = list(self.tools.tools_by_name.values())
234
- self._tool_node = self.tools
235
- self._llm_builtin_tools = []
236
- else:
237
- self._llm_builtin_tools = [t for t in self.tools if isinstance(t, dict)]
238
- self._tool_node = ToolNode([t for t in self.tools if not isinstance(t, dict)])
239
- self._tool_classes = list(self._tool_node.tools_by_name.values())
240
-
241
- self._should_return_direct = {t.name for t in self._tool_classes if t.return_direct}
242
- self._tool_calling_enabled = len(self._tool_classes) > 0
243
-
244
- def _setup_structured_output(self) -> None:
245
- """Set up structured output tracking for "tools" and "native" strategies.
246
-
247
- "tools" strategy for structured output:
248
- 1. Converting response format schemas to LangChain tools
249
- 2. Creating metadata for proper response reconstruction
250
- 3. Handling both Pydantic models and dict schemas
251
-
252
- "native" strategy for structured output:
253
- 1. Capturing the schema reference for later parsing
254
- 2. Binding provider-native response_format kwargs at model bind time
255
- 3. Parsing provider-enforced structured output directly into the schema
256
- """
257
- self.structured_output_tools: dict[str, OutputToolBinding[StructuredResponseT]] = {}
258
- self.native_output_binding: ProviderStrategyBinding[StructuredResponseT] | None = None
259
-
260
- if self.response_format is not None:
261
- response_format = self.response_format
262
-
263
- if isinstance(response_format, ToolStrategy):
264
- # check if response_format.schema is a union
265
- for response_schema in response_format.schema_specs:
266
- structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)
267
- self.structured_output_tools[structured_tool_info.tool.name] = (
268
- structured_tool_info
269
- )
270
- elif isinstance(response_format, ProviderStrategy):
271
- # Use native strategy - create ProviderStrategyBinding for parsing
272
- self.native_output_binding = ProviderStrategyBinding.from_schema_spec(
273
- response_format.schema_spec
274
- )
275
- else:
276
- # This shouldn't happen with the new ResponseFormat type, but keeping for safety
277
- msg = (
278
- f"Unsupported response_format type: {type(response_format)}. "
279
- f"Expected ToolStrategy."
280
- )
281
- raise ValueError(msg)
282
-
283
- def _setup_state_schema(self) -> None:
284
- """Setup state schema with validation."""
285
- if self.state_schema is not None:
286
- required_keys = {"messages", "remaining_steps"}
287
- if self.response_format is not None:
288
- required_keys.add("structured_response")
289
-
290
- schema_keys = set(get_type_hints(self.state_schema))
291
- if missing_keys := required_keys - schema_keys:
292
- msg = f"Missing required key(s) {missing_keys} in state_schema"
293
- raise ValueError(msg)
294
-
295
- self._final_state_schema = self.state_schema
296
- else:
297
- self._final_state_schema = (
298
- AgentStateWithStructuredResponse # type: ignore[assignment]
299
- if self.response_format is not None
300
- else AgentState
301
- )
302
-
303
- def _handle_structured_response_tool_calls(self, response: AIMessage) -> Command | None:
304
- """Handle tool calls that match structured output tools using the tools strategy.
305
-
306
- Args:
307
- response: The AI message containing potential tool calls
308
-
309
- Returns:
310
- Command with structured response update if found, None otherwise
311
-
312
- Raises:
313
- MultipleStructuredOutputsError: If multiple structured responses are returned
314
- and error handling is disabled
315
- StructuredOutputParsingError: If parsing fails and error handling is disabled
316
- """
317
- if not isinstance(self.response_format, ToolStrategy) or not response.tool_calls:
318
- return None
319
-
320
- structured_tool_calls = [
321
- tool_call
322
- for tool_call in response.tool_calls
323
- if tool_call["name"] in self.structured_output_tools
324
- ]
325
-
326
- if not structured_tool_calls:
327
- return None
328
-
329
- if len(structured_tool_calls) > 1:
330
- return self._handle_multiple_structured_outputs(response, structured_tool_calls)
331
-
332
- return self._handle_single_structured_output(response, structured_tool_calls[0])
333
-
334
- def _handle_multiple_structured_outputs(
335
- self,
336
- response: AIMessage,
337
- structured_tool_calls: list[ToolCall],
338
- ) -> Command:
339
- """Handle multiple structured output tool calls."""
340
- tool_names = [tool_call["name"] for tool_call in structured_tool_calls]
341
- exception = MultipleStructuredOutputsError(tool_names)
342
-
343
- should_retry, error_message = self._handle_structured_output_error(exception)
344
-
345
- if not should_retry:
346
- raise exception
347
-
348
- tool_messages = [
349
- ToolMessage(
350
- content=error_message,
351
- tool_call_id=tool_call["id"],
352
- name=tool_call["name"],
353
- )
354
- for tool_call in structured_tool_calls
355
- ]
356
-
357
- return Command(
358
- update={"messages": [response, *tool_messages]},
359
- goto="agent",
360
- )
361
-
362
- def _handle_single_structured_output(
363
- self,
364
- response: AIMessage,
365
- tool_call: Any,
366
- ) -> Command:
367
- """Handle a single structured output tool call."""
368
- structured_tool_binding = self.structured_output_tools[tool_call["name"]]
369
-
370
- try:
371
- structured_response = structured_tool_binding.parse(tool_call["args"])
372
-
373
- if isinstance(structured_response, BaseModel):
374
- structured_response_dict = structured_response.model_dump()
375
- elif is_dataclass(structured_response):
376
- structured_response_dict = asdict(structured_response) # type: ignore[arg-type]
377
- else:
378
- structured_response_dict = cast("dict", structured_response)
379
-
380
- tool_message_content = (
381
- self.response_format.tool_message_content
382
- if isinstance(self.response_format, ToolStrategy)
383
- and self.response_format.tool_message_content
384
- else f"Returning structured response: {structured_response_dict}"
385
- )
386
-
387
- return Command(
388
- update={
389
- "messages": [
390
- response,
391
- ToolMessage(
392
- content=tool_message_content,
393
- tool_call_id=tool_call["id"],
394
- name=tool_call["name"],
395
- ),
396
- ],
397
- "structured_response": structured_response,
398
- }
399
- )
400
- except Exception as exc: # noqa: BLE001
401
- exception = StructuredOutputValidationError(tool_call["name"], exc)
402
-
403
- should_retry, error_message = self._handle_structured_output_error(exception)
404
-
405
- if not should_retry:
406
- raise exception
407
-
408
- return Command(
409
- update={
410
- "messages": [
411
- response,
412
- ToolMessage(
413
- content=error_message,
414
- tool_call_id=tool_call["id"],
415
- name=tool_call["name"],
416
- ),
417
- ],
418
- },
419
- goto="agent",
420
- )
421
-
422
- def _handle_structured_output_error(
423
- self,
424
- exception: Exception,
425
- ) -> tuple[bool, str]:
426
- """Handle structured output error.
427
-
428
- Returns (should_retry, retry_tool_message).
429
- """
430
- handle_errors = cast("ToolStrategy", self.response_format).handle_errors
431
-
432
- if handle_errors is False:
433
- return False, ""
434
- if handle_errors is True:
435
- return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
436
- if isinstance(handle_errors, str):
437
- return True, handle_errors
438
- if isinstance(handle_errors, type) and issubclass(handle_errors, Exception):
439
- if isinstance(exception, handle_errors):
440
- return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
441
- return False, ""
442
- if isinstance(handle_errors, tuple):
443
- if any(isinstance(exception, exc_type) for exc_type in handle_errors):
444
- return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))
445
- return False, ""
446
- if callable(handle_errors):
447
- return True, handle_errors(exception) # type: ignore[call-arg, return-value]
448
- return False, ""
449
-
450
- def _apply_native_output_binding(self, model: LanguageModelLike) -> LanguageModelLike:
451
- """If native output is configured, bind provider-native kwargs onto the model."""
452
- if not isinstance(self.response_format, ProviderStrategy):
453
- return model
454
- kwargs = self.response_format.to_model_kwargs()
455
- return model.bind(**kwargs)
456
-
457
- def _handle_structured_response_native(self, response: AIMessage) -> Command | None:
458
- """Handle structured output using the native output.
459
-
460
- If native output is configured and there are no tool calls,
461
- parse using ProviderStrategyBinding.
462
- """
463
- if self.native_output_binding is None:
464
- return None
465
- if response.tool_calls:
466
- # if the model chooses to call tools, we let the normal flow handle it
467
- return None
468
-
469
- structured_response = self.native_output_binding.parse(response)
470
-
471
- return Command(update={"messages": [response], "structured_response": structured_response})
472
-
473
- def _setup_model(self) -> None:
474
- """Setup model-related attributes."""
475
- self._is_dynamic_model = not isinstance(self.model, (str, Runnable)) and callable(
476
- self.model
477
- )
478
- self._is_async_dynamic_model = self._is_dynamic_model and inspect.iscoroutinefunction(
479
- self.model
480
- )
481
-
482
- if not self._is_dynamic_model:
483
- model = self.model
484
- if isinstance(model, str):
485
- model = init_chat_model(model)
486
-
487
- # Collect all tools: regular tools + structured output tools
488
- structured_output_tools = list(self.structured_output_tools.values())
489
- all_tools = (
490
- self._tool_classes
491
- + self._llm_builtin_tools
492
- + [info.tool for info in structured_output_tools]
493
- )
494
-
495
- if len(all_tools) > 0:
496
- # Check if we need to force tool use for structured output
497
- tool_choice = None
498
- if self.response_format is not None and isinstance(
499
- self.response_format, ToolStrategy
500
- ):
501
- tool_choice = "any"
502
-
503
- if tool_choice:
504
- model = cast("BaseChatModel", model).bind_tools( # type: ignore[assignment]
505
- all_tools, tool_choice=tool_choice
506
- )
507
- # If native output is configured, bind tools with strict=True. Required for OpenAI.
508
- elif isinstance(self.response_format, ProviderStrategy):
509
- model = cast("BaseChatModel", model).bind_tools( # type: ignore[assignment]
510
- all_tools, strict=True
511
- )
512
- else:
513
- model = cast("BaseChatModel", model).bind_tools(all_tools) # type: ignore[assignment]
514
-
515
- # bind native structured-output kwargs
516
- model = self._apply_native_output_binding(model) # type: ignore[assignment, arg-type]
517
-
518
- # Extract just the model part for direct invocation
519
- self._static_model: Runnable | None = model # type: ignore[assignment]
520
- else:
521
- self._static_model = None
522
-
523
- def _resolve_model(self, state: StateT, runtime: Runtime[ContextT]) -> LanguageModelLike:
524
- """Resolve the model to use, handling both static and dynamic models."""
525
- if self._is_dynamic_model:
526
- dynamic_model = self.model(state, runtime) # type: ignore[operator, arg-type]
527
- return self._apply_native_output_binding(dynamic_model) # type: ignore[arg-type]
528
- return self._static_model # type: ignore[return-value]
529
-
530
- async def _aresolve_model(self, state: StateT, runtime: Runtime[ContextT]) -> LanguageModelLike:
531
- """Async resolve the model to use, handling both static and dynamic models."""
532
- if self._is_async_dynamic_model:
533
- dynamic_model = cast(
534
- "Callable[[StateT, Runtime[ContextT]], Awaitable[BaseChatModel]]",
535
- self.model,
536
- )
537
- return await dynamic_model(state, runtime)
538
- if self._is_dynamic_model:
539
- dynamic_model = self.model(state, runtime) # type: ignore[arg-type, assignment, operator]
540
- return self._apply_native_output_binding(dynamic_model) # type: ignore[arg-type]
541
- return self._static_model # type: ignore[return-value]
542
-
543
- def create_model_node(self) -> RunnableCallable:
544
- """Create the 'agent' node that calls the LLM."""
545
-
546
- def _get_model_input_state(state: StateT) -> StateT:
547
- if self.pre_model_hook is not None:
548
- messages = _get_state_value(state, "llm_input_messages") or _get_state_value(
549
- state, "messages"
550
- )
551
- error_msg = (
552
- f"Expected input to call_model to have 'llm_input_messages' "
553
- f"or 'messages' key, but got {state}"
554
- )
555
- else:
556
- messages = _get_state_value(state, "messages")
557
- error_msg = f"Expected input to call_model to have 'messages' key, but got {state}"
558
-
559
- if messages is None:
560
- raise ValueError(error_msg)
561
-
562
- _validate_chat_history(messages)
563
-
564
- if isinstance(self._final_state_schema, type) and issubclass(
565
- self._final_state_schema, BaseModel
566
- ):
567
- # we're passing messages under `messages` key, as this
568
- # is expected by the prompt
569
- state.messages = messages # type: ignore[union-attr]
570
- else:
571
- state["messages"] = messages # type: ignore[index]
572
- return state
573
-
574
- def _are_more_steps_needed(state: StateT, response: BaseMessage) -> bool:
575
- has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
576
- all_tools_return_direct = (
577
- all(call["name"] in self._should_return_direct for call in response.tool_calls)
578
- if isinstance(response, AIMessage)
579
- else False
580
- )
581
- remaining_steps = _get_state_value(state, "remaining_steps", None)
582
- return (
583
- remaining_steps is not None # type: ignore[return-value]
584
- and (
585
- (remaining_steps < 1 and all_tools_return_direct)
586
- or (remaining_steps < 2 and has_tool_calls)
587
- )
588
- )
589
-
590
- def call_model(
591
- state: StateT, runtime: Runtime[ContextT], config: RunnableConfig
592
- ) -> dict[str, Any] | Command:
593
- """Call the model with the current state and return the response."""
594
- if self._is_async_dynamic_model:
595
- msg = (
596
- "Async model callable provided but agent invoked synchronously. "
597
- "Use agent.ainvoke() or agent.astream(), or provide a sync model callable."
598
- )
599
- raise RuntimeError(msg)
600
-
601
- model_input = _get_model_input_state(state)
602
- model = self._resolve_model(state, runtime)
603
-
604
- # Get prompt runnable and invoke it first to prepare messages
605
- prompt_runnable = _get_prompt_runnable(self.prompt)
606
- prepared_messages = prompt_runnable.invoke(model_input, config)
607
-
608
- # Then invoke the model with the prepared messages
609
- response = cast("AIMessage", model.invoke(prepared_messages, config))
610
- response.name = self.name
611
-
612
- if _are_more_steps_needed(state, response):
613
- return {
614
- "messages": [
615
- AIMessage(
616
- id=response.id,
617
- content="Sorry, need more steps to process this request.",
618
- )
619
- ]
620
- }
621
-
622
- # Check if any tool calls match structured output tools
623
- structured_command = self._handle_structured_response_tool_calls(response)
624
- if structured_command:
625
- return structured_command
626
-
627
- # Native structured output
628
- native_command = self._handle_structured_response_native(response)
629
- if native_command:
630
- return native_command
631
-
632
- return {"messages": [response]}
633
-
634
- async def acall_model(
635
- state: StateT, runtime: Runtime[ContextT], config: RunnableConfig
636
- ) -> dict[str, Any] | Command:
637
- """Call the model with the current state and return the response."""
638
- model_input = _get_model_input_state(state)
639
-
640
- model = await self._aresolve_model(state, runtime)
641
-
642
- # Get prompt runnable and invoke it first to prepare messages
643
- prompt_runnable = _get_prompt_runnable(self.prompt)
644
- prepared_messages = await prompt_runnable.ainvoke(model_input, config)
645
-
646
- # Then invoke the model with the prepared messages
647
- response = cast(
648
- "AIMessage",
649
- await model.ainvoke(prepared_messages, config),
650
- )
651
- response.name = self.name
652
- if _are_more_steps_needed(state, response):
653
- return {
654
- "messages": [
655
- AIMessage(
656
- id=response.id,
657
- content="Sorry, need more steps to process this request.",
658
- )
659
- ]
660
- }
661
-
662
- # Check if any tool calls match structured output tools
663
- structured_command = self._handle_structured_response_tool_calls(response)
664
- if structured_command:
665
- return structured_command
666
-
667
- # Native structured output
668
- native_command = self._handle_structured_response_native(response)
669
- if native_command:
670
- return native_command
671
-
672
- return {"messages": [response]}
673
-
674
- return RunnableCallable(call_model, acall_model)
675
-
676
- def _get_input_schema(self) -> type[StateT]:
677
- """Get input schema for model node."""
678
- if self.pre_model_hook is not None:
679
- if isinstance(self._final_state_schema, type) and issubclass(
680
- self._final_state_schema, BaseModel
681
- ):
682
- from pydantic import create_model
683
-
684
- return create_model(
685
- "CallModelInputSchema",
686
- llm_input_messages=(list[AnyMessage], ...),
687
- __base__=self._final_state_schema,
688
- )
689
-
690
- class CallModelInputSchema(self._final_state_schema): # type: ignore[name-defined, misc]
691
- llm_input_messages: list[AnyMessage]
692
-
693
- return CallModelInputSchema
694
- return self._final_state_schema
695
-
696
- def create_model_router(self) -> Callable[[StateT], str | list[Send]]:
697
- """Create routing function for model node conditional edges."""
698
-
699
- def should_continue(state: StateT) -> str | list[Send]:
700
- messages = _get_state_value(state, "messages")
701
- last_message = messages[-1]
702
-
703
- # Check if the last message is a ToolMessage from a structured tool.
704
- # This condition exists to support structured output via tools.
705
- # Once a tool has been called for structured output, we skip
706
- # tool execution and go to END (if there is no post_model_hook).
707
- if (
708
- isinstance(last_message, ToolMessage)
709
- and last_message.name in self.structured_output_tools
710
- ):
711
- return END
712
-
713
- if isinstance(last_message, ToolMessage):
714
- return END
715
-
716
- if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
717
- if self.post_model_hook is not None:
718
- return "post_model_hook"
719
- return END
720
- if self.version == "v1":
721
- return "tools"
722
- if self.version == "v2":
723
- if self.post_model_hook is not None:
724
- return "post_model_hook"
725
- tool_calls = [
726
- self._tool_node.inject_tool_args(call, state, self.store) # type: ignore[arg-type]
727
- for call in last_message.tool_calls
728
- ]
729
- return [Send("tools", [tool_call]) for tool_call in tool_calls]
730
- return None
731
-
732
- return should_continue
733
-
734
- def create_post_model_hook_router(
735
- self,
736
- ) -> Callable[[StateT], str | list[Send]]:
737
- """Create a routing function for post_model_hook node conditional edges."""
738
-
739
- def post_model_hook_router(state: StateT) -> str | list[Send]:
740
- messages = _get_state_value(state, "messages")
741
-
742
- # Check if the last message is a ToolMessage from a structured tool.
743
- # This condition exists to support structured output via tools.
744
- # Once a tool has been called for structured output, we skip
745
- # tool execution and go to END (if there is no post_model_hook).
746
- last_message = messages[-1]
747
- if (
748
- isinstance(last_message, ToolMessage)
749
- and last_message.name in self.structured_output_tools
750
- ):
751
- return END
752
-
753
- tool_messages = [m.tool_call_id for m in messages if isinstance(m, ToolMessage)]
754
- last_ai_message = next(m for m in reversed(messages) if isinstance(m, AIMessage))
755
- pending_tool_calls = [
756
- c for c in last_ai_message.tool_calls if c["id"] not in tool_messages
757
- ]
758
-
759
- if pending_tool_calls:
760
- pending_tool_calls = [
761
- self._tool_node.inject_tool_args(call, state, self.store) # type: ignore[arg-type]
762
- for call in pending_tool_calls
763
- ]
764
- return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]
765
- if isinstance(messages[-1], ToolMessage):
766
- return self._get_entry_point()
767
- return END
768
-
769
- return post_model_hook_router
770
-
771
- def create_tools_router(self) -> Callable[[StateT], str] | None:
772
- """Create a routing function for tools node conditional edges."""
773
- if not self._should_return_direct:
774
- return None
775
-
776
- def route_tool_responses(state: StateT) -> str:
777
- messages = _get_state_value(state, "messages")
778
- for m in reversed(messages):
779
- if not isinstance(m, ToolMessage):
780
- break
781
- if m.name in self._should_return_direct:
782
- return END
783
-
784
- if (
785
- isinstance(m, AIMessage)
786
- and m.tool_calls
787
- and any(call["name"] in self._should_return_direct for call in m.tool_calls)
788
- ):
789
- return END
790
-
791
- return self._get_entry_point()
792
-
793
- return route_tool_responses
794
-
795
- def _get_entry_point(self) -> str:
796
- """Get the workflow entry point."""
797
- return "pre_model_hook" if self.pre_model_hook else "agent"
798
-
799
- def _get_model_paths(self) -> list[str]:
800
- """Get possible edge destinations from model node."""
801
- paths = []
802
- if self._tool_calling_enabled:
803
- paths.append("tools")
804
- if self.post_model_hook:
805
- paths.append("post_model_hook")
806
- else:
807
- paths.append(END)
808
-
809
- return paths
810
-
811
- def _get_post_model_hook_paths(self) -> list[str]:
812
- """Get possible edge destinations from post_model_hook node."""
813
- paths = []
814
- if self._tool_calling_enabled:
815
- paths = [self._get_entry_point(), "tools"]
816
- paths.append(END)
817
- return paths
818
-
819
- def build(self) -> StateGraph[StateT, ContextT]:
820
- """Build the agent workflow graph (uncompiled)."""
821
- workflow = StateGraph(
822
- state_schema=self._final_state_schema,
823
- context_schema=self.context_schema,
824
- )
825
-
826
- # Set entry point
827
- workflow.set_entry_point(self._get_entry_point())
828
-
829
- # Add nodes
830
- workflow.add_node("agent", self.create_model_node(), input_schema=self._get_input_schema())
831
-
832
- if self._tool_calling_enabled:
833
- workflow.add_node("tools", self._tool_node)
834
-
835
- if self.pre_model_hook:
836
- workflow.add_node("pre_model_hook", self.pre_model_hook) # type: ignore[arg-type]
837
-
838
- if self.post_model_hook:
839
- workflow.add_node("post_model_hook", self.post_model_hook) # type: ignore[arg-type]
840
-
841
- # Add edges
842
- if self.pre_model_hook:
843
- workflow.add_edge("pre_model_hook", "agent")
844
-
845
- if self.post_model_hook:
846
- workflow.add_edge("agent", "post_model_hook")
847
- post_hook_paths = self._get_post_model_hook_paths()
848
- if len(post_hook_paths) == 1:
849
- # No need for a conditional edge if there's only one path
850
- workflow.add_edge("post_model_hook", post_hook_paths[0])
851
- else:
852
- workflow.add_conditional_edges(
853
- "post_model_hook",
854
- self.create_post_model_hook_router(),
855
- path_map=post_hook_paths,
856
- )
857
- else:
858
- model_paths = self._get_model_paths()
859
- if len(model_paths) == 1:
860
- # No need for a conditional edge if there's only one path
861
- workflow.add_edge("agent", model_paths[0])
862
- else:
863
- workflow.add_conditional_edges(
864
- "agent",
865
- self.create_model_router(),
866
- path_map=model_paths,
867
- )
868
-
869
- if self._tool_calling_enabled:
870
- # In some cases, tools can return directly. In these cases
871
- # we add a conditional edge from the tools node to the END node
872
- # instead of going to the entry point.
873
- tools_router = self.create_tools_router()
874
- if tools_router:
875
- workflow.add_conditional_edges(
876
- "tools",
877
- tools_router,
878
- path_map=[self._get_entry_point(), END],
879
- )
880
- else:
881
- workflow.add_edge("tools", self._get_entry_point())
882
-
883
- return workflow
884
-
885
-
886
- def _supports_native_structured_output(
887
- model: str | BaseChatModel | SyncOrAsync[[StateT, Runtime[ContextT]], BaseChatModel],
888
- ) -> bool:
889
- """Check if a model supports native structured output.
890
-
891
- TODO: replace with more robust model profiles.
892
- """
893
- model_name: str | None = None
894
- if isinstance(model, str):
895
- model_name = model
896
- elif isinstance(model, BaseChatModel):
897
- model_name = getattr(model, "model_name", None)
898
-
899
- return (
900
- "grok" in model_name.lower()
901
- or any(part in model_name for part in ["gpt-5", "gpt-4.1", "gpt-oss", "o3-pro", "o3-mini"])
902
- if model_name
903
- else False
904
- )
905
-
906
-
907
- def create_agent( # noqa: D417
908
- model: str | BaseChatModel | SyncOrAsync[[StateT, Runtime[ContextT]], BaseChatModel],
909
- tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,
910
- *,
911
- middleware: Sequence[AgentMiddleware] = (),
912
- prompt: Prompt | None = None,
913
- response_format: ToolStrategy[StructuredResponseT]
914
- | ProviderStrategy[StructuredResponseT]
915
- | type[StructuredResponseT]
916
- | None = None,
917
- pre_model_hook: RunnableLike | None = None,
918
- post_model_hook: RunnableLike | None = None,
919
- state_schema: type[StateT] | None = None,
920
- context_schema: type[ContextT] | None = None,
921
- checkpointer: Checkpointer | None = None,
922
- store: BaseStore | None = None,
923
- interrupt_before: list[str] | None = None,
924
- interrupt_after: list[str] | None = None,
925
- debug: bool = False,
926
- version: Literal["v1", "v2"] = "v2",
927
- name: str | None = None,
928
- **deprecated_kwargs: Any,
929
- ) -> CompiledStateGraph[StateT, ContextT]:
930
- """Creates an agent graph that calls tools in a loop until a stopping condition is met.
931
-
932
- For more details on using `create_agent`,
933
- visit [Agents](https://langchain-ai.github.io/langgraph/agents/overview/) documentation.
934
-
935
- Args:
936
- model: The language model for the agent. Supports static and dynamic
937
- model selection.
938
-
939
- - **Static model**: A chat model instance (e.g., `ChatOpenAI()`) or
940
- string identifier (e.g., `"openai:gpt-4"`)
941
- - **Dynamic model**: A callable with signature
942
- `(state, runtime) -> BaseChatModel` that returns different models
943
- based on runtime context
944
- If the model has tools bound via `.bind_tools()` or other configurations,
945
- the return type should be a Runnable[LanguageModelInput, BaseMessage]
946
- Coroutines are also supported, allowing for asynchronous model selection.
947
-
948
- Dynamic functions receive graph state and runtime, enabling
949
- context-dependent model selection. Must return a `BaseChatModel`
950
- instance. For tool calling, bind tools using `.bind_tools()`.
951
- Bound tools must be a subset of the `tools` parameter.
952
-
953
- Dynamic model example:
954
- ```python
955
- from dataclasses import dataclass
956
-
957
-
958
- @dataclass
959
- class ModelContext:
960
- model_name: str = "gpt-3.5-turbo"
961
-
962
-
963
- # Instantiate models globally
964
- gpt4_model = ChatOpenAI(model="gpt-4")
965
- gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")
966
-
967
-
968
- def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:
969
- model_name = runtime.context.model_name
970
- model = gpt4_model if model_name == "gpt-4" else gpt35_model
971
- return model.bind_tools(tools)
972
- ```
973
-
974
- .. note::
975
- Ensure returned models have appropriate tools bound via
976
- `.bind_tools()` and support required functionality. Bound tools
977
- must be a subset of those specified in the `tools` parameter.
978
-
979
- tools: A list of tools or a ToolNode instance.
980
- If an empty list is provided, the agent will consist of a single LLM node
981
- without tool calling.
982
- prompt: An optional prompt for the LLM. Can take a few different forms:
983
-
984
- - str: This is converted to a SystemMessage and added to the beginning
985
- of the list of messages in state["messages"].
986
- - SystemMessage: this is added to the beginning of the list of messages
987
- in state["messages"].
988
- - Callable: This function should take in full graph state and the output is
989
- then passed to the language model.
990
- - Runnable: This runnable should take in full graph state and the output is
991
- then passed to the language model.
992
-
993
- response_format: An optional UsingToolStrategy configuration for structured responses.
994
-
995
- If provided, the agent will handle structured output via tool calls
996
- during the normal conversation flow.
997
- When the model calls a structured output tool, the response will be captured
998
- and returned in the 'structured_response' state key.
999
- If not provided, `structured_response` will not be present in the output state.
1000
-
1001
- The UsingToolStrategy should contain:
1002
-
1003
- - schemas: A sequence of ResponseSchema objects that define
1004
- the structured output format
1005
- - tool_choice: Either "required" or "auto" to control when structured
1006
- output is used
1007
-
1008
- Each ResponseSchema contains:
1009
-
1010
- - schema: A Pydantic model that defines the structure
1011
- - name: Optional custom name for the tool (defaults to model name)
1012
- - description: Optional custom description (defaults to model docstring)
1013
- - strict: Whether to enforce strict validation
1014
-
1015
- .. important::
1016
- `response_format` requires the model to support tool calling
1017
-
1018
- .. note::
1019
- Structured responses are handled directly in the model call node via
1020
- tool calls, eliminating the need for separate structured response nodes.
1021
-
1022
- pre_model_hook: An optional node to add before the `agent` node
1023
- (i.e., the node that calls the LLM).
1024
- Useful for managing long message histories
1025
- (e.g., message trimming, summarization, etc.).
1026
- Pre-model hook must be a callable or a runnable that takes in current
1027
- graph state and returns a state update in the form of
1028
- ```python
1029
- # At least one of `messages` or `llm_input_messages` MUST be provided
1030
- {
1031
- # If provided, will UPDATE the `messages` in the state
1032
- "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), ...],
1033
- # If provided, will be used as the input to the LLM,
1034
- # and will NOT UPDATE `messages` in the state
1035
- "llm_input_messages": [...],
1036
- # Any other state keys that need to be propagated
1037
- ...
1038
- }
1039
- ```
1040
-
1041
- .. important::
1042
- At least one of `messages` or `llm_input_messages` MUST be provided
1043
- and will be used as an input to the `agent` node.
1044
- The rest of the keys will be added to the graph state.
1045
-
1046
- .. warning::
1047
- If you are returning `messages` in the pre-model hook,
1048
- you should OVERWRITE the `messages` key by doing the following:
1049
-
1050
- ```python
1051
- {
1052
- "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *new_messages]
1053
- ...
1054
- }
1055
- ```
1056
- post_model_hook: An optional node to add after the `agent` node
1057
- (i.e., the node that calls the LLM).
1058
- Useful for implementing human-in-the-loop, guardrails, validation,
1059
- or other post-processing.
1060
- Post-model hook must be a callable or a runnable that takes in
1061
- current graph state and returns a state update.
1062
-
1063
- .. note::
1064
- Only available with `version="v2"`.
1065
- state_schema: An optional state schema that defines graph state.
1066
- Must have `messages` and `remaining_steps` keys.
1067
- Defaults to `AgentState` that defines those two keys.
1068
- context_schema: An optional schema for runtime context.
1069
- checkpointer: An optional checkpoint saver object. This is used for persisting
1070
- the state of the graph (e.g., as chat memory) for a single thread
1071
- (e.g., a single conversation).
1072
- store: An optional store object. This is used for persisting data
1073
- across multiple threads (e.g., multiple conversations / users).
1074
- interrupt_before: An optional list of node names to interrupt before.
1075
- Should be one of the following: "agent", "tools".
1076
- This is useful if you want to add a user confirmation or other interrupt
1077
- before taking an action.
1078
- interrupt_after: An optional list of node names to interrupt after.
1079
- Should be one of the following: "agent", "tools".
1080
- This is useful if you want to return directly or run additional processing on an output.
1081
- debug: A flag indicating whether to enable debug mode.
1082
- version: Determines the version of the graph to create.
1083
- Can be one of:
1084
-
1085
- - `"v1"`: The tool node processes a single message. All tool
1086
- calls in the message are executed in parallel within the tool node.
1087
- - `"v2"`: The tool node processes a tool call.
1088
- Tool calls are distributed across multiple instances of the tool
1089
- node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)
1090
- API.
1091
- name: An optional name for the CompiledStateGraph.
1092
- This name will be automatically used when adding ReAct agent graph to
1093
- another graph as a subgraph node -
1094
- particularly useful for building multi-agent systems.
1095
-
1096
- .. warning::
1097
- The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.
1098
- Please use `context_schema` instead to specify the schema for run-scoped context.
1099
-
1100
-
1101
- Returns:
1102
- A compiled LangChain runnable that can be used for chat interactions.
1103
-
1104
- The "agent" node calls the language model with the messages list (after applying the prompt).
1105
- If the resulting AIMessage contains `tool_calls`,
1106
- the graph will then call the ["tools"][langgraph.prebuilt.tool_node.ToolNode].
1107
- The "tools" node executes the tools (1 tool per `tool_call`)
1108
- and adds the responses to the messages list as `ToolMessage` objects.
1109
- The agent node then calls the language model again.
1110
- The process repeats until no more `tool_calls` are present in the response.
1111
- The agent then returns the full list of messages as a dictionary containing the key "messages".
1112
-
1113
- ``` mermaid
1114
- sequenceDiagram
1115
- participant U as User
1116
- participant A as LLM
1117
- participant T as Tools
1118
- U->>A: Initial input
1119
- Note over A: Prompt + LLM
1120
- loop while tool_calls present
1121
- A->>T: Execute tools
1122
- T-->>A: ToolMessage for each tool_calls
1123
- end
1124
- A->>U: Return final state
1125
- ```
1126
-
1127
- Example:
1128
- ```python
1129
- from langchain.agents import create_agent
1130
-
1131
- def check_weather(location: str) -> str:
1132
- '''Return the weather forecast for the specified location.'''
1133
- return f"It's always sunny in {location}"
1134
-
1135
- graph = create_agent(
1136
- "anthropic:claude-3-7-sonnet-latest",
1137
- tools=[check_weather],
1138
- prompt="You are a helpful assistant",
1139
- )
1140
- inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}
1141
- for chunk in graph.stream(inputs, stream_mode="updates"):
1142
- print(chunk)
1143
- ```
1144
- """
1145
- if middleware:
1146
- assert isinstance(model, str | BaseChatModel) # noqa: S101
1147
- assert isinstance(prompt, str | None) # noqa: S101
1148
- assert not isinstance(response_format, tuple) # noqa: S101
1149
- assert pre_model_hook is None # noqa: S101
1150
- assert post_model_hook is None # noqa: S101
1151
- assert state_schema is None # noqa: S101
1152
- return create_middleware_agent( # type: ignore[return-value]
1153
- model=model,
1154
- tools=tools,
1155
- system_prompt=prompt,
1156
- middleware=middleware,
1157
- response_format=response_format,
1158
- context_schema=context_schema,
1159
- ).compile(
1160
- checkpointer=checkpointer,
1161
- store=store,
1162
- name=name,
1163
- interrupt_after=interrupt_after,
1164
- interrupt_before=interrupt_before,
1165
- debug=debug,
1166
- )
1167
-
1168
- # Handle deprecated config_schema parameter
1169
- if (config_schema := deprecated_kwargs.pop("config_schema", MISSING)) is not MISSING:
1170
- warn(
1171
- "`config_schema` is deprecated and will be removed. "
1172
- "Please use `context_schema` instead.",
1173
- category=DeprecationWarning,
1174
- stacklevel=2,
1175
- )
1176
- if context_schema is None:
1177
- context_schema = config_schema
1178
-
1179
- if len(deprecated_kwargs) > 0:
1180
- msg = f"create_agent() got unexpected keyword arguments: {deprecated_kwargs}"
1181
- raise TypeError(msg)
1182
-
1183
- if response_format and not isinstance(response_format, (ToolStrategy, ProviderStrategy)):
1184
- if _supports_native_structured_output(model):
1185
- response_format = ProviderStrategy(
1186
- schema=response_format,
1187
- )
1188
- else:
1189
- response_format = ToolStrategy(
1190
- schema=response_format,
1191
- )
1192
- elif isinstance(response_format, tuple) and len(response_format) == 2:
1193
- msg = "Passing a 2-tuple as response_format is no longer supported. "
1194
- raise ValueError(msg)
1195
-
1196
- # Create and configure the agent builder
1197
- builder = _AgentBuilder(
1198
- model=model,
1199
- tools=tools,
1200
- prompt=prompt,
1201
- response_format=cast("ResponseFormat[StructuredResponseT] | None", response_format),
1202
- pre_model_hook=pre_model_hook,
1203
- post_model_hook=post_model_hook,
1204
- state_schema=state_schema,
1205
- context_schema=context_schema,
1206
- version=version,
1207
- name=name,
1208
- store=store,
1209
- )
1210
-
1211
- # Build and compile the workflow
1212
- workflow = builder.build()
1213
- return workflow.compile( # type: ignore[return-value]
1214
- checkpointer=checkpointer,
1215
- store=store,
1216
- interrupt_before=interrupt_before,
1217
- interrupt_after=interrupt_after,
1218
- debug=debug,
1219
- name=name,
1220
- )
1221
-
1222
-
1223
- __all__ = [
1224
- "AgentState",
1225
- "AgentStatePydantic",
1226
- "AgentStateWithStructuredResponse",
1227
- "AgentStateWithStructuredResponsePydantic",
1228
- "create_agent",
1229
- ]