langgraph-prebuilt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,839 @@
1
+ import functools
2
+ import inspect
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ Literal,
7
+ Optional,
8
+ Sequence,
9
+ Type,
10
+ TypeVar,
11
+ Union,
12
+ cast,
13
+ )
14
+
15
+ from langchain_core.language_models import (
16
+ BaseChatModel,
17
+ LanguageModelInput,
18
+ LanguageModelLike,
19
+ )
20
+ from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
21
+ from langchain_core.runnables import (
22
+ Runnable,
23
+ RunnableBinding,
24
+ RunnableConfig,
25
+ )
26
+ from langchain_core.tools import BaseTool
27
+ from pydantic import BaseModel
28
+ from typing_extensions import Annotated, TypedDict
29
+
30
+ from langgraph.errors import ErrorCode, create_error_message
31
+ from langgraph.graph import END, StateGraph
32
+ from langgraph.graph.graph import CompiledGraph
33
+ from langgraph.graph.message import add_messages
34
+ from langgraph.managed import IsLastStep, RemainingSteps
35
+ from langgraph.prebuilt.tool_node import ToolNode
36
+ from langgraph.store.base import BaseStore
37
+ from langgraph.types import Checkpointer, Send
38
+ from langgraph.utils.runnable import RunnableCallable
39
+
40
+ StructuredResponse = Union[dict, BaseModel]
41
+ StructuredResponseSchema = Union[dict, type[BaseModel]]
42
+ F = TypeVar("F", bound=Callable[..., Any])
43
+
44
+
45
+ # We create the AgentState that we will pass around
46
+ # This simply involves a list of messages
47
+ # We want steps to return messages to append to the list
48
+ # So we annotate the messages attribute with `add_messages` reducer
49
+ class AgentState(TypedDict):
50
+ """The state of the agent."""
51
+
52
+ messages: Annotated[Sequence[BaseMessage], add_messages]
53
+
54
+ is_last_step: IsLastStep
55
+
56
+ remaining_steps: RemainingSteps
57
+
58
+
59
+ class AgentStateWithStructuredResponse(AgentState):
60
+ """The state of the agent with a structured response."""
61
+
62
+ structured_response: StructuredResponse
63
+
64
+
65
+ StateSchema = TypeVar("StateSchema", bound=AgentState)
66
+ StateSchemaType = Type[StateSchema]
67
+
68
+ PROMPT_RUNNABLE_NAME = "Prompt"
69
+
70
+ Prompt = Union[
71
+ SystemMessage,
72
+ str,
73
+ Callable[[StateSchema], LanguageModelInput],
74
+ Runnable[StateSchema, LanguageModelInput],
75
+ ]
76
+
77
+
78
+ def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
79
+ prompt_runnable: Runnable
80
+ if prompt is None:
81
+ prompt_runnable = RunnableCallable(
82
+ lambda state: state["messages"], name=PROMPT_RUNNABLE_NAME
83
+ )
84
+ elif isinstance(prompt, str):
85
+ _system_message: BaseMessage = SystemMessage(content=prompt)
86
+ prompt_runnable = RunnableCallable(
87
+ lambda state: [_system_message] + state["messages"],
88
+ name=PROMPT_RUNNABLE_NAME,
89
+ )
90
+ elif isinstance(prompt, SystemMessage):
91
+ prompt_runnable = RunnableCallable(
92
+ lambda state: [prompt] + state["messages"],
93
+ name=PROMPT_RUNNABLE_NAME,
94
+ )
95
+ elif inspect.iscoroutinefunction(prompt):
96
+ prompt_runnable = RunnableCallable(
97
+ None,
98
+ prompt,
99
+ name=PROMPT_RUNNABLE_NAME,
100
+ )
101
+ elif callable(prompt):
102
+ prompt_runnable = RunnableCallable(
103
+ prompt,
104
+ name=PROMPT_RUNNABLE_NAME,
105
+ )
106
+ elif isinstance(prompt, Runnable):
107
+ prompt_runnable = prompt
108
+ else:
109
+ raise ValueError(f"Got unexpected type for `prompt`: {type(prompt)}")
110
+
111
+ return prompt_runnable
112
+
113
+
114
+ def _convert_modifier_to_prompt(func: F) -> F:
115
+ """Decorator that converts state_modifier kwarg to prompt kwarg."""
116
+
117
+ @functools.wraps(func)
118
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
119
+ prompt = kwargs.get("prompt")
120
+ state_modifier = kwargs.pop("state_modifier", None)
121
+ if sum(p is not None for p in (prompt, state_modifier)) > 1:
122
+ raise ValueError(
123
+ "Expected only one of (prompt, state_modifier), got multiple values"
124
+ )
125
+
126
+ if state_modifier is not None:
127
+ prompt = state_modifier
128
+
129
+ kwargs["prompt"] = prompt
130
+ return func(*args, **kwargs)
131
+
132
+ return cast(F, wrapper)
133
+
134
+
135
+ def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> bool:
136
+ if not isinstance(model, RunnableBinding):
137
+ return True
138
+
139
+ if "tools" not in model.kwargs:
140
+ return True
141
+
142
+ bound_tools = model.kwargs["tools"]
143
+ if len(tools) != len(bound_tools):
144
+ raise ValueError(
145
+ "Number of tools in the model.bind_tools() and tools passed to create_react_agent must match"
146
+ )
147
+
148
+ tool_names = set(tool.name for tool in tools)
149
+ bound_tool_names = set()
150
+ for bound_tool in bound_tools:
151
+ # OpenAI-style tool
152
+ if bound_tool.get("type") == "function":
153
+ bound_tool_name = bound_tool["function"]["name"]
154
+ # Anthropic-style tool
155
+ elif bound_tool.get("name"):
156
+ bound_tool_name = bound_tool["name"]
157
+ else:
158
+ # unknown tool type so we'll ignore it
159
+ continue
160
+
161
+ bound_tool_names.add(bound_tool_name)
162
+
163
+ if missing_tools := tool_names - bound_tool_names:
164
+ raise ValueError(f"Missing tools '{missing_tools}' in the model.bind_tools()")
165
+
166
+ return False
167
+
168
+
169
+ def _get_model(model: LanguageModelLike) -> BaseChatModel:
170
+ """Get the underlying model from a RunnableBinding or return the model itself."""
171
+ if isinstance(model, RunnableBinding):
172
+ model = model.bound
173
+
174
+ if not isinstance(model, BaseChatModel):
175
+ raise TypeError(
176
+ f"Expected `model` to be a ChatModel or RunnableBinding (e.g. model.bind_tools(...)), got {type(model)}"
177
+ )
178
+
179
+ return model
180
+
181
+
182
+ def _validate_chat_history(
183
+ messages: Sequence[BaseMessage],
184
+ ) -> None:
185
+ """Validate that all tool calls in AIMessages have a corresponding ToolMessage."""
186
+ all_tool_calls = [
187
+ tool_call
188
+ for message in messages
189
+ if isinstance(message, AIMessage)
190
+ for tool_call in message.tool_calls
191
+ ]
192
+ tool_call_ids_with_results = {
193
+ message.tool_call_id for message in messages if isinstance(message, ToolMessage)
194
+ }
195
+ tool_calls_without_results = [
196
+ tool_call
197
+ for tool_call in all_tool_calls
198
+ if tool_call["id"] not in tool_call_ids_with_results
199
+ ]
200
+ if not tool_calls_without_results:
201
+ return
202
+
203
+ error_message = create_error_message(
204
+ message="Found AIMessages with tool_calls that do not have a corresponding ToolMessage. "
205
+ f"Here are the first few of those tool calls: {tool_calls_without_results[:3]}.\n\n"
206
+ "Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage "
207
+ "(result of a tool invocation to return to the LLM) - this is required by most LLM providers.",
208
+ error_code=ErrorCode.INVALID_CHAT_HISTORY,
209
+ )
210
+ raise ValueError(error_message)
211
+
212
+
213
+ @_convert_modifier_to_prompt
214
+ def create_react_agent(
215
+ model: Union[str, LanguageModelLike],
216
+ tools: Union[Sequence[BaseTool], ToolNode],
217
+ *,
218
+ prompt: Optional[Prompt] = None,
219
+ response_format: Optional[
220
+ Union[StructuredResponseSchema, tuple[str, StructuredResponseSchema]]
221
+ ] = None,
222
+ state_schema: Optional[StateSchemaType] = None,
223
+ config_schema: Optional[Type[Any]] = None,
224
+ checkpointer: Optional[Checkpointer] = None,
225
+ store: Optional[BaseStore] = None,
226
+ interrupt_before: Optional[list[str]] = None,
227
+ interrupt_after: Optional[list[str]] = None,
228
+ debug: bool = False,
229
+ version: Literal["v1", "v2"] = "v1",
230
+ name: Optional[str] = None,
231
+ ) -> CompiledGraph:
232
+ """Creates a graph that works with a chat model that utilizes tool calling.
233
+
234
+ Args:
235
+ model: The `LangChain` chat model that supports tool calling.
236
+ tools: A list of tools or a ToolNode instance.
237
+ If an empty list is provided, the agent will consist of a single LLM node without tool calling.
238
+ prompt: An optional prompt for the LLM. Can take a few different forms:
239
+
240
+ - str: This is converted to a SystemMessage and added to the beginning of the list of messages in state["messages"].
241
+ - SystemMessage: this is added to the beginning of the list of messages in state["messages"].
242
+ - Callable: This function should take in full graph state and the output is then passed to the language model.
243
+ - Runnable: This runnable should take in full graph state and the output is then passed to the language model.
244
+
245
+ response_format: An optional schema for the final agent output.
246
+
247
+ If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.
248
+ If not provided, `structured_response` will not be present in the output state.
249
+ Can be passed in as:
250
+
251
+ - an OpenAI function/tool schema,
252
+ - a JSON Schema,
253
+ - a TypedDict class,
254
+ - or a Pydantic class.
255
+ - a tuple (prompt, schema), where schema is one of the above.
256
+ The prompt will be used together with the model that is being used to generate the structured response.
257
+
258
+ !!! Important
259
+ `response_format` requires the model to support `.with_structured_output`
260
+
261
+ !!! Note
262
+ The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.
263
+ This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).
264
+ state_schema: An optional state schema that defines graph state.
265
+ Must have `messages` and `is_last_step` keys.
266
+ Defaults to `AgentState` that defines those two keys.
267
+ config_schema: An optional schema for configuration.
268
+ Use this to expose configurable parameters via agent.config_specs.
269
+ checkpointer: An optional checkpoint saver object. This is used for persisting
270
+ the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).
271
+ store: An optional store object. This is used for persisting data
272
+ across multiple threads (e.g., multiple conversations / users).
273
+ interrupt_before: An optional list of node names to interrupt before.
274
+ Should be one of the following: "agent", "tools".
275
+ This is useful if you want to add a user confirmation or other interrupt before taking an action.
276
+ interrupt_after: An optional list of node names to interrupt after.
277
+ Should be one of the following: "agent", "tools".
278
+ This is useful if you want to return directly or run additional processing on an output.
279
+ debug: A flag indicating whether to enable debug mode.
280
+ version: Determines the version of the graph to create.
281
+ Can be one of:
282
+
283
+ - `"v1"`: The tool node processes a single message. All tool
284
+ calls in the message are executed in parallel within the tool node.
285
+ - `"v2"`: The tool node processes a tool call.
286
+ Tool calls are distributed across multiple instances of the tool
287
+ node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)
288
+ API.
289
+ name: An optional name for the CompiledStateGraph.
290
+ This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node -
291
+ particularly useful for building multi-agent systems.
292
+
293
+ Returns:
294
+ A compiled LangChain runnable that can be used for chat interactions.
295
+
296
+ The resulting graph looks like this:
297
+
298
+ ``` mermaid
299
+ stateDiagram-v2
300
+ [*] --> Start
301
+ Start --> Agent
302
+ Agent --> Tools : continue
303
+ Tools --> Agent
304
+ Agent --> End : end
305
+ End --> [*]
306
+
307
+ classDef startClass fill:#ffdfba;
308
+ classDef endClass fill:#baffc9;
309
+ classDef otherClass fill:#fad7de;
310
+
311
+ class Start startClass
312
+ class End endClass
313
+ class Agent,Tools otherClass
314
+ ```
315
+
316
+ The "agent" node calls the language model with the messages list (after applying the messages modifier).
317
+ If the resulting AIMessage contains `tool_calls`, the graph will then call the ["tools"][langgraph.prebuilt.tool_node.ToolNode].
318
+ The "tools" node executes the tools (1 tool per `tool_call`) and adds the responses to the messages list
319
+ as `ToolMessage` objects. The agent node then calls the language model again.
320
+ The process repeats until no more `tool_calls` are present in the response.
321
+ The agent then returns the full list of messages as a dictionary containing the key "messages".
322
+
323
+ ``` mermaid
324
+ sequenceDiagram
325
+ participant U as User
326
+ participant A as Agent (LLM)
327
+ participant T as Tools
328
+ U->>A: Initial input
329
+ Note over A: Messages modifier + LLM
330
+ loop while tool_calls present
331
+ A->>T: Execute tools
332
+ T-->>A: ToolMessage for each tool_calls
333
+ end
334
+ A->>U: Return final state
335
+ ```
336
+
337
+ Examples:
338
+ Use with a simple tool:
339
+
340
+ ```pycon
341
+ >>> from datetime import datetime
342
+ >>> from langchain_openai import ChatOpenAI
343
+ >>> from langgraph.prebuilt import create_react_agent
344
+
345
+
346
+ ... def check_weather(location: str, at_time: datetime | None = None) -> str:
347
+ ... '''Return the weather forecast for the specified location.'''
348
+ ... return f"It's always sunny in {location}"
349
+ >>>
350
+ >>> tools = [check_weather]
351
+ >>> model = ChatOpenAI(model="gpt-4o")
352
+ >>> graph = create_react_agent(model, tools=tools)
353
+ >>> inputs = {"messages": [("user", "what is the weather in sf")]}
354
+ >>> for s in graph.stream(inputs, stream_mode="values"):
355
+ ... message = s["messages"][-1]
356
+ ... if isinstance(message, tuple):
357
+ ... print(message)
358
+ ... else:
359
+ ... message.pretty_print()
360
+ ('user', 'what is the weather in sf')
361
+ ================================== Ai Message ==================================
362
+ Tool Calls:
363
+ check_weather (call_LUzFvKJRuaWQPeXvBOzwhQOu)
364
+ Call ID: call_LUzFvKJRuaWQPeXvBOzwhQOu
365
+ Args:
366
+ location: San Francisco
367
+ ================================= Tool Message =================================
368
+ Name: check_weather
369
+ It's always sunny in San Francisco
370
+ ================================== Ai Message ==================================
371
+ The weather in San Francisco is sunny.
372
+ ```
373
+ Add a system prompt for the LLM:
374
+
375
+ ```pycon
376
+ >>> system_prompt = "You are a helpful bot named Fred."
377
+ >>> graph = create_react_agent(model, tools, prompt=system_prompt)
378
+ >>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]}
379
+ >>> for s in graph.stream(inputs, stream_mode="values"):
380
+ ... message = s["messages"][-1]
381
+ ... if isinstance(message, tuple):
382
+ ... print(message)
383
+ ... else:
384
+ ... message.pretty_print()
385
+ ('user', "What's your name? And what's the weather in SF?")
386
+ ================================== Ai Message ==================================
387
+ Hi, my name is Fred. Let me check the weather in San Francisco for you.
388
+ Tool Calls:
389
+ check_weather (call_lqhj4O0hXYkW9eknB4S41EXk)
390
+ Call ID: call_lqhj4O0hXYkW9eknB4S41EXk
391
+ Args:
392
+ location: San Francisco
393
+ ================================= Tool Message =================================
394
+ Name: check_weather
395
+ It's always sunny in San Francisco
396
+ ================================== Ai Message ==================================
397
+ The weather in San Francisco is currently sunny. If you need any more details or have other questions, feel free to ask!
398
+ ```
399
+
400
+ Add a more complex prompt for the LLM:
401
+
402
+ ```pycon
403
+ >>> from langchain_core.prompts import ChatPromptTemplate
404
+ >>> prompt = ChatPromptTemplate.from_messages([
405
+ ... ("system", "You are a helpful bot named Fred."),
406
+ ... ("placeholder", "{messages}"),
407
+ ... ("user", "Remember, always be polite!"),
408
+ ... ])
409
+ >>>
410
+ >>> graph = create_react_agent(model, tools, prompt=prompt)
411
+ >>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]}
412
+ >>> for s in graph.stream(inputs, stream_mode="values"):
413
+ ... message = s["messages"][-1]
414
+ ... if isinstance(message, tuple):
415
+ ... print(message)
416
+ ... else:
417
+ ... message.pretty_print()
418
+ ```
419
+
420
+ Add complex prompt with custom graph state:
421
+
422
+ ```pycon
423
+ >>> from typing_extensions import TypedDict
424
+ >>>
425
+ >>> from langgraph.managed import IsLastStep
426
+ >>> prompt = ChatPromptTemplate.from_messages(
427
+ ... [
428
+ ... ("system", "Today is {today}"),
429
+ ... ("placeholder", "{messages}"),
430
+ ... ]
431
+ ... )
432
+ >>>
433
+ >>> class CustomState(TypedDict):
434
+ ... today: str
435
+ ... messages: Annotated[list[BaseMessage], add_messages]
436
+ ... is_last_step: IsLastStep
437
+ >>>
438
+ >>> graph = create_react_agent(model, tools, state_schema=CustomState, prompt=prompt)
439
+ >>> inputs = {"messages": [("user", "What's today's date? And what's the weather in SF?")], "today": "July 16, 2004"}
440
+ >>> for s in graph.stream(inputs, stream_mode="values"):
441
+ ... message = s["messages"][-1]
442
+ ... if isinstance(message, tuple):
443
+ ... print(message)
444
+ ... else:
445
+ ... message.pretty_print()
446
+ ```
447
+
448
+ Add thread-level "chat memory" to the graph:
449
+
450
+ ```pycon
451
+ >>> from langgraph.checkpoint.memory import MemorySaver
452
+ >>> graph = create_react_agent(model, tools, checkpointer=MemorySaver())
453
+ >>> config = {"configurable": {"thread_id": "thread-1"}}
454
+ >>> def print_stream(graph, inputs, config):
455
+ ... for s in graph.stream(inputs, config, stream_mode="values"):
456
+ ... message = s["messages"][-1]
457
+ ... if isinstance(message, tuple):
458
+ ... print(message)
459
+ ... else:
460
+ ... message.pretty_print()
461
+ >>> inputs = {"messages": [("user", "What's the weather in SF?")]}
462
+ >>> print_stream(graph, inputs, config)
463
+ >>> inputs2 = {"messages": [("user", "Cool, so then should i go biking today?")]}
464
+ >>> print_stream(graph, inputs2, config)
465
+ ('user', "What's the weather in SF?")
466
+ ================================== Ai Message ==================================
467
+ Tool Calls:
468
+ check_weather (call_ChndaktJxpr6EMPEB5JfOFYc)
469
+ Call ID: call_ChndaktJxpr6EMPEB5JfOFYc
470
+ Args:
471
+ location: San Francisco
472
+ ================================= Tool Message =================================
473
+ Name: check_weather
474
+ It's always sunny in San Francisco
475
+ ================================== Ai Message ==================================
476
+ The weather in San Francisco is sunny. Enjoy your day!
477
+ ================================ Human Message =================================
478
+ Cool, so then should i go biking today?
479
+ ================================== Ai Message ==================================
480
+ Since the weather in San Francisco is sunny, it sounds like a great day for biking! Enjoy your ride!
481
+ ```
482
+
483
+ Add an interrupt to let the user confirm before taking an action:
484
+
485
+ ```pycon
486
+ >>> graph = create_react_agent(
487
+ ... model, tools, interrupt_before=["tools"], checkpointer=MemorySaver()
488
+ >>> )
489
+ >>> config = {"configurable": {"thread_id": "thread-1"}}
490
+
491
+ >>> inputs = {"messages": [("user", "What's the weather in SF?")]}
492
+ >>> print_stream(graph, inputs, config)
493
+ >>> snapshot = graph.get_state(config)
494
+ >>> print("Next step: ", snapshot.next)
495
+ >>> print_stream(graph, None, config)
496
+ ```
497
+
498
+ Add cross-thread memory to the graph:
499
+
500
+ ```pycon
501
+ >>> from langgraph.prebuilt import InjectedStore
502
+ >>> from langgraph.store.base import BaseStore
503
+
504
+ >>> def save_memory(memory: str, *, config: RunnableConfig, store: Annotated[BaseStore, InjectedStore()]) -> str:
505
+ ... '''Save the given memory for the current user.'''
506
+ ... # This is a **tool** the model can use to save memories to storage
507
+ ... user_id = config.get("configurable", {}).get("user_id")
508
+ ... namespace = ("memories", user_id)
509
+ ... store.put(namespace, f"memory_{len(store.search(namespace))}", {"data": memory})
510
+ ... return f"Saved memory: {memory}"
511
+
512
+ >>> def prepare_model_inputs(state: AgentState, config: RunnableConfig, store: BaseStore):
513
+ ... # Retrieve user memories and add them to the system message
514
+ ... # This function is called **every time** the model is prompted. It converts the state to a prompt
515
+ ... user_id = config.get("configurable", {}).get("user_id")
516
+ ... namespace = ("memories", user_id)
517
+ ... memories = [m.value["data"] for m in store.search(namespace)]
518
+ ... system_msg = f"User memories: {', '.join(memories)}"
519
+ ... return [{"role": "system", "content": system_msg)] + state["messages"]
520
+
521
+ >>> from langgraph.checkpoint.memory import MemorySaver
522
+ >>> from langgraph.store.memory import InMemoryStore
523
+ >>> store = InMemoryStore()
524
+ >>> graph = create_react_agent(model, [save_memory], prompt=prepare_model_inputs, store=store, checkpointer=MemorySaver())
525
+ >>> config = {"configurable": {"thread_id": "thread-1", "user_id": "1"}}
526
+
527
+ >>> inputs = {"messages": [("user", "Hey I'm Will, how's it going?")]}
528
+ >>> print_stream(graph, inputs, config)
529
+ ('user', "Hey I'm Will, how's it going?")
530
+ ================================== Ai Message ==================================
531
+ Hello Will! It's nice to meet you. I'm doing well, thank you for asking. How are you doing today?
532
+
533
+ >>> inputs2 = {"messages": [("user", "I like to bike")]}
534
+ >>> print_stream(graph, inputs2, config)
535
+ ================================ Human Message =================================
536
+ I like to bike
537
+ ================================== Ai Message ==================================
538
+ That's great to hear, Will! Biking is an excellent hobby and form of exercise. It's a fun way to stay active and explore your surroundings. Do you have any favorite biking routes or trails you enjoy? Or perhaps you're into a specific type of biking, like mountain biking or road cycling?
539
+
540
+ >>> config = {"configurable": {"thread_id": "thread-2", "user_id": "1"}}
541
+ >>> inputs3 = {"messages": [("user", "Hi there! Remember me?")]}
542
+ >>> print_stream(graph, inputs3, config)
543
+ ================================ Human Message =================================
544
+ Hi there! Remember me?
545
+ ================================== Ai Message ==================================
546
+ User memories:
547
+ Hello! Of course, I remember you, Will! You mentioned earlier that you like to bike. It's great to hear from you again. How have you been? Have you been on any interesting bike rides lately?
548
+ ```
549
+
550
+ Add a timeout for a given step:
551
+
552
+ ```pycon
553
+ >>> import time
554
+ ... def check_weather(location: str, at_time: datetime | None = None) -> float:
555
+ ... '''Return the weather forecast for the specified location.'''
556
+ ... time.sleep(2)
557
+ ... return f"It's always sunny in {location}"
558
+ >>>
559
+ >>> tools = [check_weather]
560
+ >>> graph = create_react_agent(model, tools)
561
+ >>> graph.step_timeout = 1 # Seconds
562
+ >>> for s in graph.stream({"messages": [("user", "what is the weather in sf")]}):
563
+ ... print(s)
564
+ TimeoutError: Timed out at step 2
565
+ ```
566
+ """
567
+ if version not in ("v1", "v2"):
568
+ raise ValueError(
569
+ f"Invalid version {version}. Supported versions are 'v1' and 'v2'."
570
+ )
571
+
572
+ if state_schema is not None:
573
+ required_keys = {"messages", "remaining_steps"}
574
+ if response_format is not None:
575
+ required_keys.add("structured_response")
576
+
577
+ if missing_keys := required_keys - set(state_schema.__annotations__):
578
+ raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
579
+
580
+ if state_schema is None:
581
+ state_schema = (
582
+ AgentStateWithStructuredResponse
583
+ if response_format is not None
584
+ else AgentState
585
+ )
586
+
587
+ if isinstance(tools, ToolNode):
588
+ tool_classes = list(tools.tools_by_name.values())
589
+ tool_node = tools
590
+ else:
591
+ tool_node = ToolNode(tools)
592
+ # get the tool functions wrapped in a tool class from the ToolNode
593
+ tool_classes = list(tool_node.tools_by_name.values())
594
+
595
+ if isinstance(model, str):
596
+ try:
597
+ from langchain.chat_models import ( # type: ignore[import-not-found]
598
+ init_chat_model,
599
+ )
600
+ except ImportError:
601
+ raise ImportError(
602
+ "Please install langchain (`pip install langchain`) to use '<provider>:<model>' string syntax for `model` parameter."
603
+ )
604
+
605
+ model = cast(BaseChatModel, init_chat_model(model))
606
+
607
+ tool_calling_enabled = len(tool_classes) > 0
608
+
609
+ if _should_bind_tools(model, tool_classes) and tool_calling_enabled:
610
+ model = cast(BaseChatModel, model).bind_tools(tool_classes)
611
+
612
+ model_runnable = _get_prompt_runnable(prompt) | model
613
+
614
+ # If any of the tools are configured to return_directly after running,
615
+ # our graph needs to check if these were called
616
+ should_return_direct = {t.name for t in tool_classes if t.return_direct}
617
+
618
+ # Define the function that calls the model
619
+ def call_model(state: AgentState, config: RunnableConfig) -> AgentState:
620
+ _validate_chat_history(state["messages"])
621
+ response = cast(AIMessage, model_runnable.invoke(state, config))
622
+ # add agent name to the AIMessage
623
+ response.name = name
624
+ has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
625
+ all_tools_return_direct = (
626
+ all(call["name"] in should_return_direct for call in response.tool_calls)
627
+ if isinstance(response, AIMessage)
628
+ else False
629
+ )
630
+ if (
631
+ (
632
+ "remaining_steps" not in state
633
+ and state.get("is_last_step", False)
634
+ and has_tool_calls
635
+ )
636
+ or (
637
+ "remaining_steps" in state
638
+ and state["remaining_steps"] < 1
639
+ and all_tools_return_direct
640
+ )
641
+ or (
642
+ "remaining_steps" in state
643
+ and state["remaining_steps"] < 2
644
+ and has_tool_calls
645
+ )
646
+ ):
647
+ return {
648
+ "messages": [
649
+ AIMessage(
650
+ id=response.id,
651
+ content="Sorry, need more steps to process this request.",
652
+ )
653
+ ]
654
+ }
655
+ # We return a list, because this will get added to the existing list
656
+ return {"messages": [response]}
657
+
658
+ async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
659
+ _validate_chat_history(state["messages"])
660
+ response = cast(AIMessage, await model_runnable.ainvoke(state, config))
661
+ # add agent name to the AIMessage
662
+ response.name = name
663
+ has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
664
+ all_tools_return_direct = (
665
+ all(call["name"] in should_return_direct for call in response.tool_calls)
666
+ if isinstance(response, AIMessage)
667
+ else False
668
+ )
669
+ if (
670
+ (
671
+ "remaining_steps" not in state
672
+ and state.get("is_last_step", False)
673
+ and has_tool_calls
674
+ )
675
+ or (
676
+ "remaining_steps" in state
677
+ and state["remaining_steps"] < 1
678
+ and all_tools_return_direct
679
+ )
680
+ or (
681
+ "remaining_steps" in state
682
+ and state["remaining_steps"] < 2
683
+ and has_tool_calls
684
+ )
685
+ ):
686
+ return {
687
+ "messages": [
688
+ AIMessage(
689
+ id=response.id,
690
+ content="Sorry, need more steps to process this request.",
691
+ )
692
+ ]
693
+ }
694
+ # We return a list, because this will get added to the existing list
695
+ return {"messages": [response]}
696
+
697
+ def generate_structured_response(
698
+ state: AgentState, config: RunnableConfig
699
+ ) -> AgentState:
700
+ # NOTE: we exclude the last message because there is enough information
701
+ # for the LLM to generate the structured response
702
+ messages = state["messages"][:-1]
703
+ structured_response_schema = response_format
704
+ if isinstance(response_format, tuple):
705
+ system_prompt, structured_response_schema = response_format
706
+ messages = [SystemMessage(content=system_prompt)] + list(messages)
707
+
708
+ model_with_structured_output = _get_model(model).with_structured_output(
709
+ cast(StructuredResponseSchema, structured_response_schema)
710
+ )
711
+ response = model_with_structured_output.invoke(messages, config)
712
+ return {"structured_response": response}
713
+
714
+ async def agenerate_structured_response(
715
+ state: AgentState, config: RunnableConfig
716
+ ) -> AgentState:
717
+ # NOTE: we exclude the last message because there is enough information
718
+ # for the LLM to generate the structured response
719
+ messages = state["messages"][:-1]
720
+ structured_response_schema = response_format
721
+ if isinstance(response_format, tuple):
722
+ system_prompt, structured_response_schema = response_format
723
+ messages = [SystemMessage(content=system_prompt)] + list(messages)
724
+
725
+ model_with_structured_output = _get_model(model).with_structured_output(
726
+ cast(StructuredResponseSchema, structured_response_schema)
727
+ )
728
+ response = await model_with_structured_output.ainvoke(messages, config)
729
+ return {"structured_response": response}
730
+
731
+ if not tool_calling_enabled:
732
+ # Define a new graph
733
+ workflow = StateGraph(state_schema, config_schema=config_schema)
734
+ workflow.add_node("agent", RunnableCallable(call_model, acall_model))
735
+ workflow.set_entry_point("agent")
736
+ if response_format is not None:
737
+ workflow.add_node(
738
+ "generate_structured_response",
739
+ RunnableCallable(
740
+ generate_structured_response, agenerate_structured_response
741
+ ),
742
+ )
743
+ workflow.add_edge("agent", "generate_structured_response")
744
+
745
+ return workflow.compile(
746
+ checkpointer=checkpointer,
747
+ store=store,
748
+ interrupt_before=interrupt_before,
749
+ interrupt_after=interrupt_after,
750
+ debug=debug,
751
+ name=name,
752
+ )
753
+
754
+ # Define the function that determines whether to continue or not
755
+ def should_continue(state: AgentState) -> Union[str, list]:
756
+ messages = state["messages"]
757
+ last_message = messages[-1]
758
+ # If there is no function call, then we finish
759
+ if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
760
+ return END if response_format is None else "generate_structured_response"
761
+ # Otherwise if there is, we continue
762
+ else:
763
+ if version == "v1":
764
+ return "tools"
765
+ elif version == "v2":
766
+ tool_calls = [
767
+ tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
768
+ for call in last_message.tool_calls
769
+ ]
770
+ return [Send("tools", [tool_call]) for tool_call in tool_calls]
771
+
772
+ # Define a new graph
773
+ workflow = StateGraph(state_schema or AgentState, config_schema=config_schema)
774
+
775
+ # Define the two nodes we will cycle between
776
+ workflow.add_node("agent", RunnableCallable(call_model, acall_model))
777
+ workflow.add_node("tools", tool_node)
778
+
779
+ # Set the entrypoint as `agent`
780
+ # This means that this node is the first one called
781
+ workflow.set_entry_point("agent")
782
+
783
+ # Add a structured output node if response_format is provided
784
+ if response_format is not None:
785
+ workflow.add_node(
786
+ "generate_structured_response",
787
+ RunnableCallable(
788
+ generate_structured_response, agenerate_structured_response
789
+ ),
790
+ )
791
+ workflow.add_edge("generate_structured_response", END)
792
+ should_continue_destinations = ["tools", "generate_structured_response"]
793
+ else:
794
+ should_continue_destinations = ["tools", END]
795
+
796
+ # We now add a conditional edge
797
+ workflow.add_conditional_edges(
798
+ # First, we define the start node. We use `agent`.
799
+ # This means these are the edges taken after the `agent` node is called.
800
+ "agent",
801
+ # Next, we pass in the function that will determine which node is called next.
802
+ should_continue,
803
+ path_map=should_continue_destinations,
804
+ )
805
+
806
+ def route_tool_responses(state: AgentState) -> Literal["agent", "__end__"]:
807
+ for m in reversed(state["messages"]):
808
+ if not isinstance(m, ToolMessage):
809
+ break
810
+ if m.name in should_return_direct:
811
+ return END
812
+ return "agent"
813
+
814
+ if should_return_direct:
815
+ workflow.add_conditional_edges("tools", route_tool_responses)
816
+ else:
817
+ workflow.add_edge("tools", "agent")
818
+
819
+ # Finally, we compile it!
820
+ # This compiles it into a LangChain Runnable,
821
+ # meaning you can use it as you would any other runnable
822
+ return workflow.compile(
823
+ checkpointer=checkpointer,
824
+ store=store,
825
+ interrupt_before=interrupt_before,
826
+ interrupt_after=interrupt_after,
827
+ debug=debug,
828
+ name=name,
829
+ )
830
+
831
+
832
+ # Keep for backwards compatibility
833
+ create_tool_calling_executor = create_react_agent
834
+
835
+ __all__ = [
836
+ "create_react_agent",
837
+ "create_tool_calling_executor",
838
+ "AgentState",
839
+ ]