Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
chat_agent_executor.py1016 linesDownload Raw Back to prebuilt
1import inspect2import warnings3from collections.abc import Awaitable, Callable, Sequence4from typing import (5    Annotated,6    Any,7    Literal,8    TypeVar,9    cast,10    get_type_hints,11)12 13from langchain_core.language_models import (14    BaseChatModel,15    LanguageModelInput,16    LanguageModelLike,17)18from langchain_core.messages import (19    AIMessage,20    AnyMessage,21    BaseMessage,22    SystemMessage,23    ToolMessage,24)25from langchain_core.runnables import (26    Runnable,27    RunnableBinding,28    RunnableConfig,29    RunnableSequence,30)31from langchain_core.tools import BaseTool32from langgraph._internal._runnable import RunnableCallable, RunnableLike33from langgraph._internal._typing import MISSING34from langgraph.errors import ErrorCode, create_error_message35from langgraph.graph import END, StateGraph36from langgraph.graph.message import add_messages37from langgraph.graph.state import CompiledStateGraph38from langgraph.managed import RemainingSteps39from langgraph.runtime import Runtime40from langgraph.store.base import BaseStore41from langgraph.types import Checkpointer, Send42from langgraph.typing import ContextT43from langgraph.warnings import LangGraphDeprecatedSinceV1044from pydantic import BaseModel45from typing_extensions import NotRequired, TypedDict, deprecated46 47from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode48 49StructuredResponse = dict | BaseModel50StructuredResponseSchema = dict | type[BaseModel]51 52 53@deprecated(54    "AgentState has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentState`.",55    category=LangGraphDeprecatedSinceV10,56)57class AgentState(TypedDict):58    """The state of the agent."""59 60    messages: Annotated[Sequence[BaseMessage], add_messages]61 62    remaining_steps: NotRequired[RemainingSteps]63 64 65@deprecated(66    "AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",67    category=LangGraphDeprecatedSinceV10,68)69class AgentStatePydantic(BaseModel):70    """The state of the agent."""71 72    messages: Annotated[Sequence[BaseMessage], add_messages]73 74    remaining_steps: RemainingSteps = 2575 76 77with warnings.catch_warnings():78    warnings.filterwarnings(79        "ignore",80        category=LangGraphDeprecatedSinceV10,81        message="AgentState has been moved to `langchain.agents`.*",82    )83 84    @deprecated(85        "AgentStateWithStructuredResponse has been deprecated in favor of AgentState in `langchain.agents`.",86        category=LangGraphDeprecatedSinceV10,87    )88    class AgentStateWithStructuredResponse(AgentState):89        """The state of the agent with a structured response."""90 91        structured_response: StructuredResponse92 93 94with warnings.catch_warnings():95    warnings.filterwarnings(96        "ignore",97        category=LangGraphDeprecatedSinceV10,98        message="AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",99    )100 101    @deprecated(102        "AgentStateWithStructuredResponsePydantic has been deprecated in favor of AgentState in `langchain.agents`.",103        category=LangGraphDeprecatedSinceV10,104    )105    class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):106        """The state of the agent with a structured response."""107 108        structured_response: StructuredResponse109 110 111with warnings.catch_warnings():112    warnings.filterwarnings(113        "ignore",114        category=LangGraphDeprecatedSinceV10,115    )116    StateSchema = TypeVar("StateSchema", bound=AgentState | AgentStatePydantic)117    StateSchemaType = type[StateSchema]118 119PROMPT_RUNNABLE_NAME = "Prompt"120 121Prompt = (122    SystemMessage123    | str124    | Callable[[StateSchema], LanguageModelInput]125    | Runnable[StateSchema, LanguageModelInput]126)127 128 129def _get_state_value(state: StateSchema, key: str, default: Any = None) -> Any:130    return (131        state.get(key, default)132        if isinstance(state, dict)133        else getattr(state, key, default)134    )135 136 137def _get_prompt_runnable(prompt: Prompt | None) -> Runnable:138    prompt_runnable: Runnable139    if prompt is None:140        prompt_runnable = RunnableCallable(141            lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME142        )143    elif isinstance(prompt, str):144        _system_message: BaseMessage = SystemMessage(content=prompt)145        prompt_runnable = RunnableCallable(146            lambda state: [_system_message] + _get_state_value(state, "messages"),147            name=PROMPT_RUNNABLE_NAME,148        )149    elif isinstance(prompt, SystemMessage):150        prompt_runnable = RunnableCallable(151            lambda state: [prompt] + _get_state_value(state, "messages"),152            name=PROMPT_RUNNABLE_NAME,153        )154    elif inspect.iscoroutinefunction(prompt):155        prompt_runnable = RunnableCallable(156            None,157            prompt,158            name=PROMPT_RUNNABLE_NAME,159        )160    elif callable(prompt):161        prompt_runnable = RunnableCallable(162            prompt,163            name=PROMPT_RUNNABLE_NAME,164        )165    elif isinstance(prompt, Runnable):166        prompt_runnable = prompt167    else:168        raise ValueError(f"Got unexpected type for `prompt`: {type(prompt)}")169 170    return prompt_runnable171 172 173def _should_bind_tools(174    model: LanguageModelLike, tools: Sequence[BaseTool], num_builtin: int = 0175) -> bool:176    if isinstance(model, RunnableSequence):177        model = next(178            (179                step180                for step in model.steps181                if isinstance(step, (RunnableBinding, BaseChatModel))182            ),183            model,184        )185 186    if not isinstance(model, RunnableBinding):187        return True188 189    if "tools" not in model.kwargs:190        return True191 192    bound_tools = model.kwargs["tools"]193    if len(tools) != len(bound_tools) - num_builtin:194        raise ValueError(195            "Number of tools in the model.bind_tools() and tools passed to create_react_agent must match"196            f" Got {len(tools)} tools, expected {len(bound_tools) - num_builtin}"197        )198 199    tool_names = set(tool.name for tool in tools)200    bound_tool_names = set()201    for bound_tool in bound_tools:202        # OpenAI-style tool203        if bound_tool.get("type") == "function":204            bound_tool_name = bound_tool["function"]["name"]205        # Anthropic-style tool206        elif bound_tool.get("name"):207            bound_tool_name = bound_tool["name"]208        else:209            # unknown tool type so we'll ignore it210            continue211 212        bound_tool_names.add(bound_tool_name)213 214    if missing_tools := tool_names - bound_tool_names:215        raise ValueError(f"Missing tools '{missing_tools}' in the model.bind_tools()")216 217    return False218 219 220def _get_model(model: LanguageModelLike) -> BaseChatModel:221    """Get the underlying model from a RunnableBinding or return the model itself."""222    if isinstance(model, RunnableSequence):223        model = next(224            (225                step226                for step in model.steps227                if isinstance(step, (RunnableBinding, BaseChatModel))228            ),229            model,230        )231 232    if isinstance(model, RunnableBinding):233        model = model.bound234 235    if not isinstance(model, BaseChatModel):236        raise TypeError(237            f"Expected `model` to be a ChatModel or RunnableBinding (e.g. model.bind_tools(...)), got {type(model)}"238        )239 240    return model241 242 243def _validate_chat_history(244    messages: Sequence[BaseMessage],245) -> None:246    """Validate that all tool calls in AIMessages have a corresponding ToolMessage."""247    all_tool_calls = [248        tool_call249        for message in messages250        if isinstance(message, AIMessage)251        for tool_call in message.tool_calls252    ]253    tool_call_ids_with_results = {254        message.tool_call_id for message in messages if isinstance(message, ToolMessage)255    }256    tool_calls_without_results = [257        tool_call258        for tool_call in all_tool_calls259        if tool_call["id"] not in tool_call_ids_with_results260    ]261    if not tool_calls_without_results:262        return263 264    error_message = create_error_message(265        message="Found AIMessages with tool_calls that do not have a corresponding ToolMessage. "266        f"Here are the first few of those tool calls: {tool_calls_without_results[:3]}.\n\n"267        "Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage "268        "(result of a tool invocation to return to the LLM) - this is required by most LLM providers.",269        error_code=ErrorCode.INVALID_CHAT_HISTORY,270    )271    raise ValueError(error_message)272 273 274@deprecated(275    "create_react_agent has been moved to `langchain.agents`. Please update your import to `from langchain.agents import create_agent`.",276    category=LangGraphDeprecatedSinceV10,277)278def create_react_agent(279    model: str280    | LanguageModelLike281    | Callable[[StateSchema, Runtime[ContextT]], BaseChatModel]282    | Callable[[StateSchema, Runtime[ContextT]], Awaitable[BaseChatModel]]283    | Callable[284        [StateSchema, Runtime[ContextT]], Runnable[LanguageModelInput, BaseMessage]285    ]286    | Callable[287        [StateSchema, Runtime[ContextT]],288        Awaitable[Runnable[LanguageModelInput, BaseMessage]],289    ],290    tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,291    *,292    prompt: Prompt | None = None,293    response_format: StructuredResponseSchema294    | tuple[str, StructuredResponseSchema]295    | None = None,296    pre_model_hook: RunnableLike | None = None,297    post_model_hook: RunnableLike | None = None,298    state_schema: StateSchemaType | None = None,299    context_schema: type[Any] | None = None,300    checkpointer: Checkpointer | None = None,301    store: BaseStore | None = None,302    interrupt_before: list[str] | None = None,303    interrupt_after: list[str] | None = None,304    debug: bool = False,305    version: Literal["v1", "v2"] = "v2",306    name: str | None = None,307    **deprecated_kwargs: Any,308) -> CompiledStateGraph:309    """Creates an agent graph that calls tools in a loop until a stopping condition is met.310 311    !!! warning312 313        This function is deprecated in favor of314        [`create_agent`][langchain.agents.create_agent] from the `langchain`315        package, which provides an equivalent agent factory with a flexible316        middleware system. For migration guidance, see317        [Migrating from LangGraph v0](https://docs.langchain.com/oss/python/migrate/langgraph-v1).318 319    Args:320        model: The language model for the agent. Supports static and dynamic321            model selection.322 323            - **Static model**: A chat model instance (e.g.,324                [`ChatOpenAI`][langchain_openai.ChatOpenAI]) or string identifier (e.g.,325                `"openai:gpt-4"`)326            - **Dynamic model**: A callable with signature327                `(state, runtime) -> BaseChatModel` that returns different models328                based on runtime context329 330                If the model has tools bound via `bind_tools` or other configurations,331                the return type should be a `Runnable[LanguageModelInput, BaseMessage]`332                Coroutines are also supported, allowing for asynchronous model selection.333 334            Dynamic functions receive graph state and runtime, enabling335            context-dependent model selection. Must return a `BaseChatModel`336            instance. For tool calling, bind tools using `.bind_tools()`.337            Bound tools must be a subset of the `tools` parameter.338 339            !!! example "Dynamic model"340 341                ```python342                from dataclasses import dataclass343 344                @dataclass345                class ModelContext:346                    model_name: str = "gpt-3.5-turbo"347 348                # Instantiate models globally349                gpt4_model = ChatOpenAI(model="gpt-4")350                gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")351 352                def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:353                    model_name = runtime.context.model_name354                    model = gpt4_model if model_name == "gpt-4" else gpt35_model355                    return model.bind_tools(tools)356                ```357 358            !!! note "Dynamic Model Requirements"359 360                Ensure returned models have appropriate tools bound via361                `.bind_tools()` and support required functionality. Bound tools362                must be a subset of those specified in the `tools` parameter.363 364        tools: A list of tools or a `ToolNode` instance.365            If an empty list is provided, the agent will consist of a single LLM node without tool calling.366        prompt: An optional prompt for the LLM. Can take a few different forms:367 368            - `str`: This is converted to a `SystemMessage` and added to the beginning of the list of messages in `state["messages"]`.369            - `SystemMessage`: this is added to the beginning of the list of messages in `state["messages"]`.370            - `Callable`: This function should take in full graph state and the output is then passed to the language model.371            - `Runnable`: This runnable should take in full graph state and the output is then passed to the language model.372 373        response_format: An optional schema for the final agent output.374 375            If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.376 377            If not provided, `structured_response` will not be present in the output state.378 379            Can be passed in as:380 381            - An OpenAI function/tool schema,382            - A JSON Schema,383            - A TypedDict class,384            - A Pydantic class.385            - A tuple `(prompt, schema)`, where schema is one of the above.386                The prompt will be used together with the model that is being used to387                generate the structured response.388 389            !!! Important390                `response_format` requires the model to support `.with_structured_output`391 392            !!! Note393                The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.394                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/).395 396        pre_model_hook: An optional node to add before the `agent` node (i.e., the node that calls the LLM).397            Useful for managing long message histories (e.g., message trimming, summarization, etc.).398            Pre-model hook must be a callable or a runnable that takes in current graph state and returns a state update in the form of399                ```python400                # At least one of `messages` or `llm_input_messages` MUST be provided401                {402                    # If provided, will UPDATE the `messages` in the state403                    "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), ...],404                    # If provided, will be used as the input to the LLM,405                    # and will NOT UPDATE `messages` in the state406                    "llm_input_messages": [...],407                    # Any other state keys that need to be propagated408                    ...409                }410                ```411 412            !!! Important413                At least one of `messages` or `llm_input_messages` MUST be provided and will be used as an input to the `agent` node.414                The rest of the keys will be added to the graph state.415 416            !!! Warning417                If you are returning `messages` in the pre-model hook, you should OVERWRITE the `messages` key by doing the following:418 419                ```python420                {421                    "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *new_messages]422                    ...423                }424                ```425        post_model_hook: An optional node to add after the `agent` node (i.e., the node that calls the LLM).426            Useful for implementing human-in-the-loop, guardrails, validation, or other post-processing.427            Post-model hook must be a callable or a runnable that takes in current graph state and returns a state update.428 429            !!! Note430                Only available with `version="v2"`.431        state_schema: An optional state schema that defines graph state.432            Must have `messages` and `remaining_steps` keys.433            Defaults to `AgentState` that defines those two keys.434            !!! Note435                `remaining_steps` is used to limit the number of steps the react agent can take.436                Calculated roughly as `recursion_limit` - `total_steps_taken`.437                If `remaining_steps` is less than 2 and tool calls are present in the response,438                the react agent will return a final AI Message with439                the content "Sorry, need more steps to process this request.".440                No `GraphRecusionError` will be raised in this case.441 442        context_schema: An optional schema for runtime context.443        checkpointer: An optional checkpoint saver object. This is used for persisting444            the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).445        store: An optional store object. This is used for persisting data446            across multiple threads (e.g., multiple conversations / users).447        interrupt_before: An optional list of node names to interrupt before.448            Should be one of the following: `"agent"`, `"tools"`.449 450            This is useful if you want to add a user confirmation or other interrupt before taking an action.451        interrupt_after: An optional list of node names to interrupt after.452            Should be one of the following: `"agent"`, `"tools"`.453 454            This is useful if you want to return directly or run additional processing on an output.455        debug: A flag indicating whether to enable debug mode.456        version: Determines the version of the graph to create.457 458            Can be one of:459 460            - `"v1"`: The tool node processes a single message. All tool461                calls in the message are executed in parallel within the tool node.462            - `"v2"`: The tool node processes a tool call.463                Tool calls are distributed across multiple instances of the tool464                node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)465                API.466        name: An optional name for the `CompiledStateGraph`.467            This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node -468            particularly useful for building multi-agent systems.469 470    !!! warning "`config_schema` Deprecated"471        The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.472        Please use `context_schema` instead to specify the schema for run-scoped context.473 474 475    Returns:476        A compiled LangChain `Runnable` that can be used for chat interactions.477 478    The "agent" node calls the language model with the messages list (after applying the prompt).479    If the resulting AIMessage contains `tool_calls`, the graph will then call the ["tools"][langgraph.prebuilt.tool_node.ToolNode].480    The "tools" node executes the tools (1 tool per `tool_call`) and adds the responses to the messages list481    as `ToolMessage` objects. The agent node then calls the language model again.482    The process repeats until no more `tool_calls` are present in the response.483    The agent then returns the full list of messages as a dictionary containing the key `'messages'`.484 485    ``` mermaid486        sequenceDiagram487            participant U as User488            participant A as LLM489            participant T as Tools490            U->>A: Initial input491            Note over A: Prompt + LLM492            loop while tool_calls present493                A->>T: Execute tools494                T-->>A: ToolMessage for each tool_calls495            end496            A->>U: Return final state497    ```498 499    Example:500        ```python501        from langgraph.prebuilt import create_react_agent502 503        def check_weather(location: str) -> str:504            '''Return the weather forecast for the specified location.'''505            return f"It's always sunny in {location}"506 507        graph = create_react_agent(508            "anthropic:claude-3-7-sonnet-latest",509            tools=[check_weather],510            prompt="You are a helpful assistant",511        )512        inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}513        for chunk in graph.stream(inputs, stream_mode="updates"):514            print(chunk)515        ```516    """517    if (518        config_schema := deprecated_kwargs.pop("config_schema", MISSING)519    ) is not MISSING:520        warnings.warn(521            "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",522            category=LangGraphDeprecatedSinceV10,523        )524 525        if context_schema is None:526            context_schema = config_schema527 528    if len(deprecated_kwargs) > 0:529        raise TypeError(530            f"create_react_agent() got unexpected keyword arguments: {deprecated_kwargs}"531        )532 533    if version not in ("v1", "v2"):534        raise ValueError(535            f"Invalid version {version}. Supported versions are 'v1' and 'v2'."536        )537 538    if state_schema is not None:539        required_keys = {"messages", "remaining_steps"}540        if response_format is not None:541            required_keys.add("structured_response")542 543        schema_keys = set(get_type_hints(state_schema))544        if missing_keys := required_keys - set(schema_keys):545            raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")546 547    if state_schema is None:548        state_schema = (549            AgentStateWithStructuredResponse550            if response_format is not None551            else AgentState552        )553 554    llm_builtin_tools: list[dict] = []555    if isinstance(tools, ToolNode):556        tool_classes = list(tools.tools_by_name.values())557        tool_node = tools558    else:559        llm_builtin_tools = [t for t in tools if isinstance(t, dict)]560        tool_node = ToolNode([t for t in tools if not isinstance(t, dict)])561        tool_classes = list(tool_node.tools_by_name.values())562 563    is_dynamic_model = not isinstance(model, (str, Runnable)) and callable(model)564    is_async_dynamic_model = is_dynamic_model and inspect.iscoroutinefunction(model)565 566    tool_calling_enabled = len(tool_classes) > 0567 568    if not is_dynamic_model:569        if isinstance(model, str):570            try:571                from langchain.chat_models import (  # type: ignore[import-not-found]572                    init_chat_model,573                )574            except ImportError:575                raise ImportError(576                    "Please install langchain (`pip install langchain`) to "577                    "use '<provider>:<model>' string syntax for `model` parameter."578                )579 580            model = cast(BaseChatModel, init_chat_model(model))581 582        if (583            _should_bind_tools(model, tool_classes, num_builtin=len(llm_builtin_tools))  # type: ignore[arg-type]584            and len(tool_classes + llm_builtin_tools) > 0585        ):586            model = cast(BaseChatModel, model).bind_tools(587                tool_classes + llm_builtin_tools  # type: ignore[operator]588            )589 590        static_model: Runnable | None = _get_prompt_runnable(prompt) | model  # type: ignore[operator]591    else:592        # For dynamic models, we'll create the runnable at runtime593        static_model = None594 595    # If any of the tools are configured to return_directly after running,596    # our graph needs to check if these were called597    should_return_direct = {t.name for t in tool_classes if t.return_direct}598 599    def _resolve_model(600        state: StateSchema, runtime: Runtime[ContextT]601    ) -> LanguageModelLike:602        """Resolve the model to use, handling both static and dynamic models."""603        if is_dynamic_model:604            return _get_prompt_runnable(prompt) | model(state, runtime)  # type: ignore[operator]605        else:606            return static_model607 608    async def _aresolve_model(609        state: StateSchema, runtime: Runtime[ContextT]610    ) -> LanguageModelLike:611        """Async resolve the model to use, handling both static and dynamic models."""612        if is_async_dynamic_model:613            resolved_model = await model(state, runtime)  # type: ignore[misc,operator]614            return _get_prompt_runnable(prompt) | resolved_model615        elif is_dynamic_model:616            return _get_prompt_runnable(prompt) | model(state, runtime)  # type: ignore[operator]617        else:618            return static_model619 620    def _are_more_steps_needed(state: StateSchema, response: BaseMessage) -> bool:621        has_tool_calls = isinstance(response, AIMessage) and response.tool_calls622        all_tools_return_direct = (623            all(call["name"] in should_return_direct for call in response.tool_calls)624            if isinstance(response, AIMessage)625            else False626        )627        remaining_steps = _get_state_value(state, "remaining_steps", None)628        if remaining_steps is not None:629            if remaining_steps < 1 and all_tools_return_direct:630                return True631            elif remaining_steps < 2 and has_tool_calls:632                return True633 634        return False635 636    def _get_model_input_state(state: StateSchema) -> StateSchema:637        if pre_model_hook is not None:638            messages = (639                _get_state_value(state, "llm_input_messages")640            ) or _get_state_value(state, "messages")641            error_msg = f"Expected input to call_model to have 'llm_input_messages' or 'messages' key, but got {state}"642        else:643            messages = _get_state_value(state, "messages")644            error_msg = (645                f"Expected input to call_model to have 'messages' key, but got {state}"646            )647 648        if messages is None:649            raise ValueError(error_msg)650 651        _validate_chat_history(messages)652        # we're passing messages under `messages` key, as this is expected by the prompt653        if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):654            state.messages = messages  # type: ignore655        else:656            state["messages"] = messages  # type: ignore657 658        return state659 660    # Define the function that calls the model661    def call_model(662        state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig663    ) -> StateSchema:664        if is_async_dynamic_model:665            msg = (666                "Async model callable provided but agent invoked synchronously. "667                "Use agent.ainvoke() or agent.astream(), or "668                "provide a sync model callable."669            )670            raise RuntimeError(msg)671 672        model_input = _get_model_input_state(state)673 674        if is_dynamic_model:675            # Resolve dynamic model at runtime and apply prompt676            dynamic_model = _resolve_model(state, runtime)677            response = cast(AIMessage, dynamic_model.invoke(model_input, config))  # type: ignore[arg-type]678        else:679            response = cast(AIMessage, static_model.invoke(model_input, config))  # type: ignore[union-attr]680 681        # add agent name to the AIMessage682        response.name = name683 684        if _are_more_steps_needed(state, response):685            return {686                "messages": [687                    AIMessage(688                        id=response.id,689                        content="Sorry, need more steps to process this request.",690                    )691                ]692            }693        # We return a list, because this will get added to the existing list694        return {"messages": [response]}695 696    async def acall_model(697        state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig698    ) -> StateSchema:699        model_input = _get_model_input_state(state)700 701        if is_dynamic_model:702            # Resolve dynamic model at runtime and apply prompt703            # (supports both sync and async)704            dynamic_model = await _aresolve_model(state, runtime)705            response = cast(AIMessage, await dynamic_model.ainvoke(model_input, config))  # type: ignore[arg-type]706        else:707            response = cast(AIMessage, await static_model.ainvoke(model_input, config))  # type: ignore[union-attr]708 709        # add agent name to the AIMessage710        response.name = name711        if _are_more_steps_needed(state, response):712            return {713                "messages": [714                    AIMessage(715                        id=response.id,716                        content="Sorry, need more steps to process this request.",717                    )718                ]719            }720        # We return a list, because this will get added to the existing list721        return {"messages": [response]}722 723    input_schema: StateSchemaType724    if pre_model_hook is not None:725        # Dynamically create a schema that inherits from state_schema and adds 'llm_input_messages'726        if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):727            # For Pydantic schemas728            from pydantic import create_model729 730            input_schema = create_model(731                "CallModelInputSchema",732                llm_input_messages=(list[AnyMessage], ...),733                __base__=state_schema,734            )735        else:736            # For TypedDict schemas737            class CallModelInputSchema(state_schema):  # type: ignore738                llm_input_messages: list[AnyMessage]739 740            input_schema = CallModelInputSchema741    else:742        input_schema = state_schema743 744    def generate_structured_response(745        state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig746    ) -> StateSchema:747        if is_async_dynamic_model:748            msg = (749                "Async model callable provided but agent invoked synchronously. "750                "Use agent.ainvoke() or agent.astream(), or provide a sync model callable."751            )752            raise RuntimeError(msg)753 754        messages = _get_state_value(state, "messages")755        structured_response_schema = response_format756        if isinstance(response_format, tuple):757            system_prompt, structured_response_schema = response_format758            messages = [SystemMessage(content=system_prompt)] + list(messages)759 760        resolved_model = _resolve_model(state, runtime)761        model_with_structured_output = _get_model(762            resolved_model763        ).with_structured_output(764            cast(StructuredResponseSchema, structured_response_schema)765        )766        response = model_with_structured_output.invoke(messages, config)767        return {"structured_response": response}768 769    async def agenerate_structured_response(770        state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig771    ) -> StateSchema:772        messages = _get_state_value(state, "messages")773        structured_response_schema = response_format774        if isinstance(response_format, tuple):775            system_prompt, structured_response_schema = response_format776            messages = [SystemMessage(content=system_prompt)] + list(messages)777 778        resolved_model = await _aresolve_model(state, runtime)779        model_with_structured_output = _get_model(780            resolved_model781        ).with_structured_output(782            cast(StructuredResponseSchema, structured_response_schema)783        )784        response = await model_with_structured_output.ainvoke(messages, config)785        return {"structured_response": response}786 787    if not tool_calling_enabled:788        # Define a new graph789        workflow = StateGraph(state_schema=state_schema, context_schema=context_schema)790        workflow.add_node(791            "agent",792            RunnableCallable(call_model, acall_model),793            input_schema=input_schema,794        )795        if pre_model_hook is not None:796            workflow.add_node("pre_model_hook", pre_model_hook)  # type: ignore[arg-type]797            workflow.add_edge("pre_model_hook", "agent")798            entrypoint = "pre_model_hook"799        else:800            entrypoint = "agent"801 802        workflow.set_entry_point(entrypoint)803 804        if post_model_hook is not None:805            workflow.add_node("post_model_hook", post_model_hook)  # type: ignore[arg-type]806            workflow.add_edge("agent", "post_model_hook")807 808        if response_format is not None:809            workflow.add_node(810                "generate_structured_response",811                RunnableCallable(812                    generate_structured_response,813                    agenerate_structured_response,814                ),815            )816            if post_model_hook is not None:817                workflow.add_edge("post_model_hook", "generate_structured_response")818            else:819                workflow.add_edge("agent", "generate_structured_response")820 821        return workflow.compile(822            checkpointer=checkpointer,823            store=store,824            interrupt_before=interrupt_before,825            interrupt_after=interrupt_after,826            debug=debug,827            name=name,828        )829 830    # Define the function that determines whether to continue or not831    def should_continue(state: StateSchema) -> str | list[Send]:832        messages = _get_state_value(state, "messages")833        last_message = messages[-1]834        # If there is no function call, then we finish835        if not isinstance(last_message, AIMessage) or not last_message.tool_calls:836            if post_model_hook is not None:837                return "post_model_hook"838            elif response_format is not None:839                return "generate_structured_response"840            else:841                return END842        # Otherwise if there is, we continue843        else:844            if version == "v1":845                return "tools"846            elif version == "v2":847                if post_model_hook is not None:848                    return "post_model_hook"849                return [850                    Send(851                        "tools",852                        ToolCallWithContext(853                            __type="tool_call_with_context",854                            tool_call=call,855                            state=state,856                        ),857                    )858                    for call in last_message.tool_calls859                ]860 861    # Define a new graph862    workflow = StateGraph(863        state_schema=state_schema or AgentState, context_schema=context_schema864    )865 866    # Define the two nodes we will cycle between867    workflow.add_node(868        "agent",869        RunnableCallable(call_model, acall_model),870        input_schema=input_schema,871    )872    workflow.add_node("tools", tool_node)873 874    # Optionally add a pre-model hook node that will be called875    # every time before the "agent" (LLM-calling node)876    if pre_model_hook is not None:877        workflow.add_node("pre_model_hook", pre_model_hook)  # type: ignore[arg-type]878        workflow.add_edge("pre_model_hook", "agent")879        entrypoint = "pre_model_hook"880    else:881        entrypoint = "agent"882 883    # Set the entrypoint as `agent`884    # This means that this node is the first one called885    workflow.set_entry_point(entrypoint)886 887    agent_paths = []888    post_model_hook_paths = [entrypoint, "tools"]889 890    # Add a post model hook node if post_model_hook is provided891    if post_model_hook is not None:892        workflow.add_node("post_model_hook", post_model_hook)  # type: ignore[arg-type]893        agent_paths.append("post_model_hook")894        workflow.add_edge("agent", "post_model_hook")895    else:896        agent_paths.append("tools")897 898    # Add a structured output node if response_format is provided899    if response_format is not None:900        workflow.add_node(901            "generate_structured_response",902            RunnableCallable(903                generate_structured_response,904                agenerate_structured_response,905            ),906        )907        if post_model_hook is not None:908            post_model_hook_paths.append("generate_structured_response")909        else:910            agent_paths.append("generate_structured_response")911    else:912        if post_model_hook is not None:913            post_model_hook_paths.append(END)914        else:915            agent_paths.append(END)916 917    if post_model_hook is not None:918 919        def post_model_hook_router(state: StateSchema) -> str | list[Send]:920            """Route to the next node after post_model_hook.921 922            Routes to one of:923            * "tools": if there are pending tool calls without a corresponding message.924            * "generate_structured_response": if no pending tool calls exist and response_format is specified.925            * END: if no pending tool calls exist and no response_format is specified.926            """927 928            messages = _get_state_value(state, "messages")929            tool_messages = [930                m.tool_call_id for m in messages if isinstance(m, ToolMessage)931            ]932            last_ai_message = next(933                m for m in reversed(messages) if isinstance(m, AIMessage)934            )935            pending_tool_calls = [936                c for c in last_ai_message.tool_calls if c["id"] not in tool_messages937            ]938 939            if pending_tool_calls:940                return [941                    Send(942                        "tools",943                        ToolCallWithContext(944                            __type="tool_call_with_context",945                            tool_call=call,946                            state=state,947                        ),948                    )949                    for call in pending_tool_calls950                ]951            elif isinstance(messages[-1], ToolMessage):952                return entrypoint953            elif response_format is not None:954                return "generate_structured_response"955            else:956                return END957 958        workflow.add_conditional_edges(959            "post_model_hook",960            post_model_hook_router,961            path_map=post_model_hook_paths,962        )963 964    workflow.add_conditional_edges(965        "agent",966        should_continue,967        path_map=agent_paths,968    )969 970    def route_tool_responses(state: StateSchema) -> str:971        for m in reversed(_get_state_value(state, "messages")):972            if not isinstance(m, ToolMessage):973                break974            if m.name in should_return_direct:975                return END976 977        # handle a case of parallel tool calls where978        # the tool w/ `return_direct` was executed in a different `Send`979        if isinstance(m, AIMessage) and m.tool_calls:980            if any(call["name"] in should_return_direct for call in m.tool_calls):981                return END982 983        return entrypoint984 985    if should_return_direct:986        workflow.add_conditional_edges(987            "tools", route_tool_responses, path_map=[entrypoint, END]988        )989    else:990        workflow.add_edge("tools", entrypoint)991 992    # Finally, we compile it!993    # This compiles it into a LangChain Runnable,994    # meaning you can use it as you would any other runnable995    return workflow.compile(996        checkpointer=checkpointer,997        store=store,998        interrupt_before=interrupt_before,999        interrupt_after=interrupt_after,1000        debug=debug,1001        name=name,1002    )1003 1004 1005# Keep for backwards compatibility1006create_tool_calling_executor = create_react_agent1007 1008__all__ = [1009    "create_react_agent",1010    "create_tool_calling_executor",1011    "AgentState",1012    "AgentStatePydantic",1013    "AgentStateWithStructuredResponse",1014    "AgentStateWithStructuredResponsePydantic",1015]1016 
codekingpro/portable-devtools · Team Ai