diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index c2d6d80d2..afc4c91c6 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -21,7 +21,11 @@ jobs: - "3.10" - "3.11" - "3.12" - name: "test #${{ matrix.python-version }}" + core-version: + - ">=0.3.0.dev1,<0.4.0" + - "latest" + + name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }})" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }} @@ -35,7 +39,11 @@ jobs: - name: Install dependencies shell: bash working-directory: ${{ inputs.working-directory }} - run: poetry install --with dev + run: | + poetry install --with dev + if [ "${{ matrix.core-version }}" != "latest" ]; then + poetry run pip install "langchain-core${{ matrix.core-version }}" + fi - name: Run core tests shell: bash diff --git a/README.md b/README.md index 87d19961f..c3da88ec4 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage from langchain_anthropic import ChatAnthropic from langchain_core.tools import tool from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import END, StateGraph, MessagesState +from langgraph.graph import END, START, StateGraph, MessagesState from langgraph.prebuilt import ToolNode @@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") # We now add a conditional edge workflow.add_conditional_edges( diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 2440185f9..28bd89111 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -26,10 +26,14 @@ _MANUAL = { "streaming-events-from-within-tools.ipynb", "streaming-events-from-within-tools-without-langchain.ipynb", "streaming-from-final-node.ipynb", + "streaming-subgraphs.ipynb", "persistence.ipynb", "input_output_schema.ipynb", "pass_private_state.ipynb", "memory/manage-conversation-history.ipynb", + "memory/shared-state.ipynb", + "subgraphs-manage-state.ipynb", + "subgraph-transform-state.ipynb", "memory/delete-messages.ipynb", "memory/add-summary-conversation-history.ipynb", "persistence_postgres.ipynb", @@ -38,6 +42,7 @@ _MANUAL = { "visualization.ipynb", "state-model.ipynb", "subgraph.ipynb", + "recursion-limit.ipynb", "force-calling-a-tool-first.ipynb", "pass-run-time-values-to-tools.ipynb", "tool-calling.ipynb", @@ -56,11 +61,14 @@ _MANUAL = { "create-react-agent-memory.ipynb", "create-react-agent-hitl.ipynb", "human_in_the_loop/breakpoints.ipynb", + "human_in_the_loop/dynamic_breakpoints.ipynb", "human_in_the_loop/time-travel.ipynb", "human_in_the_loop/edit-graph-state.ipynb", "human_in_the_loop/wait-user-input.ipynb", "human_in_the_loop/review-tool-calls.ipynb", "node-retries.ipynb", + "react_diagrams.png", + "react-agent-structured-output.ipynb", ], "tutorials": [ "introduction.ipynb", diff --git a/docs/docs/cloud/concepts/api.md b/docs/docs/cloud/concepts/api.md index e18f52c55..99bd6c9ab 100644 --- a/docs/docs/cloud/concepts/api.md +++ b/docs/docs/cloud/concepts/api.md @@ -39,7 +39,7 @@ It's often useful to run graphs on some schedule. LangGraph Cloud supports cron - Create a new thread with the specified assistant - Send the specified input to that thread -Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs. +Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cron_jobs.md) for creating cron jobs. The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons) for more details. @@ -51,16 +51,108 @@ The LangGraph Cloud API offers several features to support complex agent archite Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. The LangGraph Cloud API supports five streaming modes. -- `values`: Stream the full state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values. +- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values. - `messages`: Stream complete messages (at the end of node execution) as well as tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. This is only an option if your graph contains a `messages` key. See the [how-to guide](../how-tos/stream_messages.md) for streaming messages. - `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_updates.md) for streaming updates. -- `events`: Stream all events (including the state of the graph) after each node is executed. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs. -- `debug`: Stream debug events after each node is executed. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events. +- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs. +- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events. You can also specify multiple streaming modes at the same time. See the [how-to guide](../how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream) for how to create streaming runs. +Streaming modes `values`, `updates`, and `debug` are very similar to modes available in the LangGraph library - for a deeper conceptual explanation of those, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming). + +Streaming mode `events` is the same as using `.astream_events` in the LangGraph library - for a deeper conceptual explanation of this, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming). + +#### `mode="messages"` +Streaming mode `messages` is a new streaming mode, currently only available in the API. What does this mode enable? + +This mode is focused on streaming back messages. It currently assumes that you have a `messages` key in your graph that is a list of messages. Assuming we have a simple react agent deployed, what does this stream look like? + +All events emitted have two attributes: + +- `event`: This is the name of the event +- `data`: This is data associated with the event + +Let's run it on a question that should trigger a tool call: + +```python +thread = await client.threads.create() +input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + +events = [] +async for event in client.runs.stream( + thread["thread_id"], + assistant_id="agent", # This may need to change depending on the graph you deployed + input=input, + stream_mode="messages", +): + print(event.event) +``` +```shell +metadata +messages/complete +messages/metadata +messages/partial +... +messages/partial +messages/complete +messages/complete +messages/metadata +messages/partial +... +messages/partial +messages/complete +end +``` + +We first get some `metadata` - this is metadata about the run. + +```python +StreamPart(event='metadata', data={'run_id': '1ef657cf-ae55-6f65-97d4-f4ed1dbdabc6'}) +``` + +We then get a `messages/complete` event - this a fully formed message getting emitted. In this case, +this was the just the input message we sent in. + +```python +StreamPart(event='messages/complete', data=[{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '833c09a3-bb19-46c9-81d9-1e5954ec5f92', 'example': False}]) +``` + +We then get a `messages/metadata` - this is just letting us know that a new message is starting. + +```python +StreamPart(event='messages/metadata', data={'run-985c0f14-9f43-40d4-a505-4637fc58e333': {'metadata': {'created_by': 'system', 'run_id': '1ef657de-7594-66df-8eb2-31518e4a1ee2', 'graph_id': 'agent', 'thread_id': 'c178eab5-e293-423c-8e7d-1d113ffe7cd9', 'model_name': 'openai', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_provider': 'openai', 'ls_model_name': 'gpt-4o', 'ls_model_type': 'chat', 'ls_temperature': 0.0}}}) +``` + +We then get a BUNCH of `messages/partial` events - these are the individual tokens from the LLM! In the case below, we can see the START of a tool call. + +```python +StreamPart(event='messages/partial', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [{'name': 'tavily_search_results_json', 'args': '', 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'error': None}], 'usage_metadata': None}]) +``` + +After that, we get a `messages/complete` event - this is the AIMessage finishing. It's now a complete tool call: + +```python +StreamPart(event='messages/complete', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '{"query":"current weather in San Francisco"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}], 'invalid_tool_calls': [], 'usage_metadata': None}]) +``` + +After that, we get ANOTHER `messages/complete` event. This is a tool message - our agent has called a tool, gotten a response, and now inserting it into the state in the form of a tool message. + +```python +StreamPart(event='messages/complete', data=[{'content': '[{"url": "https://www.weatherapi.com/", "content": "{\'location\': {\'name\': \'San Francisco\', \'region\': \'California\', \'country\': \'United States of America\', \'lat\': 37.78, \'lon\': -122.42, \'tz_id\': \'America/Los_Angeles\', \'localtime_epoch\': 1724877689, \'localtime\': \'2024-08-28 13:41\'}, \'current\': {\'last_updated_epoch\': 1724877000, \'last_updated\': \'2024-08-28 13:30\', \'temp_c\': 23.3, \'temp_f\': 73.9, \'is_day\': 1, \'condition\': {\'text\': \'Partly cloudy\', \'icon\': \'//cdn.weatherapi.com/weather/64x64/day/116.png\', \'code\': 1003}, \'wind_mph\': 15.0, \'wind_kph\': 24.1, \'wind_degree\': 310, \'wind_dir\': \'NW\', \'pressure_mb\': 1014.0, \'pressure_in\': 29.93, \'precip_mm\': 0.0, \'precip_in\': 0.0, \'humidity\': 57, \'cloud\': 25, \'feelslike_c\': 25.0, \'feelslike_f\': 77.1, \'windchill_c\': 20.9, \'windchill_f\': 69.6, \'heatindex_c\': 23.3, \'heatindex_f\': 74.0, \'dewpoint_c\': 12.9, \'dewpoint_f\': 55.2, \'vis_km\': 16.0, \'vis_miles\': 9.0, \'uv\': 6.0, \'gust_mph\': 19.5, \'gust_kph\': 31.3}}"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '0112eba5-7660-4375-9f24-c7a1d6777b97', 'tool_call_id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}]) +``` + +After that, we see the agent doing another LLM call and streaming back a response. We then get an `end` event: + +```python +StreamPart(event='end', data=None) +``` + +And that's it! This is more focused streaming mode specifically focused on streaming back messages. See this [how-to guide](../how-tos/stream_messages.md) for more information. + + ### Human-in-the-Loop There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in the [human in the loop how-tos](../how-tos/index.md#human-in-the-loop). @@ -90,13 +182,13 @@ The only difference is in stateless background runs, if the task worker dies hal - whereas a stateful background run would retry from the last successful checkpoint - a stateless background run would retry from the beginning -See the [how-to guide](../how-tos/cloud_examples/stateless_runs.ipynb) for creating stateless runs. +See the [how-to guide](../how-tos/stateless_runs.md) for creating stateless runs. ### Webhooks For all types of runs, langgraph cloud supports completion webhooks. When you create the run you can pass a webhook URL to be called when the completes (successfully or not). This is especially useful for background runs and cron jobs, as the webhook can give you an indication the run has completed and you can perform further actions for your appilcation. -See this [how-to guide](../how-tos/cloud_examples/webhooks.ipynb) to learn about how to use webhooks with LangGraph Cloud. +See this [how-to guide](../how-tos/webhooks.md) to learn about how to use webhooks with LangGraph Cloud. ## Deployment diff --git a/docs/docs/cloud/deployment/custom_docker.md b/docs/docs/cloud/deployment/custom_docker.md new file mode 100644 index 000000000..8bc520f12 --- /dev/null +++ b/docs/docs/cloud/deployment/custom_docker.md @@ -0,0 +1,19 @@ +# How to customize Dockerfile + +Users can add an array of additional lines to add to the Dockerfile following the import from the parent LangGraph image. In order to do this, you simply need to modify your `langgraph.json` file by passing in the commands you want run to the `dockerfile_lines` key. For example, if we wanted to use `Pillow` in our graph you would need to add the following dependencies: + +``` +{ + "dependencies": ["."], + "graphs": { + "openai_agent": "./openai_agent.py:agent", + }, + "env": "./.env", + "dockerfile_lines": [ + "RUN apt-get update && apt-get install -y libjpeg-dev zlib1g-dev libpng-dev", + "RUN pip install Pillow" + ] +} +``` + +This would install the system packages required to use Pillow if we were working with `jpeq` or `png` image formats. \ No newline at end of file diff --git a/docs/docs/cloud/deployment/graph_rebuild.md b/docs/docs/cloud/deployment/graph_rebuild.md index c7853b30a..b1034cd0f 100644 --- a/docs/docs/cloud/deployment/graph_rebuild.md +++ b/docs/docs/cloud/deployment/graph_rebuild.md @@ -28,7 +28,7 @@ In the standard LangGraph API configuration, the server uses the compiled graph ```python from langchain_openai import ChatOpenAI -from langgraph.graph import END, MessageGraph +from langgraph.graph import END, START, MessageGraph model = ChatOpenAI(temperature=0) @@ -36,7 +36,7 @@ graph_workflow = MessageGraph() graph_workflow.add_node("agent", model) graph_workflow.add_edge("agent", END) -graph_workflow.set_entry_point("agent") +graph_workflow.add_edge(START, "agent") agent = graph_workflow.compile() ``` @@ -60,7 +60,7 @@ To make your graph rebuild on each new run with custom configuration, you need t ```python from typing import Annotated, TypedDict from langchain_openai import ChatOpenAI -from langgraph.graph import END, MessageGraph +from langgraph.graph import END, START, MessageGraph from langgraph.graph.state import StateGraph from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode @@ -83,7 +83,7 @@ def make_default_graph(): graph_workflow.add_node("agent", call_model) graph_workflow.add_edge("agent", END) - graph_workflow.set_entry_point("agent") + graph_workflow.add_edge(START, "agent") agent = graph_workflow.compile() return agent @@ -113,7 +113,7 @@ def make_alternative_graph(): graph_workflow.add_node("agent", call_model) graph_workflow.add_node("tools", tool_node) graph_workflow.add_edge("tools", "agent") - graph_workflow.set_entry_point("agent") + graph_workflow.add_edge(START, "agent") graph_workflow.add_conditional_edges("agent", should_continue) agent = graph_workflow.compile() diff --git a/docs/docs/cloud/deployment/self_hosted.md b/docs/docs/cloud/deployment/self_hosted.md deleted file mode 100644 index 33f0ff29d..000000000 --- a/docs/docs/cloud/deployment/self_hosted.md +++ /dev/null @@ -1,35 +0,0 @@ -# How to Self-Host LangGraph Cloud API - -!!! warning "Enterprise License Required" - Self-hosting LangGraph Cloud API requires a license key. Please contact sales@langchain.dev for more details. - -LangGraph Cloud APIs can be self-hosted with a valid LangGraph Cloud license key. Self-hosted deployments are built with Docker and deployed with Helm (on Kubernetes) or with Docker Compose. Ensure that the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/) is installed. - -LangGraph Cloud license key should be passed to the service as an environment variable named LANGGRAPH_CLOUD_LICENSE_KEY. - -## Build Docker Image - -1. Follow the [How-to Guide](setup.md) for setting up a LangGraph application for deployment. Your LangGraph application will vary from the example in the How-to Guide. However, ensure that the [LangGraph API configuration file](../reference/cli.md#configuration-file) is created. -1. Install the [LangGraph CLI](../reference/cli.md#installation). -1. Run the following LangGraph CLI `build` command to build a Docker image. Specify the image tag (`-t`) and other desired [options](../reference/cli.md#build). - - langgraph build -t tag_name - -!!! info "Build Platform" - When building the Docker image, ensure that the image is built for the platform of the target Kubernetes cluster: `langgraph build -t tag_name --platform linux/amd64,linux/arm64` - -## Self-Host on Kubernetes - -This section is for self-hosting LangGraph Cloud API on Kubernetes via Helm. A Kubernetes cluster must be provisioned before proceeding with these steps. The public Helm chart for LangGraph Cloud is available [here](https://github.com/langchain-ai/helm/tree/main/charts/langgraph-cloud). - -1. Publish the built Docker image to a repository that can be accessed by the target Kubernetes cluster. -1. Ensure that the [Helm client](https://github.com/helm/helm?tab=readme-ov-file#install) is installed. -1. Make note of all environment variables that are needed for the application. These values will need to be set in the Helm `values` YAML configuration. -1. Follow [these instructions](https://github.com/langchain-ai/helm/tree/main/charts/langgraph-cloud#readme) to configure the Helm chart and deploy to Kubernetes. - -## Self-Host with Docker - -!!! warning "Under Construction" - This section of the documentation is in progress. - -Docker Compose can be used to deploy LangGraph Cloud to the compute infrastructure of your choice (e.g. VM). diff --git a/docs/docs/cloud/deployment/setup.md b/docs/docs/cloud/deployment/setup.md index 170e19394..cd6248a8f 100644 --- a/docs/docs/cloud/deployment/setup.md +++ b/docs/docs/cloud/deployment/setup.md @@ -1,15 +1,14 @@ # How to Set Up a LangGraph Application for Deployment -A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies. +A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies. This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment. !!! tip "Setup with pyproject.toml" - If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud. +If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud. !!! tip "Setup with a Monorepo" - If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so. - +If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so. The final repo structure will look something like this: @@ -35,23 +34,27 @@ After each step, an example file directory is provided to demonstrate how code c Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range: + ``` -langgraph>=0.2.0,<0.3.0 +langgraph>=0.2.7,<0.3.0 +langgraph-checkpoint>=1.0.4 langchain-core>=0.2.27,<0.3.0 langsmith>=0.1.63 -orjson>=3.10.1 -httpx>=0.27.0 -tenacity>=8.3.0 -uvicorn>=0.29.0 +orjson>=3.9.7 +httpx>=0.25.0 +tenacity>=8.0.0 +uvicorn>=0.26.0 sse-starlette>=2.1.0 -uvloop>=0.19.0 -httptools>=0.6.1 -jsonschema-rs>=0.18.0 +uvloop>=0.18.0 +httptools>=0.5.0 +jsonschema-rs>=0.16.3 croniter>=1.0.1 -structlog>=24.4.0 +structlog>=23.1.0 +redis>=5.0.0,<6.0.0 ``` Example `requirements.txt` file: + ``` langgraph langchain_anthropic @@ -62,6 +65,7 @@ langchain_openai ``` Example file directory: + ```bash my-app/ ├── my_agent # all project code lies within here @@ -73,6 +77,7 @@ my-app/ Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment. Example `.env` file: + ``` MY_ENV_VAR_1=foo MY_ENV_VAR_2=bar @@ -94,12 +99,11 @@ Implement your graphs! Graphs can be defined in a single file or multiple files. Example `agent.py` file, which shows how to import from other modules you define (code for the modules is not shown here, please see [this repo](https://github.com/langchain-ai/langgraph-example) to see their implementation): - ```python # my_agent/agent.py from typing import TypedDict, Literal -from langgraph.graph import StateGraph, END +from langgraph.graph import StateGraph, END, START from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes from my_agent.utils.state import AgentState # import state @@ -110,7 +114,7 @@ class GraphConfig(TypedDict): workflow = StateGraph(AgentState, config_schema=GraphConfig) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") workflow.add_conditional_edges( "agent", should_continue, @@ -125,9 +129,10 @@ graph = workflow.compile() ``` !!! warning "Assign `CompiledGraph` to Variable" - The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). +The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). Example file directory: + ```bash my-app/ ├── my_agent # all project code lies within here @@ -147,6 +152,7 @@ my-app/ Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file. Example `langgraph.json` file: + ```json { "dependencies": ["./my_agent"], @@ -160,7 +166,7 @@ Example `langgraph.json` file: Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:`). !!! warning "Configuration Location" - The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies. +The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies. Example file directory: diff --git a/docs/docs/cloud/deployment/setup_javascript.md b/docs/docs/cloud/deployment/setup_javascript.md new file mode 100644 index 000000000..fa4c7b823 --- /dev/null +++ b/docs/docs/cloud/deployment/setup_javascript.md @@ -0,0 +1,200 @@ +# How to Set Up a LangGraph.js Application for Deployment + +A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies. + +This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment. + +The final repo structure will look something like this: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +After each step, an example file directory is provided to demonstrate how code can be organized. + +## Specify Dependencies + +Dependencies can be specified in a `package.json`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). + +Example `package.json` file: + +```json +{ + "name": "langgraphjs-studio-starter", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "^0.2.0", + "@langchain/openai": "^0.2.8" + } +} +``` + +Example file directory: + +```bash +my-app/ +└── package.json # package dependencies +``` + +## Specify Environment Variables + +Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment. + +Example `.env` file: + +``` +MY_ENV_VAR_1=foo +MY_ENV_VAR_2=bar +OPENAI_API_KEY=key +TAVILY_API_KEY=key_2 +``` + +Example file directory: + +```bash +my-app/ +├── package.json +└── .env # environment variables +``` + +## Define Graphs + +Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each compiled graph to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file). + +Here is an example `agent.ts`: + +```ts +import type { AIMessage } from "@langchain/core/messages"; +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { ChatOpenAI } from "@langchain/openai"; + +import { MessagesAnnotation, StateGraph } from "@langchain/langgraph"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; + +const tools = [ + new TavilySearchResults({ maxResults: 3, }), +]; + +// Define the function that calls the model +async function callModel( + state: typeof MessagesAnnotation.State, +) { + /** + * Call the LLM powering our agent. + * Feel free to customize the prompt, model, and other logic! + */ + const model = new ChatOpenAI({ + model: "gpt-4o", + }).bindTools(tools); + + const response = await model.invoke([ + { + role: "system", + content: `You are a helpful assistant. The current date is ${new Date().getTime()}.` + }, + ...state.messages + ]); + + // MessagesAnnotation supports returning a single message or array of messages + return { messages: response }; +} + +// Define the function that determines whether to continue or not +function routeModelOutput(state: typeof MessagesAnnotation.State) { + const messages = state.messages; + const lastMessage: AIMessage = messages[messages.length - 1]; + // If the LLM is invoking tools, route there. + if ((lastMessage?.tool_calls?.length ?? 0) > 0) { + return "tools"; + } + // Otherwise end the graph. + return "__end__"; +} + +// Define a new graph. +// See https://langchain-ai.github.io/langgraphjs/how-tos/define-state/#getting-started for +// more on defining custom graph states. +const workflow = new StateGraph(MessagesAnnotation) + // Define the two nodes we will cycle between + .addNode("callModel", callModel) + .addNode("tools", new ToolNode(tools)) + // Set the entrypoint as `callModel` + // This means that this node is the first one called + .addEdge("__start__", "callModel") + .addConditionalEdges( + // First, we define the edges' source node. We use `callModel`. + // This means these are the edges taken after the `callModel` node is called. + "callModel", + // Next, we pass in the function that will determine the sink node(s), which + // will be called after the source node is called. + routeModelOutput, + // List of the possible destinations the conditional edge can route to. + // Required for conditional edges to properly render the graph in Studio + [ + "tools", + "__end__" + ], + ) + // This means that after `tools` is called, `callModel` node is called next. + .addEdge("tools", "callModel"); + +// Finally, we compile it! +// This compiles it into a graph you can invoke and deploy. +export const graph = workflow.compile(); +``` + +!!! info "Assign `CompiledGraph` to Variable" + The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). + +Example file directory: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +## Create LangGraph API Config + +Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file. + +Example `langgraph.json` file: + +```json +{ + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" +} +``` + +Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:`). + +!!! info "Configuration Location" + The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies. + +## Next + +After you setup your project and place it in a github repo, it's time to [deploy your app](./cloud.md). diff --git a/docs/docs/cloud/deployment/setup_pyproject.md b/docs/docs/cloud/deployment/setup_pyproject.md index 767171f05..445ec0e4e 100644 --- a/docs/docs/cloud/deployment/setup_pyproject.md +++ b/docs/docs/cloud/deployment/setup_pyproject.md @@ -1,8 +1,8 @@ # How to Set Up a LangGraph Application for Deployment -A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `pyproject.toml` to define your package's dependencies. +A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `pyproject.toml` to define your package's dependencies. -This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment. +This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example-pyproject), which you can play around with to learn more about how to setup your LangGraph application for deployment. !!! tip "Setup with requirements.txt" If you prefer using `requirements.txt` for dependency management, check out [this how-to guide](./setup.md). @@ -34,20 +34,23 @@ After each step, an example file directory is provided to demonstrate how code c Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range: + ``` -langgraph>=0.2.0,<0.3.0 +langgraph>=0.2.7,<0.3.0 +langgraph-checkpoint>=1.0.4 langchain-core>=0.2.27,<0.3.0 langsmith>=0.1.63 -orjson>=3.10.1 -httpx>=0.27.0 -tenacity>=8.3.0 -uvicorn>=0.29.0 +orjson>=3.9.7 +httpx>=0.25.0 +tenacity>=8.0.0 +uvicorn>=0.26.0 sse-starlette>=2.1.0 -uvloop>=0.19.0 -httptools>=0.6.1 -jsonschema-rs>=0.18.0 +uvloop>=0.18.0 +httptools>=0.5.0 +jsonschema-rs>=0.16.3 croniter>=1.0.1 structlog>=24.4.0 +redis>=5.0.8,<6.0.0 ``` Example `pyproject.toml` file: @@ -109,7 +112,7 @@ Example `agent.py` file, which shows how to import from other modules you define # my_agent/agent.py from typing import TypedDict, Literal -from langgraph.graph import StateGraph, END +from langgraph.graph import StateGraph, END, START from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes from my_agent.utils.state import AgentState # import state @@ -120,7 +123,7 @@ class GraphConfig(TypedDict): workflow = StateGraph(AgentState, config_schema=GraphConfig) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") workflow.add_conditional_edges( "agent", should_continue, diff --git a/docs/docs/cloud/deployment/test_locally.md b/docs/docs/cloud/deployment/test_locally.md index a5a06624a..2822b4052 100644 --- a/docs/docs/cloud/deployment/test_locally.md +++ b/docs/docs/cloud/deployment/test_locally.md @@ -49,6 +49,7 @@ You can either initialize by passing authentication or by setting an environment # only pass the url argument to get_client() if you changed the default port when calling langgraph up client = get_client(url=,api_key=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -60,7 +61,8 @@ You can either initialize by passing authentication or by setting an environment // only set the apiUrl if you changed the default port when calling langgraph up const client = new Client({ apiUrl: , apiKey: }); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -85,6 +87,7 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex # only pass the url argument to get_client() if you changed the default port when calling langgraph up client = get_client() + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -96,7 +99,8 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex // only set the apiUrl if you changed the default port when calling langgraph up const client = new Client(); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` diff --git a/docs/docs/cloud/faq/studio.md b/docs/docs/cloud/faq/studio.md index cef962f34..232699315 100644 --- a/docs/docs/cloud/faq/studio.md +++ b/docs/docs/cloud/faq/studio.md @@ -43,23 +43,31 @@ If you don't define your conditional edges carefully, you might notice extra edg ### Solution 1: Include a path map -The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: +The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary or array that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: -```python -graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) -``` +=== "Python" + + ```python + graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) + ``` + +=== "Javascript" + + ```ts + graph.addConditionalEdges("node_a", routingFunction, { true: "node_b", false: "node_c" }); + ``` In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively. -### Solution 2: Update the typing of the router +### Solution 2: Update the typing of the router (Python only) Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way: ```python def routing_function(state: GraphState) -> Literal["node_b","node_c"]: if state['some_condition'] == True: - return "node_a" - else: return "node_b" + else: + return "node_c" ``` diff --git a/docs/docs/cloud/how-tos/background_run.md b/docs/docs/cloud/how-tos/background_run.md new file mode 100644 index 000000000..9dc23a1c7 --- /dev/null +++ b/docs/docs/cloud/how-tos/background_run.md @@ -0,0 +1,444 @@ +# How to kick off background runs + +This guide covers how to kick off background runs for your agent. +This can be useful for long running jobs. + +First let's set up our client and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +Output: + + { + 'thread_id': '5cb1e8a1-34b3-4a61-a34e-71a9799bd00d', + 'created_at': '2024-08-30T20:35:52.062934+00:00', + 'updated_at': '2024-08-30T20:35:52.062934+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +If we list the current runs on this thread, we will see that it's empty: + +=== "Python" + + ```python + runs = await client.runs.list(thread["thread_id"]) + print(runs) + ``` + +=== "Javascript" + + ```js + let runs = await client.runs.list(thread['thread_id']); + console.log(runs); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs + ``` + +Output: + + [] + +Now let's kick off a run: + +=== "Python" + + ```python + input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]} + run = await client.runs.create(thread["thread_id"], assistant_id, input=input) + ``` + +=== "Javascript" + + ```js + let input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]}; + let run = await client.runs.create(thread["thread_id"], assistantID, { input }); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": + }' + ``` + +The first time we poll it, we can see `status=pending`: + +=== "Python" + + ```python + print(await client.runs.get(thread["thread_id"], run["run_id"])) + ``` + +=== "Javascript" + + ```js + console.log(await client.runs.get(thread["thread_id"], run["run_id"])); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs/ + ``` + +Output: + + { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "created_at": "2024-09-04T01:46:47.244887+00:00", + "updated_at": "2024-09-04T01:46:47.244887+00:00", + "metadata": {}, + "status": "pending", + "kwargs": { + "input": { + "messages": [ + { + "role": "human", + "content": "what's the weather in sf" + } + ] + }, + "config": { + "metadata": { + "created_by": "system" + }, + "configurable": { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "user_id": "", + "graph_id": "agent", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "checkpoint_id": null + } + }, + "webhook": null, + "temporary": false, + "stream_mode": [ + "values" + ], + "feedback_keys": null, + "interrupt_after": null, + "interrupt_before": null + }, + "multitask_strategy": "reject" + } + + + +Now we can join the run, wait for it to finish and check that status again: + +=== "Python" + + ```python + await client.runs.join(thread["thread_id"], run["run_id"]) + print(await client.runs.get(thread["thread_id"], run["run_id"])) + ``` + +=== "Javascript" + + ```js + await client.runs.join(thread["thread_id"], run["run_id"]); + console.log(await client.runs.get(thread["thread_id"], run["run_id"])); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs//join && + curl --request GET \ + --url /threads//runs/ + ``` + +Output: + + { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "created_at": "2024-09-04T01:46:47.244887+00:00", + "updated_at": "2024-09-04T01:46:47.244887+00:00", + "metadata": {}, + "status": "success", + "kwargs": { + "input": { + "messages": [ + { + "role": "human", + "content": "what's the weather in sf" + } + ] + }, + "config": { + "metadata": { + "created_by": "system" + }, + "configurable": { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "user_id": "", + "graph_id": "agent", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "checkpoint_id": null + } + }, + "webhook": null, + "temporary": false, + "stream_mode": [ + "values" + ], + "feedback_keys": null, + "interrupt_after": null, + "interrupt_before": null + }, + "multitask_strategy": "reject" + } + + +Perfect! The run succeeded as we would expect. We can double check that the run worked as expected by printing out the final state: + +=== "Python" + + ```python + final_result = await client.threads.get_state(thread["thread_id"]) + print(final_result) + ``` + +=== "Javascript" + + ```js + let finalResult = await client.threads.getState(thread["thread_id"]); + console.log(finalResult); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state + ``` + +Output: + + { + "values": { + "messages": [ + { + "content": "what's the weather in sf", + "additional_kwargs": {}, + "response_metadata": {}, + "type": "human", + "name": null, + "id": "beba31bf-320d-4125-9c37-cadf526ac47a", + "example": false + }, + { + "content": [ + { + "id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "input": {}, + "name": "tavily_search_results_json", + "type": "tool_use", + "index": 0, + "partial_json": "{\"query\": \"weather in san francisco\"}" + } + ], + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "tool_use", + "stop_sequence": null + }, + "type": "ai", + "name": null, + "id": "run-f220faf8-1d27-4f73-ad91-6bb3f47e8639", + "example": false, + "tool_calls": [ + { + "name": "tavily_search_results_json", + "args": { + "query": "weather in san francisco" + }, + "id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "type": "tool_call" + } + ], + "invalid_tool_calls": [], + "usage_metadata": { + "input_tokens": 273, + "output_tokens": 61, + "total_tokens": 334 + } + }, + { + "content": "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}\"}]", + "additional_kwargs": {}, + "response_metadata": {}, + "type": "tool", + "name": "tavily_search_results_json", + "id": "686b2487-f332-4e58-9508-89b3a814cd81", + "tool_call_id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "artifact": { + "query": "weather in san francisco", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "title": "Weather in San Francisco", + "url": "https://www.weatherapi.com/", + "content": "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}", + "score": 0.976148, + "raw_content": null + } + ], + "response_time": 3.07 + }, + "status": "success" + }, + { + "content": [ + { + "text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.", + "type": "text", + "index": 0 + } + ], + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "end_turn", + "stop_sequence": null + }, + "type": "ai", + "name": null, + "id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a", + "example": false, + "tool_calls": [], + "invalid_tool_calls": [], + "usage_metadata": { + "input_tokens": 837, + "output_tokens": 124, + "total_tokens": 961 + } + } + ] + }, + "next": [], + "tasks": [], + "metadata": { + "step": 3, + "run_id": "1ef67140-eb23-684b-8253-91d4c90bb05e", + "source": "loop", + "writes": { + "agent": { + "messages": [ + { + "id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a", + "name": null, + "type": "ai", + "content": [ + { + "text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.", + "type": "text", + "index": 0 + } + ], + "example": false, + "tool_calls": [], + "usage_metadata": { + "input_tokens": 837, + "total_tokens": 961, + "output_tokens": 124 + }, + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "end_turn", + "stop_sequence": null + }, + "invalid_tool_calls": [] + } + ] + } + }, + "user_id": "", + "graph_id": "agent", + "thread_id": "5cb1e8a1-34b3-4a61-a34e-71a9799bd00d", + "created_by": "system", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca" + }, + "created_at": "2024-08-30T21:09:00.079909+00:00", + "checkpoint_id": "1ef67141-3ca2-6fae-8003-fe96832e57d6", + "parent_checkpoint_id": "1ef67141-2129-6b37-8002-61fc3bf69cb5" + } + +We can also just print the content of the last AIMessage: + +=== "Python" + + ```python + print(final_result['values']['messages'][-1]['content'][0]['text']) + ``` + +=== "Javascript" + + ```js + console.log(finalResult['values']['messages'][finalResult['values']['messages'].length-1]['content'][0]['text']); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -r '.values.messages[-1].content.[0].text' + ``` + +Output: + + The search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70°F (21.1°C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions. \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/check_thread_status.md b/docs/docs/cloud/how-tos/check_thread_status.md index 655b918c1..8e9d5f196 100644 --- a/docs/docs/cloud/how-tos/check_thread_status.md +++ b/docs/docs/cloud/how-tos/check_thread_status.md @@ -13,6 +13,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -23,7 +24,8 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = agent; + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -32,7 +34,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` ## Find idle threads @@ -48,7 +51,7 @@ We can use the following commands to find threads that are idle, which means tha === "Javascript" ```js - console.log(await client.threads.search({status: "idle",limit:1})); + console.log(await client.threads.search({ status: "idle", limit: 1 })); ``` === "CURL" @@ -83,7 +86,7 @@ We can use the following commands to find threads that have been interrupted in === "Javascript" ```js - console.log(await client.threads.search({status: "interrupted",limit:1})); + console.log(await client.threads.search({ status: "interrupted", limit: 1 })); ``` === "CURL" @@ -117,7 +120,7 @@ We can use the following commands to find threads that are busy, meaning they ar === "Javascript" ```js - console.log(await client.threads.search({status: "busy",limit: 1})); + console.log(await client.threads.search({ status: "busy", limit: 1 })); ``` === "CURL" @@ -183,7 +186,7 @@ The search endpoint for threads also allows you to filter on metadata, which can === "Javascript" ```js - console.log((await client.threads.search({metadata: {"foo":"bar"},limit: 1}))[0].status); + console.log((await client.threads.search({ metadata: { "foo": "bar" }, limit: 1 }))[0].status); ``` === "CURL" diff --git a/docs/docs/cloud/how-tos/configuration_cloud.md b/docs/docs/cloud/how-tos/configuration_cloud.md new file mode 100644 index 000000000..becdf802d --- /dev/null +++ b/docs/docs/cloud/how-tos/configuration_cloud.md @@ -0,0 +1,264 @@ +# How to create agents with configuration + +One of the benefits of LangGraph API is that it lets you create agents with different configurations. +This is useful when you want to: + +- Define a cognitive architecture once as a LangGraph +- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use) +- Let users create agents with arbitrary configurations, save them, and then use them in the future + +In this guide we will show how to do that for the default agent we have built in. + +If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like: + +=== "Python" + + ```python + def call_model(state, config): + messages = state["messages"] + model_name = config.get('configurable', {}).get("model_name", "anthropic") + model = _get_model(model_name) + response = model.invoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + ``` + +=== "Javascript" + + ```js + function callModel(state: State, config: RunnableConfig) { + const messages = state.messages; + const modelName = config.configurable?.model_name ?? "anthropic"; + const model = _getModel(modelName); + const response = model.invoke(messages); + // We return a list, because this will get added to the existing list + return { messages: [response] }; + } + ``` + +We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found). That means that by default we are using Anthropic as our model provider. In this example we will see an example of how to create an example agent that is configured to use OpenAI. + +First let's set up our client and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Select an assistant that is not configured + assistants = await client.assistants.search() + assistant = [a for a in assistants if not a["config"]][0] + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Select an assistant that is not configured + const assistants = await client.assistants.search(); + const assistant = assistants.find(a => !a.config); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' + ``` + +We can now call `.get_schemas` to get schemas associated with this graph: + +=== "Python" + + ```python + schemas = await client.assistants.get_schemas( + assistant_id=assistant["assistant_id"] + ) + # There are multiple types of schemas + # We can get the `config_schema` to look at the the configurable parameters + print(schemas["config_schema"]) + ``` + +=== "Javascript" + + ```js + const schemas = await client.assistants.getSchemas( + assistant["assistant_id"] + ); + // There are multiple types of schemas + // We can get the `config_schema` to look at the the configurable parameters + console.log(schemas.config_schema); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants//schemas | jq -r '.config_schema' + ``` + +Output: + + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } + +Now we can initialize an assistant with config: + +=== "Python" + + ```python + openai_assistant = await client.assistants.create( + # "agent" is the name of a graph we deployed + "agent", config={"configurable": {"model_name": "openai"}} + ) + + print(openai_assistant) + ``` + +=== "Javascript" + + ```js + let openAIAssistant = await client.assistants.create( + // "agent" is the name of a graph we deployed + "agent", { "configurable": { "model_name": "openai" } } + ); + + console.log(openAIAssistant); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants \ + --header 'Content-Type: application/json' \ + --data '{"graph_id":"agent","config":{"configurable":{"model_name":"open_ai"}}}' + ``` + +Output: + + { + "assistant_id": "62e209ca-9154-432a-b9e9-2d75c7a9219b", + "graph_id": "agent", + "created_at": "2024-08-31T03:09:10.230718+00:00", + "updated_at": "2024-08-31T03:09:10.230718+00:00", + "config": { + "configurable": { + "model_name": "open_ai" + } + }, + "metadata": {} + } + +We can verify the config is indeed taking effect: + +=== "Python" + + ```python + thread = await client.threads.create() + input = {"messages": [{"role": "user", "content": "who made you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + openai_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + const thread = await client.threads.create(); + let input = { "messages": [{ "role": "user", "content": "who made you?" }] }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + openAIAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + thread_id=$(curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' | jq -r '.thread_id') && \ + curl --request POST \ + --url "/threads/${thread_id}/runs/stream" \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "who made you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef6746e-5893-67b1-978a-0f1cd4060e16'} + + + + Receiving event of type: updates + {'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-e1a6b25c-8416-41f2-9981-f9cfe043f414', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + + + diff --git a/docs/docs/cloud/how-tos/copy_threads.md b/docs/docs/cloud/how-tos/copy_threads.md index 9a27282b2..22586ed4d 100644 --- a/docs/docs/cloud/how-tos/copy_threads.md +++ b/docs/docs/cloud/how-tos/copy_threads.md @@ -24,8 +24,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```js import { Client } from "@langchain/langgraph-sdk"; - const client = new Client({ apiUrl:"" }); - const assistantId = agent; + const client = new Client({ apiUrl: "" }); + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -92,21 +92,21 @@ We can verify that the history from the prior thread did indeed copy over correc ```js function removeThreadId(d) { - if (d.metadata && d.metadata.thread_id) { - delete d.metadata.thread_id; - } - return d; + if (d.metadata && d.metadata.thread_id) { + delete d.metadata.thread_id; + } + return d; } // Assuming `client.threads.getHistory(threadId)` is an async function that returns a list of dicts async function compareThreadHistories(threadId, copiedThreadId) { - const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId); - const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId); + const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId); + const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId); - // Compare the two histories - console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory)) - // if we made it here the assertion passed! - console.log("The histories are the same."); + // Compare the two histories + console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory)); + // if we made it here the assertion passed! + console.log("The histories are the same."); } // Example usage diff --git a/docs/docs/cloud/how-tos/cron_jobs.md b/docs/docs/cloud/how-tos/cron_jobs.md new file mode 100644 index 000000000..f787c8b9c --- /dev/null +++ b/docs/docs/cloud/how-tos/cron_jobs.md @@ -0,0 +1,184 @@ +# Cron Jobs + +Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph. + +## Setup + +First, let's setup our SDK client, assistant, and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantId = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +Output: + + { + 'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217', + 'created_at': '2024-08-30T23:07:38.242730+00:00', + 'updated_at': '2024-08-30T23:07:38.242730+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +## Cron job on a thread + +To create a cron job associated with a specific thread, you can write: + + +=== "Python" + + ```python + # This schedules a job to run at 15:27 (3:27PM) every day + cron_job = await client.crons.create_for_thread( + thread["thread_id"], + assistant_id, + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "What time is it?"}]}, + ) + ``` + +=== "Javascript" + + ```js + // This schedules a job to run at 15:27 (3:27PM) every day + const cronJob = await client.crons.create_for_thread( + thread["thread_id"], + assistantId, + { + schedule: "27 15 * * *", + input: { messages: [{ role: "user", content: "What time is it?" }] } + } + ); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/crons \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code: + +=== "Python" + + ```python + await client.crons.delete(cron_job["cron_id"]) + ``` + +=== "Javascript" + + ```js + await client.crons.delete(cronJob["cron_id"]); + ``` + +=== "CURL" + + ```bash + curl --request DELETE \ + --url /runs/crons/ + ``` + +## Cron job stateless + +You can also create stateless cron jobs by using the following code: + +=== "Python" + + ```python + # This schedules a job to run at 15:27 (3:27PM) every day + cron_job_stateless = await client.crons.create( + assistant_id, + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "What time is it?"}]}, + ) + ``` + +=== "Javascript" + + ```js + // This schedules a job to run at 15:27 (3:27PM) every day + const cronJobStateless = await client.crons.create( + assistantId, + { + schedule: "27 15 * * *", + input: { messages: [{ role: "user", content: "What time is it?" }] } + } + ); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /runs/crons \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Again, remember to delete your job once you are done with it! + +=== "Python" + + ```python + await client.crons.delete(cron_job_stateless["cron_id"]) + ``` + +=== "Javascript" + + ```js + await client.crons.delete(cronJobStateless["cron_id"]); + ``` + +=== "CURL" + + ```bash + curl --request DELETE \ + --url /runs/crons/ + ``` diff --git a/docs/docs/cloud/how-tos/enqueue_concurrent.md b/docs/docs/cloud/how-tos/enqueue_concurrent.md index 17c4ea0f5..06864e876 100644 --- a/docs/docs/cloud/how-tos/enqueue_concurrent.md +++ b/docs/docs/cloud/how-tos/enqueue_concurrent.md @@ -5,20 +5,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `enqueue` option for double texting, which adds the interruptions to a queue and executes them in the order they are received by the client. Below is a quick example of using the `enqueue` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Then, let's import our required packages and instantiate our client, assistant, and thread. @@ -32,6 +56,7 @@ Then, let's import our required packages and instantiate our client, assistant, from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -43,9 +68,19 @@ Then, let's import our required packages and instantiate our client, assistant, const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` Now let's start two runs, with the second interrupting the first one with a multitask strategy of "enqueue": @@ -82,6 +117,25 @@ Now let's start two runs, with the second interrupting the first one with a mult ) ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"enqueue\" + }" + ``` + Verify that the thread has data from both runs: === "Python" @@ -108,12 +162,25 @@ Verify that the thread has data from both runs: } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//runs//join && \ + curl --request GET --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01Dez1sJre4oA2Y7NsKJV6VT', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -121,11 +188,11 @@ Output: Call ID: toolu_01Dez1sJre4oA2Y7NsKJV6VT Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629", "content": "Get the current and future weather conditions for San Francisco, CA, including temperature, precipitation, wind, air quality and more. See the hourly and 10-day outlook, radar maps, alerts and allergy information."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to AccuWeather, the current weather conditions in San Francisco are: @@ -145,10 +212,10 @@ Output: Sunday: Partly sunny, high of 61°F (16°C) So in summary, expect seasonable spring weather in San Francisco over the next several days, with a mix of sun and clouds and temperatures ranging from the upper 40s at night to the low 60s during the days. Typical dry conditions with no rain in the forecast. - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'text': 'Here are the current weather conditions and forecast for New York City:', 'type': 'text'}, {'id': 'toolu_01FFft5Sx9oS6AdVJuRWWcGp', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -156,11 +223,11 @@ Output: Call ID: toolu_01FFft5Sx9oS6AdVJuRWWcGp Args: query: weather in new york city - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to the weather data from WeatherAPI: diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md b/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md index 56c1931e2..120f55998 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md @@ -22,6 +22,7 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -32,7 +33,8 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -41,7 +43,8 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` ## Adding a breakpoint @@ -73,7 +76,7 @@ And, now let's compile it with a breakpoint before the tool node: === "Javascript" ```js - const input = { "messages": [{ "role": "human", "content": "what's the weather in sf"}] } + const input = { messages: [{ role: "human", content: "what's the weather in sf" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -81,9 +84,10 @@ And, now let's compile it with a breakpoint before the tool node: { input: input, streamMode: "updates", - interruptBefore: ["action"], + interruptBefore: ["action"] } ); + for await (const chunk of streamResponse) { console.log(`Receiving new event of type: ${chunk.event}...`); console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md b/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md index 668d826a8..e4a2ec3c9 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md @@ -18,6 +18,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -28,6 +29,7 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -37,7 +39,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` ## Editing state @@ -65,7 +68,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node. === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "search for weather in SF"}] } + const input = { messages: [{ role: "human", content: "search for weather in SF" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -76,6 +79,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node. interruptBefore: ["action"], } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -154,15 +158,15 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot === "Javascript" ```js - // First, lets get the current state - const currentState = await client.threads.getState(thread['thread_id']); + // First, let's get the current state + const currentState = await client.threads.getState(thread["thread_id"]); // Let's now get the last message in the state // This is the one with the tool calls that we want to update - let lastMessage = currentState['values']['messages'][-1]; + let lastMessage = currentState.values.messages.slice(-1)[0]; // Let's now update the args for that tool call - lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'}; + lastMessage.tool_calls[0].args = { query: "current weather in Sidi Frej" }; // Let's now call `update_state` to pass in this message in the `messages` key // This will get treated as any other update to the state @@ -170,7 +174,7 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot // That reducer function will use the ID of the message to update it // It's important that it has the right ID! Otherwise it would get appended // as a new message - await client.threads.updateState(thread['thread_id'], {values:{"messages": lastMessage}}); + await client.threads.updateState(thread["thread_id"], { values: { messages: lastMessage } }); ``` === "CURL" @@ -220,6 +224,7 @@ Now we can resume our graph run but with the updated state: streamMode: "updates", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md b/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md index e9a698181..7ea17a07d 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md @@ -29,6 +29,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -39,10 +40,20 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + ## Example with no review Let's look at an example when no review is required (because no tools are called) @@ -66,7 +77,7 @@ Let's look at an example when no review is required (because no tools are called === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "hi!"}] } + const input = { "messages": [{ "role": "human", "content": "hi!" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -77,6 +88,7 @@ Let's look at an example when no review is required (because no tools are called interruptBefore: ["action"], } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -84,6 +96,42 @@ Let's look at an example when no review is required (because no tools are called } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"hi!\"}]}, + \"stream_mode\": [ + \"updates\" + ], + \"interrupt_before\": [\"action\"] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]} @@ -108,6 +156,13 @@ If we check the state, we can see that it is finished console.log(state.next); ``` +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -c '.next' + ``` + Output: [] @@ -123,9 +178,8 @@ Let's now look at what it looks like to approve a tool call. Note that we don't async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -134,16 +188,16 @@ Let's now look at what it looks like to approve a tool call. Note that we don't === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: input, - streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -151,6 +205,38 @@ Let's now look at what it looks like to approve a tool call. Note that we don't } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]} @@ -175,6 +261,13 @@ If we now check, we can see that it is waiting on human review: console.log(state.next); ``` +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -c '.next' + ``` + Output: ['human_review_node'] @@ -186,7 +279,7 @@ To approve the tool call, we can just continue the thread with no edits. To do t ```python async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=None, stream_mode="values", ): @@ -201,10 +294,11 @@ To approve the tool call, we can just continue the thread with no edits. To do t thread["thread_id"], assistantId, { - input: undefined, + input: null, streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -212,6 +306,37 @@ To approve the tool call, we can just continue the thread with no edits. To do t } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\" + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}]} @@ -228,7 +353,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, stream_mode="values", ): @@ -239,7 +364,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -249,6 +374,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -256,6 +382,38 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]} @@ -308,9 +466,8 @@ To do this, we first need to update the state. We can do this by passing a messa # Let's now continue executing from here async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=None, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -332,43 +489,93 @@ To do this, we first need to update the state. We can do this by passing a messa // Construct a replacement tool call const newMessage = { - role: "assistant", - content: currentContent, - tool_calls: [ - { - id: toolCallId, - name: "weather_search", - args: { city: "San Francisco, USA" } - } - ], - // Ensure the ID is the same as the message you're replacing - id: currentId + role: "assistant", + content: currentContent, + tool_calls: [ + { + id: toolCallId, + name: "weather_search", + args: { city: "San Francisco, USA" } + } + ], + // Ensure the ID is the same as the message you're replacing + id: currentId }; await client.threads.updateState( - thread.thread_id, // Thread ID - { + thread.thread_id, // Thread ID + { values: { "messages": [newMessage] }, // Updated message asNode: "human_review_node" - } // Acting as human_review_node + } // Acting as human_review_node ); console.log("\nResuming Execution"); // Continue executing from here const streamResponseResumed = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: undefined, - streamMode: "values", - interruptBefore: ["action"], - } + thread["thread_id"], + assistantId, + { + input: null, + } ); + for await (const chunk of streamResponseResumed) { - if (chunk.data && chunk.event !== "metadata") { + if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); + } } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data "{ + \"values\": { \"messages\": [$(curl --request GET \ + --url /threads//state | + jq -c '{ + role: "assistant", + content: .values.messages[-1].content, + tool_calls: [ + { + id: .values.messages[-1].tool_calls[0].id, + name: "weather_search", + args: { city: "San Francisco, USA" } + } + ], + id: .values.messages[-1].id + }') + ]}, + \"as_node\": \"human_review_node\" + }" && echo "Resuming Execution" && curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": "agent" + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: @@ -402,9 +609,8 @@ For this example we will just add a single tool call representing the feedback. async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -413,16 +619,16 @@ For this example we will just add a single tool call representing the feedback. === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: input, - streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -430,6 +636,38 @@ For this example we will just add a single tool call representing the feedback. } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]} @@ -471,7 +709,7 @@ To do this, we first need to update the state. We can do this by passing a messa # Let's now continue executing from here async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=None, stream_mode="values", ): @@ -493,38 +731,85 @@ To do this, we first need to update the state. We can do this by passing a messa // Construct a replacement tool call const newMessage = { - role: "tool", - content: "User requested changes: pass in the country as well", - name: "weather_search", - tool_call_id: toolCallId, + role: "tool", + content: "User requested changes: pass in the country as well", + name: "weather_search", + tool_call_id: toolCallId, }; await client.threads.updateState( - thread.thread_id, // Thread ID - { + thread.thread_id, // Thread ID + { values: { "messages": [newMessage] }, // Updated message asNode: "human_review_node" - } // Acting as human_review_node + } // Acting as human_review_node ); console.log("\nResuming Execution"); // Continue executing from here const streamResponseEdited = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: undefined, + thread["thread_id"], + assistantId, + { + input: null, streamMode: "values", interruptBefore: ["action"], - } + } ); + for await (const chunk of streamResponseEdited) { - if (chunk.data && chunk.event !== "metadata") { + if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); - } + } } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data "{ + \"values\": { \"messages\": [$(curl --request GET \ + --url /threads//state | + jq -c '{ + role: "tool", + content: "User requested changes: pass in the country as well", + name: "get_weather", + tool_call_id: .values.messages[-1].id.tool_calls[0].id + }') + ]}, + \"as_node\": \"human_review_node\" + }" && echo "Resuming Execution" && curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": "agent" + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + + Output: Current State: @@ -543,9 +828,8 @@ We can see that we now get to another breakpoint - because it went back to the m ```python async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=None, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -558,10 +842,10 @@ We can see that we now get to another breakpoint - because it went back to the m thread["thread_id"], assistantId, { - input: undefined, - streamMode: "values", + input: null, } ); + for await (const chunk of streamResponseResumed) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -569,6 +853,37 @@ We can see that we now get to another breakpoint - because it went back to the m } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\" + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}]} diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md index aac3ee566..d24b5d060 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md @@ -15,6 +15,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -25,7 +26,8 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = agent; + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -34,7 +36,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` ## Replay a state @@ -46,11 +49,11 @@ Before replaying a state - we need to create states to replay from! In order to === "Python" ```python - input = { 'messages':[{ "role":"user", "content":"Please search the weather in SF" }] } + input = {"messages": [{"role": "user", "content": "Please search the weather in SF"}]} async for chunk in client.runs.stream( thread["thread_id"], - assistant_id, # graph_id + assistant_id, input=input, stream_mode="updates", ): @@ -61,7 +64,7 @@ Before replaying a state - we need to create states to replay from! In order to === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "Please search the weather in SF"}] } + const input = { "messages": [{ "role": "human", "content": "Please search the weather in SF" }] } const streamResponse = client.runs.stream( thread["thread_id"], @@ -155,17 +158,23 @@ Output: -To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows: +To rerun from a state, we need first issue an empty update to the thread state. Then we need to pass in the resulting `checkpoint_id` as follows: === "Python" ```python + state_to_replay = states[2] + updated_config = await client.threads.update_state( + thread["thread_id"], + {"messages": []}, + checkpoint_id=state_to_replay["checkpoint_id"] + ) async for chunk in client.runs.stream( thread["thread_id"], assistant_id, # graph_id input=None, stream_mode="updates", - config={"configurable": {"checkpoint_id": state_to_replay['checkpoint_id']}} + checkpoint_id=updated_config["checkpoint_id"] ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -174,13 +183,15 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of === "Javascript" ```js + const stateToReplay = states[2]; + const config = await client.threads.updateState(thread["thread_id"], { values: {"messages": [] }, checkpointId: stateToReplay["checkpoint_id"] }); const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: null, streamMode: "updates", - config: {"configurable": {"checkpoint_id": stateToReplay['checkpoint_id']}}, + checkpointId: config["checkpoint_id"] } ); for await (const chunk of streamResponse) { @@ -193,38 +204,46 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of === "CURL" ```bash - curl --request GET --url /threads//history | jq -r '.[2].checkpoint_id' | { - read checkpoint_id - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"config\": {\"configurable\": {\"checkpoint_id\": \"$checkpoint_id\"}}, - \"stream_mode\": [ - \"updates\" - ] - }" | \ - sed 's/\r$//' | \ - awk ' - /^event:/ { - if (data_content != "" && event_type != "metadata") { - print data_content "\n" - } - sub(/^event: /, "", $0) - event_type = $0 - data_content = "" - } - /^data:/ { - sub(/^data: /, "", $0) - data_content = $0 - } - END { - if (data_content != "" && event_type != "metadata") { - print data_content "\n" - } + curl --request GET --url /threads//history | jq -c ' + .[2] as $state_to_replay | + { + values: { messages: .[2].values.messages[-1] }, + checkpoint_id: $state_to_replay.checkpoint_id + }' | \ + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data @- | jq .checkpoint_id | \ + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"checkpoint_id\": \"$1\", + \"stream_mode\": [ + \"updates\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" } - ' + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: @@ -251,7 +270,7 @@ Let's show how to do this to edit the state at a particular point in time. Let's # Let's now update the args for that tool call last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'} - new_state = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id']) + config = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id']) ``` === "Javascript" @@ -262,9 +281,9 @@ Let's show how to do this to edit the state at a particular point in time. Let's let lastMessage = stateToReplay['values']['messages'][-1]; // Let's now update the args for that tool call - lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in SF'}; + lastMessage['tool_calls'][0]['args'] = { 'query': 'current weather in SF' }; - const newState = await client.threads.updateState(thread['thread_id'],{values:{"messages":[lastMessage]},checkpointId:stateToReplay['checkpoint_id']}); + const config = await client.threads.updateState(thread['thread_id'], { values: { "messages": [lastMessage] }, checkpointId: stateToReplay['checkpoint_id'] }); ``` === "CURL" @@ -291,10 +310,10 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```python async for chunk in client.runs.stream( thread["thread_id"], - assistant["assistant_id"], # graph_id + assistant_id, input=None, stream_mode="updates", - config={"configurable": {"checkpoint_id": new_state['configurable']['checkpoint_id']}} + checkpoint_id=config['checkpoint_id'] ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -305,11 +324,11 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```js const streamResponse = client.runs.stream( thread["thread_id"], - assistant["assistant_id"], + assistantId, { input: null, streamMode: "updates", - config: {"configurable": {"checkpoint_id": newState['configurable']['checkpoint_id']}}, + checkpointId: config['checkpoint_id'], } ); for await (const chunk of streamResponse) { @@ -323,33 +342,37 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```bash curl -s --request GET --url /threads//state | \ - jq -r '.config.configurable.checkpoint_id' | \ - sh -c ' - CHECKPOINT_ID="$1" - curl --request POST \ - --url /threads//runs/stream \ - --header "Content-Type: application/json" \ - --data "{\"assistant_id\": \"agent\", \"config\": {\"configurable\": {\"checkpoint_id\": \"$CHECKPOINT_ID\"}}, \"stream_mode\": [\"updates\"]}" | \ - sed "s/\r$//" | \ - awk " - /^event:/ { - if (data_content != \"\" && event_type != \"metadata\") { - print data_content \"\n\" - } - sub(/^event: /, \"\", \$0) - event_type = \$0 - data_content = \"\" - } - /^data:/ { - sub(/^data: /, \"\", \$0) - data_content = \$0 - } - END { - if (data_content != \"\" && event_type != \"metadata\") { - print data_content \"\n\" - } - }" - ' _ + jq -c '.checkpoint_id' | \ + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"checkpoint_id\": \"$1\", + \"stream_mode\": [ + \"updates\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md b/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md index 9b9d1392e..78f4fddca 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md @@ -25,6 +25,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -35,6 +36,7 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -44,7 +46,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` ## Waiting for user input @@ -56,7 +59,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node: === "Python" ```python - input = { 'messages':[{ "role":"user", "content":"Use the search tool to ask the user where they are, then look up the weather there" }] } + input = { + "messages": [ + { + "role": "human", + "content": "Use the search tool to ask the user where they are, then look up the weather there", + } + ] + } async for chunk in client.runs.stream( thread["thread_id"], @@ -71,7 +81,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node: === "Javascript" ```js - const input = { "messages":[{ "role":"human", "content": "Use the search tool to ask the user where they are, then look up the weather there"}] } + const input = { + messages: [ + { + role: "human", + content: "Use the search tool to ask the user where they are, then look up the weather there" + } + ] + }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -79,9 +96,10 @@ Now, let's invoke our graph by interrupting before `ask_human` node: { input: input, streamMode: "updates", - interruptBefore: ["ask_human"], + interruptBefore: ["ask_human"] } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -152,13 +170,23 @@ Because we are treating this as a tool call, we will need to update the state as === "Javascript" ```js - const state = await client.threads.getState(thread['thread_id']); - const toolCallId = state['values']['messages'][-1]['tool_calls'][0]['id']; + const state = await client.threads.getState(thread["thread_id"]); + const toolCallId = state.values.messages[state.values.messages.length - 1].tool_calls[0].id; - # We now create the tool call with the id and the response we want - const toolMessage = [{"tool_call_id": toolCallId, "type": "tool", "content": "san francisco"}]; + // We now create the tool call with the id and the response we want + const toolMessage = [ + { + tool_call_id: toolCallId, + type: "tool", + content: "san francisco" + } + ]; - await client.threads.updateState(thread['thread_id'], {values: {"messages": toolMessage}, asNode:"ask_human"}) + await client.threads.updateState( + thread["thread_id"], + { values: { messages: toolMessage } }, + { asNode: "ask_human" } + ); ``` === "CURL" @@ -212,9 +240,10 @@ We can now tell the agent to continue. We can just pass in None as the input to assistantId, { input: null, - streamMode: "updates", + streamMode: "updates" } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/index.md b/docs/docs/cloud/how-tos/index.md index 255490d32..5fad748cb 100644 --- a/docs/docs/cloud/how-tos/index.md +++ b/docs/docs/cloud/how-tos/index.md @@ -7,15 +7,21 @@ hide: Welcome to the LangGraph Cloud how-to guides! These guides provide practical, step-by-step instructions for accomplishing key tasks in LangGraph Cloud. -## Deployment +## Setup -LangGraph Cloud gives you best in class observability, testing, and hosting services. Read more about them in these how to guides: +LangGraph Cloud gives you best in class observability, testing, and hosting services. Learn how to setup your app for deployment to LangGraph Cloud in these how-to guides - [How to set up app for deployment (requirements.txt)](../deployment/setup.md) - [How to set up app for deployment (pyproject.toml)](../deployment/setup_pyproject.md) +- [How to set up app for deployment (JavaScript)](../deployment/setup_javascript.md) +- [How to customize Dockerfile](../deployment/custom_docker.md) - [How to test locally](../deployment/test_locally.md) + +## Deploy + +Learn how to deploy your app to LangGraph Cloud in these how to guides: + - [How to deploy to LangGraph cloud](../deployment/cloud.md) -- [How to self-host](../deployment/self_hosted.md) ## Streaming @@ -61,17 +67,18 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a LangGraph Cloud supports multiple types of runs besides streaming runs. -- [How to run an agent in the background](cloud_examples/background_run.ipynb) -- [How to run multiple agents in the same thread](cloud_examples/same-thread.ipynb) -- [How to create cron jobs](cloud_examples/cron_jobs.ipynb) -- [How to create stateless runs](cloud_examples/stateless_runs.ipynb) +- [How to run an agent in the background](./background_run.md) +- [How to run multiple agents in the same thread](./same-thread.md) +- [How to create cron jobs](./cron_jobs.md) +- [How to create stateless runs](./stateless_runs.md) ## Other Other guides that may prove helpful! -- [How to configure agents](cloud_examples/configuration_cloud.ipynb) +- [How to configure agents](./configuration_cloud.md) - [How to convert LangGraph calls to LangGraph cloud calls](cloud_examples/langgraph_to_langgraph_cloud.ipynb) -- [How to integrate webhooks](cloud_examples/webhooks.ipynb) +- [How to integrate webhooks](./webhooks.md) - [How to copy threads](./copy_threads.md) - [How to check status of your threads](./check_thread_status.md) +- [How to share state between threads](./shared_state.md) \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/interrupt_concurrent.md b/docs/docs/cloud/how-tos/interrupt_concurrent.md index d58df875b..cbf789306 100644 --- a/docs/docs/cloud/how-tos/interrupt_concurrent.md +++ b/docs/docs/cloud/how-tos/interrupt_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `interrupt` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option does not delete the first run, but rather keeps it in the database but sets its status to `interrupted`. Below is a quick example of using the `interrupt` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -30,6 +54,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -40,10 +65,20 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + Now we can start our two runs and join the second on euntil it has completed: === "Python" @@ -60,7 +95,7 @@ Now we can start our two runs and join the second on euntil it has completed: thread["thread_id"], assistant_id, input={"messages": [{"role": "human", "content": "what's the weather in nyc?"}]}, - multitask_strategychrom="interrupt", + multitask_strategy="interrupt", ) # wait until the second run completes await client.runs.join(thread["thread_id"], run["run_id"]) @@ -90,6 +125,26 @@ Now we can start our two runs and join the second on euntil it has completed: await client.runs.join(thread["thread_id"], run["run_id"]); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && sleep 2 && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"interrupt\" + }" && curl --request GET \ + --url /threads//runs//join + ``` + We can see that the thread has partial data from the first run + data from the second run @@ -112,12 +167,24 @@ We can see that the thread has partial data from the first run + data from the s } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01MjNtVJwEcpujRGrf3x6Pih', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -125,14 +192,14 @@ Output: Call ID: toolu_01MjNtVJwEcpujRGrf3x6Pih Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.wunderground.com/hourly/us/ca/san-francisco/KCASANFR2002/date/2024-6-18", "content": "High 64F. Winds W at 10 to 20 mph. A few clouds from time to time. Low 49F. Winds W at 10 to 20 mph. Temp. San Francisco Weather Forecasts. Weather Underground provides local & long-range weather ..."}] - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01KtE1m1ifPLQAx4fQLyZL9Q', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -140,11 +207,11 @@ Output: Call ID: toolu_01KtE1m1ifPLQAx4fQLyZL9Q Args: query: weather in new york city - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/new-york/10021/june-weather/349727", "content": "Get the monthly weather forecast for New York, NY, including daily high/low, historical averages, to help you plan ahead."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== The search results provide weather forecasts and information for New York City. Based on the top result from AccuWeather, here are some key details about the weather in NYC: diff --git a/docs/docs/cloud/how-tos/reject_concurrent.md b/docs/docs/cloud/how-tos/reject_concurrent.md index 22d0ae30c..8e302ce26 100644 --- a/docs/docs/cloud/how-tos/reject_concurrent.md +++ b/docs/docs/cloud/how-tos/reject_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `reject` option for double texting, which rejects the new run of the graph by throwing an error and continues with the original run until completion. Below is a quick example of using the `reject` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -29,6 +53,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -39,10 +64,20 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + Now we can run a thread and try to run a second one with the "reject" option, which should fail since we have already started a run: @@ -90,6 +125,27 @@ Now we can run a thread and try to run a second one with the "reject" option, wh } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"reject\" + }" || { echo "Failed to start concurrent run"; echo "Error: $?" >&2; } + ``` + +Output: + Failed to start concurrent run Client error '409 Conflict' for url 'http://localhost:8123/threads/f9e7088b-8028-4e5c-88d2-9cc9a2870e50/runs' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 @@ -120,12 +176,25 @@ We can verify that the original thread finished executing: } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//runs//join && \ + curl --request GET --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01CyewEifV2Kmi7EFKHbMDr1', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -133,11 +202,11 @@ Output: Call ID: toolu_01CyewEifV2Kmi7EFKHbMDr1 Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/san-francisco/94103/june-weather/347629", "content": "Get the monthly weather forecast for San Francisco, CA, including daily high/low, historical averages, to help you plan ahead."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to the search results from Tavily, the current weather in San Francisco is: diff --git a/docs/docs/cloud/how-tos/rollback_concurrent.md b/docs/docs/cloud/how-tos/rollback_concurrent.md index 6fa34eb95..1bb31b2a9 100644 --- a/docs/docs/cloud/how-tos/rollback_concurrent.md +++ b/docs/docs/cloud/how-tos/rollback_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `rollback` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option is very similar to the `interrupt` option, but in this case the first run is completely deleted from the database and cannot be restarted. Below is a quick example of using the `rollback` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -31,6 +55,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -41,10 +66,20 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + Now let's run a thread with the multitask parameter set to "rollback": === "Python" @@ -91,6 +126,26 @@ Now let's run a thread with the multitask parameter set to "rollback": await client.runs.join(thread["thread_id"], run["run_id"]); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && sleep 2 && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"rollback\" + }" && curl --request GET \ + --url /threads//runs//join + ``` + We can see that the thread has data only from the second run === "Python" @@ -112,12 +167,24 @@ We can see that the thread has data only from the second run } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01JzPqefao1gxwajHQ3Yh3JD', 'input': {'query': 'weather in nyc'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -125,11 +192,11 @@ Output: Call ID: toolu_01JzPqefao1gxwajHQ3Yh3JD Args: query: weather in nyc - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== The weather API results show that the current weather in New York City is sunny with a temperature of around 85°F (29°C). The wind is light at around 2-3 mph from the south-southeast. Overall it looks like a nice sunny summer day in NYC. diff --git a/docs/docs/cloud/how-tos/same-thread.md b/docs/docs/cloud/how-tos/same-thread.md new file mode 100644 index 000000000..f48e72b6f --- /dev/null +++ b/docs/docs/cloud/how-tos/same-thread.md @@ -0,0 +1,310 @@ +# How to run multiple agents on the same thread + +In LangGraph Cloud, a thread is not explicitly associated with a particular agent. +This means that you can run multiple agents on the same thread, which allows a different agent to continue from an initial agent's progress. + +In this example, we will create two agents and then call them both on the same thread. +You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread by the first agent as context. + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + + openai_assistant = await client.assistants.create( + graph_id="agent", config={"configurable": {"model_name": "openai"}} + ) + + # There should always be a default assistant with no configuration + assistants = await client.assistants.search() + default_assistant = [a for a in assistants if not a["config"]][0] + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + + const openAIAssistant = await client.assistants.create( + { graphId: "agent", config: {"configurable": {"model_name": "openai"}}} + ); + + const assistants = await client.assistants.search(); + const defaultAssistant = assistants.find(a => !a.config); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants \ + --header 'Content-Type: application/json' \ + --data '{ + "graph_id": "agent", + "config": { "configurable": { "model_name": "openai" } } + }' && \ + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' + ``` + +We can see that these agents are different: + +=== "Python" + + ```python + print(openai_assistant) + ``` + +=== "Javascript" + + ```js + console.log(openAIAssistant); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants/ + ``` + +Output: + + { + "assistant_id": "db87f39d-b2b1-4da8-ac65-cf81beb3c766", + "graph_id": "agent", + "created_at": "2024-08-30T21:18:51.850581+00:00", + "updated_at": "2024-08-30T21:18:51.850581+00:00", + "config": { + "configurable": { + "model_name": "openai" + } + }, + "metadata": {} + } + +=== "Python" + + ```python + print(default_assistant) + ``` + +=== "Javascript" + + ```js + console.log(defaultAssistant); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants/ + ``` + +Output: + + { + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "graph_id": "agent", + "created_at": "2024-08-08T22:45:24.562906+00:00", + "updated_at": "2024-08-08T22:45:24.562906+00:00", + "config": {}, + "metadata": { + "created_by": "system" + } + } + +We can now run the OpenAI assistant on the thread first. + +=== "Python" + + ```python + thread = await client.threads.create() + input = {"messages": [{"role": "user", "content": "who made you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + openai_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + const thread = await client.threads.create(); + let input = {"messages": [{"role": "user", "content": "who made you?"}]} + + const streamResponse = client.runs.stream( + thread["thread_id"], + openAIAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + thread_id=$(curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' | jq -r '.thread_id') && \ + curl --request POST \ + --url "/threads/${thread_id}/runs/stream" \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "who made you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef671c5-fb83-6e70-b698-44dba2d9213e'} + + + Receiving event of type: updates + {'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-f5735b86-b80d-4c71-8dc3-4782b5a9c7c8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + +Now, we can run it on the default assistant and see that this second assistant is aware of the initial question, and can answer the question, "and you?": + +=== "Python" + + ```python + input = {"messages": [{"role": "user", "content": "and you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + default_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + let input = {"messages": [{"role": "user", "content": "and you?"}]} + + const streamResponse = client.runs.stream( + thread["thread_id"], + defaultAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "and you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef6722d-80b3-6fbb-9324-253796b1cd13'} + + + Receiving event of type: updates + {'agent': {'messages': [{'content': [{'text': 'I am an artificial intelligence created by Anthropic, not by OpenAI. I should not have stated that OpenAI created me, as that is incorrect. Anthropic is the company that developed and trained me using advanced language models and AI technology. I will be more careful about providing accurate information regarding my origins in the future.', 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ebaacf62-9dd9-4165-9535-db432e4793ec', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 302, 'output_tokens': 72, 'total_tokens': 374}}]}} + + + diff --git a/docs/docs/cloud/how-tos/shared_state.md b/docs/docs/cloud/how-tos/shared_state.md new file mode 100644 index 000000000..77b1b3df9 --- /dev/null +++ b/docs/docs/cloud/how-tos/shared_state.md @@ -0,0 +1,492 @@ +# How to share state between threads + +By default, state in a graph is scoped to a specific thread. LangGraph also allows you to specify a "scope" for a given key/value pair that exists between threads. This can be useful for storing information that is shared between threads. For instance, you may want to store information about a user's preferences expressed in one thread, and then use that information in another thread. + +In this notebook we will go through an example of how to use a graph that has been deployed with shared state. + +## Setup + +First, make sure that you have a deployed graph that has a shared state key. Your state definition should look something like this (support for shared state channels in JS is coming soon!): + +```python +class AgentState(TypedDict): + # This is scoped to a user_id, so it will be information specific to each user + info: Annotated[dict, SharedValue.on("user_id")] + # ... other state keys ... +``` +!!! note "Typing shared state keys" + Shared state channels (keys) MUST be dictionaries (see `info` channel in the AgentState example above) + +Now we can setup our client and an initial thread to run the graph on: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent"; + # create thread + thread = await client.threads.create() + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; + // create thread + let thread = await client.threads.create(); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +## Usage + +Now, let's run the graph on the first thread, and provide it some information about the users preferences: + +=== "Python" + + ```python + input = {"messages": [{"role": "human", "content": "i like pepperoni pizza"}]} + config = {"configurable": {"user_id": "123"}} + # stream values + async for chunk in client.runs.stream( + thread["thread_id"], + assistant_id, + input=input, + config=config, + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + // create input + let input = { + messages: [ + { + role: "human", + content: "i like pepperoni pizza", + } + ] + }; + let config = { configurable: { user_id: "123" } }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input, + config + } + ); + for await (const chunk of streamResponse) { + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"i like pepperoni pizza\"}]}, + \"config\":{\"configurable\":{\"user_id\":\"123\"}} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + +Output: + + Receiving new event of type: metadata... + {'run_id': '1ef6bdb2-ba0e-6177-84a9-c574772223b3'} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'function': {'arguments': '{"fact":"Isaac likes pepperoni pizza","topic":"Food"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-f086646f-cb38-4419-9a92-fc7cb19340ee', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'function': {'arguments': '{"fact":"Isaac likes pepperoni pizza","topic":"Food"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-f086646f-cb38-4419-9a92-fc7cb19340ee', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Saved!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': 'bed77d11-916c-4bad-b8f8-f0c850a8e494', 'tool_call_id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'artifact': None, 'status': 'success'}]} + + + +Let's stay on the same thread and provide some additional information. Note that we are not redefining the config since we want to continue the conversation on the same thread with the same user. + +=== "Python" + + ```python + input = {"messages": [{"role": "human", "content": "i also just moved to SF"}]} + # stream values + async for chunk in client.runs.stream( + thread["thread_id"], + assistant_id, # the graph name + input=input, + config=config, + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + input = { + messages: [ + { + role: "human", + content: "i also just moved to SF", + } + ] + }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input, + config + } + ); + + for await (const chunk of streamResponse) { + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"i also just moved to SF\"}]}, + \"config\":{\"configurable\":{\"user_id\":\"123\"}} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + +Output: + + Receiving new event of type: metadata... + {'run_id': '1ef6bdb2-f068-60b6-93a6-b2e2f02f117d'} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'function': {'arguments': '{"fact":"Isaac likes pepperoni pizza","topic":"Food"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-f086646f-cb38-4419-9a92-fc7cb19340ee', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Saved!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': 'bed77d11-916c-4bad-b8f8-f0c850a8e494', 'tool_call_id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'artifact': None, 'status': 'success'}, {'content': 'i also just moved to SF', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1e98940-d9fc-454c-bb65-0036e2c048c6', 'example': False}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'function': {'arguments': '{"fact":"Isaac likes pepperoni pizza","topic":"Food"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-f086646f-cb38-4419-9a92-fc7cb19340ee', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Saved!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': 'bed77d11-916c-4bad-b8f8-f0c850a8e494', 'tool_call_id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'artifact': None, 'status': 'success'}, {'content': 'i also just moved to SF', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1e98940-d9fc-454c-bb65-0036e2c048c6', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_yPNnY10h9KszyuVf5p6H2c1E', 'function': {'arguments': '{"fact":"Isaac just moved to SF","topic":"Location"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-d247f67f-ba1a-4ce7-84c2-0f30180d10c6', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac just moved to SF', 'topic': 'Location'}, 'id': 'call_yPNnY10h9KszyuVf5p6H2c1E', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'i like pepperoni pizza', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1244b8b-e54e-4ebe-ada4-63aadf4a7701', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'function': {'arguments': '{"fact":"Isaac likes pepperoni pizza","topic":"Food"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-f086646f-cb38-4419-9a92-fc7cb19340ee', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac likes pepperoni pizza', 'topic': 'Food'}, 'id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Saved!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': 'bed77d11-916c-4bad-b8f8-f0c850a8e494', 'tool_call_id': 'call_ujnk8CIx0xeguFHHe8P0ecgm', 'artifact': None, 'status': 'success'}, {'content': 'i also just moved to SF', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'f1e98940-d9fc-454c-bb65-0036e2c048c6', 'example': False}, {'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_yPNnY10h9KszyuVf5p6H2c1E', 'function': {'arguments': '{"fact":"Isaac just moved to SF","topic":"Location"}', 'name': 'Info'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-d247f67f-ba1a-4ce7-84c2-0f30180d10c6', 'example': False, 'tool_calls': [{'name': 'Info', 'args': {'fact': 'Isaac just moved to SF', 'topic': 'Location'}, 'id': 'call_yPNnY10h9KszyuVf5p6H2c1E', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Saved!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '7c4b82b0-5ee5-4d97-902b-dbec1499fe39', 'tool_call_id': 'call_yPNnY10h9KszyuVf5p6H2c1E', 'artifact': None, 'status': 'success'}]} + + + + + + + +Now, let's run the graph on a completely different thread, and see that it remembered the information we provided it: + + +=== "Python" + + ```python + # new thread for new conversation + thread = await client.threads.create() + input = {"messages": [{"role": "human", "content": "where and what should i eat for dinner? Can you list some restaurants?"}]} + # stream values + async for chunk in client.runs.stream( + thread["thread_id"], + assistant_id, + input=input, + config=config, + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + // new thread for new conversation + thread = await client.threads.create(); + + // create input + let input = { + messages: [ + { + role: "human", + content: "where and what should i eat for dinner? Can you list some restaurants?", + } + ] + }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input, + config + } + ); + for await (const chunk of streamResponse) { + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' \ + | jq -r '.thread_id' \ + | xargs -I {} \ + curl --request POST \ + --url /threads/{}/runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": "agent", + "input": { + "messages": [{ + "role": "human", + "content": "where and what should i eat for dinner? Can you list some restaurants?" + }] + }, + "config": { + "configurable": { + "user_id": "123" + } + } + }' \ + | sed 's/\r$//' \ + | awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + +Output: + + Receiving new event of type: metadata... + {'run_id': '1ef6bde9-d866-623c-8647-a56e33322334'} + + + + Receiving new event of type: values... + {'messages': [{'content': 'where and what should i eat for dinner? Can you list some restaurants?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'aaf07830-ddbf-4ec6-b520-2371490abaa8', 'example': False}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'where and what should i eat for dinner? Can you list some restaurants?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'aaf07830-ddbf-4ec6-b520-2371490abaa8', 'example': False}, {'content': "Sure! Since you just moved to SF, I can suggest some popular restaurants in the area. Here are a few options:\n\n1. Tony's Pizza Napoletana - Known for their delicious pizzas, including pepperoni pizza.\n2. The House - Offers Asian fusion cuisine in a cozy setting.\n3. Tadich Grill - A historic seafood restaurant serving classic dishes.\n4. Swan Oyster Depot - A seafood counter known for its fresh seafood selections.\n5. Zuni Cafe - A popular spot for American and Mediterranean-inspired dishes.\n\nDo any of these options sound good to you? Let me know if you need more recommendations or information about any specific cuisine!", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-dbac2e4c-0e4b-4c4d-b17f-172456222f53', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]} + + + +Perfect! The AI recommended restaurants in SF, and included a pizza restaurant at the top of it's list. + +Let's now run the graph for another user to verify that the preferences of the first user are self contained: + +=== "Python" + + ```python + # new thread for new conversation + thread = await client.threads.create() + # create input + input = {"messages": [{"role": "human", "content": "where do I live? what do I like to eat?"}]} + config = {"configurable": {"user_id": "321"}} + # stream values + async for chunk in client.runs.stream( + thread["thread_id"], + assistant_id, + input=input, + config=config, + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + // new thread for new conversation + thread = await client.threads.create(); + // create input + let input = { + messages: [ + { + role: "human", + content: "where do I live? what do I like to eat?", + } + ] + }; + let config = { configurable: { user_id: "321" } }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input, + config + } + ); + for await (const chunk of streamResponse) { + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' \ + | jq -r '.thread_id' \ + | xargs -I {} \ + curl --request POST \ + --url /threads/{}/runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"where do I live? what do I like to eat?\"}]}, + \"config\":{\"configurable\":{\"user_id\":\"321\"}} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + +Output: + + Receiving new event of type: metadata... + {'run_id': '1ef6bdf3-6aae-63ab-adc4-0a1467251531'} + + + + Receiving new event of type: values... + {'messages': [{'content': 'where do I live? what do I like to eat?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '043fd6a6-b59b-411b-9ec3-f6947260e6d3', 'example': False}]} + + + + Receiving new event of type: values... + {'messages': [{'content': 'where do I live? what do I like to eat?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '043fd6a6-b59b-411b-9ec3-f6947260e6d3', 'example': False}, {'content': "I don't have that information yet. Can you please provide me with details about where you live and what you like to eat?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-3.5-turbo-0125'}, 'type': 'ai', 'name': None, 'id': 'run-4dd3415d-e75b-44d5-9744-14f840e6c696', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]} + +Perfect! The agent does not have access to the first users preferences as we expect! + diff --git a/docs/docs/cloud/how-tos/stateless_runs.md b/docs/docs/cloud/how-tos/stateless_runs.md new file mode 100644 index 000000000..e28363963 --- /dev/null +++ b/docs/docs/cloud/how-tos/stateless_runs.md @@ -0,0 +1,180 @@ +# Stateless Runs + +Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you don't need to persist the runs you don't need to use the built in persistent state and can create stateless runs. + +## Setup + +First, let's setup our client: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantId = "agent"; + // create thread + const thread = await client.threads.create(); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +## Stateless streaming + +We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`: + +=== "Python" + + ```python + input = { + "messages": [ + {"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."} + ] + } + + async for chunk in client.runs.stream( + # Don't pass in a thread_id and the stream will be stateless + None, + assistant_id, + input=input, + stream_mode="updates", + ): + if chunk.data and "run_id" not in chunk.data: + print(chunk.data) + ``` + +=== "Javascript" + + ```js + let input = { + messages: [ + { role: "user", content: "Hello! My name is Bagatur and I am 26 years old." } + ] + }; + + const streamResponse = client.runs.stream( + // Don't pass in a thread_id and the stream will be stateless + null, + assistantId, + { + input, + streamMode: "updates" + } + ); + for await (const chunk of streamResponse) { + if (chunk.data && !("run_id" in chunk.data)) { + console.log(chunk.data); + } + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}, + \"stream_mode\": [ + \"updates\" + ] + }" | jq -c 'select(.data and (.data | has("run_id") | not)) | .data' + ``` + +Output: + + {'agent': {'messages': [{'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + +## Waiting for stateless results + +In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows: + +=== "Python" + + ```python + stateless_run_result = await client.runs.wait( + None, + assistant_id, + input=input, + ) + print(stateless_run_result) + ``` + +=== "Javascript" + + ```js + let statelessRunResult = await client.runs.wait( + null, + assistantId, + { input: input } + ); + console.log(statelessRunResult); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /runs/runs/wait \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Output: + + { + 'messages': [ + { + 'content': 'Hello! My name is Bagatur and I am 26 years old.', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': '5e088543-62c2-43de-9d95-6086ad7f8b48', + 'example': False} + , + { + 'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + } \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/stream_debug.md b/docs/docs/cloud/how-tos/stream_debug.md index e580286ca..e783b7f72 100644 --- a/docs/docs/cloud/how-tos/stream_debug.md +++ b/docs/docs/cloud/how-tos/stream_debug.md @@ -1,6 +1,10 @@ # How to stream debug events -This guide covers how to stream debug events from your graph (`stream_mode="debug"`). +This guide covers how to stream debug events from your graph (`stream_mode="debug"`). Streaming debug events produces responses containing `type` and `timestamp` keys. Debug events correspond to different steps in the graph's execution, and there are three different types of steps that will get streamed back to you: + +- `checkpoint`: These events will get streamed anytime the graph saves its state, which occurs after every super-step. Read more about checkpoints [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer) +- `task`: These events will get streamed before each super-step, and will contain information about a single task. Each super-step works by executing a list of tasks, where each task is scoped to a specific node and input. Below we will discuss the format of these tasks in more detail. +- `task_result`: After each `task` event, you will see a corresponding `task_result` event which as the name suggests contains information on the results of the task executed in the super-step. Scroll more to learn about the exact structure of these events. First let's set up our client and thread: @@ -10,6 +14,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -21,20 +27,35 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', - 'created_at': '2024-06-21T22:10:27.696862+00:00', - 'updated_at': '2024-06-21T22:10:27.696862+00:00', - 'metadata': {}} + { + 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', + 'created_at': '2024-06-21T22:10:27.696862+00:00', + 'updated_at': '2024-06-21T22:10:27.696862+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } -Streaming debug events produces responses containing `type` and `timestamp` keys. Debug events correspond to different steps in the graph's execution (e.g. `task`, `task_result`, `checkpoint`). === "Python" @@ -53,7 +74,7 @@ Streaming debug events produces responses containing `type` and `timestamp` keys # stream debug async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="debug", ): @@ -67,95 +88,143 @@ Streaming debug events produces responses containing `type` and `timestamp` keys ```js // create input const input = { - "messages": [ - { - "role": "human", - "content": "What's the weather in SF?", - } - ] - } + messages: [ + { + role: "human", + content: "What's the weather in SF?", + } + ] + }; // stream debug const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "debug" } ); + for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in SF?\"}]}, + \"stream_mode\": [ + \"debug\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + + Output: Receiving new event of type: metadata... - {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'} - - - + {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.256850+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a2e-6bb6-bfff-8423bcf47561', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': []}, 'metadata': {'source': 'input', 'step': -1, 'writes': {'messages': [{'role': 'human', 'content': "What's the weather in SF?"}]}}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.134680+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-d8f3-6b25-bfff-30a8ed6460bd', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [], 'search_results': []}, 'metadata': {'source': 'input', 'writes': {'messages': [{'role': 'human', 'content': "What's the weather in SF?"}]}, 'step': -1}, 'next': ['__start__'], 'tasks': [{'id': 'b40d2c90-dc1e-52db-82d6-08751b769c55', 'name': '__start__', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.259723+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a35-6c86-8000-f4a85315dbeb', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}]}, 'metadata': {'source': 'loop', 'step': 0, 'writes': None}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.139821+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-d900-63f1-8000-70fe53e0da5c', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}], 'search_results': []}, 'metadata': {'source': 'loop', 'writes': None, 'step': 0}, 'next': ['call_model'], 'tasks': [{'id': '685d89f6-542b-5e11-8cff-2963e7f4ea63', 'name': 'call_model', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.260021+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}], 'sleep': None}, 'triggers': ['start:agent']}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.139928+00:00', 'step': 1, 'payload': {'id': '600a6ff3-7ff1-570a-b626-f887e9a70f1c', 'name': 'call_model', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}], 'search_results': [], 'final_answer': None}, 'triggers': ['start:call_model']}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.267632+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:28.584833+00:00', 'step': 1, 'payload': {'id': '600a6ff3-7ff1-570a-b626-f887e9a70f1c', 'name': 'call_model', 'error': None, 'result': [['messages', {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.268469+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a4b-60ae-8001-dd378f965bf7', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 1, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.584991+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-dd3f-616f-8001-ce1c6f31e130', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': []}, 'metadata': {'source': 'loop', 'writes': {'call_model': {'messages': {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}}, 'step': 1}, 'next': ['exa_search', 'tavily_search'], 'tasks': [{'id': '43865935-be38-5f6e-8d38-d44ef369c278', 'name': 'exa_search', 'interrupts': []}, {'id': 'dc220677-2720-56c7-a524-caaff60fce2c', 'name': 'tavily_search', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.268659+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}, 'triggers': ['branch:agent:should_continue:tool']}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.585219+00:00', 'step': 2, 'payload': {'id': '870b5854-2f84-533d-8e7d-87158ee948fc', 'name': 'exa_search', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': [], 'final_answer': None}, 'triggers': ['call_model']}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.272916+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'result': [['messages', [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]]]}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.585219+00:00', 'step': 2, 'payload': {'id': '7589abfc-04df-58c6-8835-be172f84a7ff', 'name': 'tavily_search', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': [], 'final_answer': None}, 'triggers': ['call_model']}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.273113+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a56-6832-8002-8ab17e662980', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}, 'metadata': {'source': 'loop', 'step': 2, 'writes': {'tool': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}}}}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:32.422243+00:00', 'step': 2, 'payload': {'id': '7589abfc-04df-58c6-8835-be172f84a7ff', 'name': 'tavily_search', 'error': None, 'result': [['search_results', ["{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.273192+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}], 'sleep': None}, 'triggers': ['tool']}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:34.750124+00:00', 'step': 2, 'payload': {'id': '870b5854-2f84-533d-8e7d-87158ee948fc', 'name': 'exa_search', 'error': None, 'result': [['search_results', ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.']]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.277262+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:34.750266+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65939-180b-6087-8002-f969296f8e3d', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]}, 'metadata': {'source': 'loop', 'writes': {'exa_search': {'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.']}, 'tavily_search': {'search_results': ["{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]}}, 'step': 2}, 'next': ['summarize_search_results'], 'tasks': [{'id': '7263c738-516d-5708-b318-2c8ef54d4a33', 'name': 'summarize_search_results', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.277519+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a61-6462-8003-1316d9875b7f', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 3, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}} - - - - Receiving new event of type: end... - None - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:34.750394+00:00', 'step': 3, 'payload': {'id': '5beaa05d-57d4-5acd-95c1-c7093990910f', 'name': 'summarize_search_results', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"], 'final_answer': None}, 'triggers': ['exa_search', 'tavily_search']}} + + + + Receiving new event of type: debug... + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:35.851058+00:00', 'step': 3, 'payload': {'id': '5beaa05d-57d4-5acd-95c1-c7093990910f', 'name': 'summarize_search_results', 'error': None, 'result': [['final_answer', {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]], 'interrupts': []}} + + + + Receiving new event of type: debug... + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:35.851194+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65939-228a-6d93-8003-8b06d7483024', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"], 'final_answer': {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}, 'metadata': {'source': 'loop', 'writes': {'summarize_search_results': {'final_answer': {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}}, 'step': 3}, 'next': [], 'tasks': []}} + + + +We see that our debug events start with two `checkpoint` events at step 0 and 1, which represent checkpointing before the graph is created and after it has been created. We then see a single `task` and corresponding `task_result` which corresponds to our first node, `call_model`, being triggered. After it has finished, the entire super-step is over so the graph saves another checkpoint and we see the corresponding `checkpoint` event. + +The next super-step executed two search nodes [in parallel](https://langchain-ai.github.io/langgraph/how-tos/branching/) - specifically one node will execute an Exa search, while the other will use Tavily. Executing these nodes in parallel in the same super-step creates 2 `task` events and two corresponding `task_result` events. After we receive both of those `task_result` events, we see another `checkpoint` event as we would expect. + +Lastly, we see a final `task` and `task_result` pair corresponding to the `summarize_search_results` node, which is the last node in our graph. As soon as this super-step is done we see one final `checkpoint` event corresponding to the final checkpoint of this run. diff --git a/docs/docs/cloud/how-tos/stream_events.md b/docs/docs/cloud/how-tos/stream_events.md index 3b0c58177..e9fdb1710 100644 --- a/docs/docs/cloud/how-tos/stream_events.md +++ b/docs/docs/cloud/how-tos/stream_events.md @@ -1,5 +1,6 @@ # How to stream events -This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently. + +This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently. Read more about events in this [conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#astream_events-for-streaming-tokens-of-llm-calls). === "Python" @@ -7,6 +8,8 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -18,9 +21,11 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -28,18 +33,22 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd', - 'created_at': '2024-06-24T22:16:29.301522+00:00', - 'updated_at': '2024-06-24T22:16:29.301522+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd', + 'created_at': '2024-06-24T22:16:29.301522+00:00', + 'updated_at': '2024-06-24T22:16:29.301522+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } @@ -62,7 +71,7 @@ Streaming events produces responses containing an `event` key (in addition to ot # stream events async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="events", ): @@ -76,27 +85,27 @@ Streaming events produces responses containing an `event` key (in addition to ot ```js // create input const input = { - "messages": [ - { - "role": "human", - "content": "What's the weather in SF?", - } - ] + "messages": [ + { + "role": "human", + "content": "What's the weather in SF?", + } + ] } // stream events const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "events" } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` @@ -279,9 +288,6 @@ Output: Receiving new event of type: end... None - - - ## Token-by-Token Streaming @@ -296,7 +302,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th # stream token-by-token async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="events", ): @@ -317,7 +323,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th // stream events const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "events" diff --git a/docs/docs/cloud/how-tos/stream_messages.md b/docs/docs/cloud/how-tos/stream_messages.md index c25f2ccdd..5d7d8ed61 100644 --- a/docs/docs/cloud/how-tos/stream_messages.md +++ b/docs/docs/cloud/how-tos/stream_messages.md @@ -1,15 +1,7 @@ # How to stream messages from your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: +This guide covers how to stream messages from your graph. In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages. -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="messages"`. - -In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages. E.g., the state should look something like: === "Python" @@ -23,16 +15,28 @@ E.g., the state should look something like: messages: Annotated[list[AnyMessage], add_messages] ``` +=== "Javascript" -Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above). + ```js + import { type BaseMessage } from "@langchain/core/messages"; + import { Annotation, messagesStateReducer } from "@langchain/langgraph"; -> [!NOTE] -> LangGraph Cloud only supports hosting graphs written in Python at the moment. + export const StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + }); + ``` + +Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above). Or in Javascript: `import { MessagesAnnotation } from "@langchain/langgraph";`. With `stream_mode="messages"` two things will be streamed back: - It outputs messages produced by any chat model called inside (unless tagged in a special way) -- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like +- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like) + +Read more about how the `messages` streaming mode works [here](https://langchain-ai.github.io/langgraph/cloud/concepts/api/#modemessages) First let's set up our client and thread: @@ -42,6 +46,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -53,9 +59,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -63,17 +71,21 @@ First let's set up our client and thread: ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86', - 'created_at': '2024-06-21T15:48:59.808924+00:00', - 'updated_at': '2024-06-21T15:48:59.808924+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86', + 'created_at': '2024-06-21T15:48:59.808924+00:00', + 'updated_at': '2024-06-21T15:48:59.808924+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Let's also define a helper function for better formatting of the tool calls in messages (for CURL we will define a helper script called `process_stream.sh`) @@ -178,7 +190,7 @@ Now we can stream by messages, which will return complete messages (at the end o async for event in client.runs.stream( thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, config=config, stream_mode="messages", @@ -217,24 +229,25 @@ Now we can stream by messages, which will return complete messages (at the end o ```js const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in sf", + role: "human", + content: "What's the weather in sf", } ] - } - const config = {"configurable": {"model_name": "openai"}} + }; + const config = { configurable: { model_name: "openai" } }; const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, config, streamMode: "messages" } ); + for await (const event of streamResponse) { if (event.event === "metadata") { console.log(`Metadata: Run ID - ${event.data.run_id}`); diff --git a/docs/docs/cloud/how-tos/stream_multiple.md b/docs/docs/cloud/how-tos/stream_multiple.md index 2f03fb550..4d11aa026 100644 --- a/docs/docs/cloud/how-tos/stream_multiple.md +++ b/docs/docs/cloud/how-tos/stream_multiple.md @@ -10,6 +10,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -21,9 +23,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -31,17 +35,21 @@ First let's set up our client and thread: ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', - 'created_at': '2024-06-24T21:30:07.980789+00:00', - 'updated_at': '2024-06-24T21:30:07.980789+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', + 'created_at': '2024-06-24T21:30:07.980789+00:00', + 'updated_at': '2024-06-24T21:30:07.980789+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } When configuring multiple streaming modes for a run, responses for each respective mode will be produced. In the following example, note that a `list` of modes (`messages`, `events`, `debug`) is passed to the `stream_mode` parameter and the response contains `events`, `debug`, `messages/complete`, `messages/metadata`, and `messages/partial` event types. @@ -61,7 +69,7 @@ When configuring multiple streaming modes for a run, responses for each respecti # stream events with multiple streaming modes async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode=["messages", "events", "debug"], ): @@ -75,27 +83,27 @@ When configuring multiple streaming modes for a run, responses for each respecti ```js // create input const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in SF?", + role: "human", + content: "What's the weather in SF?", } ] - } + }; // stream events with multiple streaming modes const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: ["messages", "events", "debug"] } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` @@ -482,5 +490,4 @@ Output: None - diff --git a/docs/docs/cloud/how-tos/stream_updates.md b/docs/docs/cloud/how-tos/stream_updates.md index 38edc3381..fe68bac3b 100644 --- a/docs/docs/cloud/how-tos/stream_updates.md +++ b/docs/docs/cloud/how-tos/stream_updates.md @@ -1,13 +1,6 @@ # How to stream state updates of your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: - -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="updates"`. +This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more. First let's set up our client and thread: @@ -17,6 +10,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -28,9 +23,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -38,17 +35,21 @@ First let's set up our client and thread: ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16', - 'created_at': '2024-06-21T15:22:07.453100+00:00', - 'updated_at': '2024-06-21T15:22:07.453100+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16', + 'created_at': '2024-06-21T15:22:07.453100+00:00', + 'updated_at': '2024-06-21T15:22:07.453100+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Now we can stream by updates, which outputs updates made to the state by each node after it has executed: @@ -66,7 +67,7 @@ Now we can stream by updates, which outputs updates made to the state by each no } async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, stream_mode="updates", ): @@ -79,26 +80,27 @@ Now we can stream by updates, which outputs updates made to the state by each no ```js const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in la", + role: "human", + content: "What's the weather in la" } ] - } + }; const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "updates" } ); + for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` diff --git a/docs/docs/cloud/how-tos/stream_values.md b/docs/docs/cloud/how-tos/stream_values.md index bb2c237eb..334c816c5 100644 --- a/docs/docs/cloud/how-tos/stream_values.md +++ b/docs/docs/cloud/how-tos/stream_values.md @@ -1,13 +1,6 @@ # How to stream full state of your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: - -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="values"`. +This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more. First let's set up our client and thread: @@ -17,6 +10,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -28,9 +23,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -38,17 +35,21 @@ First let's set up our client and thread: ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data '{}' ``` Output: - {'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', - 'created_at': '2024-06-24T21:30:07.980789+00:00', - 'updated_at': '2024-06-24T21:30:07.980789+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', + 'created_at': '2024-06-24T21:30:07.980789+00:00', + 'updated_at': '2024-06-24T21:30:07.980789+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Now we can stream by values, which streams the full state of the graph after each node has finished executing: @@ -60,7 +61,7 @@ Now we can stream by values, which streams the full state of the graph after eac # stream values async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, stream_mode="values" ): @@ -76,16 +77,16 @@ Now we can stream by values, which streams the full state of the graph after eac const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "values" } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` @@ -168,7 +169,7 @@ If we want to just get the final result, we can use this endpoint and just keep final_answer = None async for chunk in client.runs.stream( thread["thread_id"], - "agent", + assistant_id, input=input, stream_mode="values" ): @@ -182,7 +183,7 @@ If we want to just get the final result, we can use this endpoint and just keep let finalAnswer; const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "values" diff --git a/docs/docs/cloud/how-tos/webhooks.md b/docs/docs/cloud/how-tos/webhooks.md new file mode 100644 index 000000000..a7148add4 --- /dev/null +++ b/docs/docs/cloud/how-tos/webhooks.md @@ -0,0 +1,125 @@ +# Use Webhooks + +You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the "webhook" parameter. + +Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows. + +The following endpoints accept `webhook` as a parameter: + +- Create Run -> POST /thread/{thread_id}/runs +- Create Thread Cron -> POST /thread/{thread_id}/runs/crons +- Stream Run -> POST /thread/{thread_id}/runs/stream +- Wait Run -> POST /thread/{thread_id}/runs/wait +- Create Cron -> POST /runs/crons +- Stream Run Stateless -> POST /runs/stream +- Wait Run Stateless -> POST /runs/wait + +In this example, we will show calling a webhook after streaming a run. First, let's setup our assistant and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +Output: + + { + 'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217', + 'created_at': '2024-08-30T23:07:38.242730+00:00', + 'updated_at': '2024-08-30T23:07:38.242730+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +Now we can invoke a run with a webhook: + +=== "Python" + + ```python + # create input + input = { "messages": [{ "role": "human", "content": "Hello!" }] } + + async for chunk in client.runs.stream( + thread_id=thread["thread_id"], + assistant_id=assistant_id, + input=input, + stream_mode="events", + webhook="your-webhook" + ): + # Do something with the stream output + pass + ``` + +=== "Javascript" + + ```js + // create input + const input = { messages: [{ role: "human", content: "Hello!" }] }; + + // stream events + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input: input, + webhook: "your-webhook" + } + ); + for await (const chunk of streamResponse) { + // Do something with the stream output + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input" : {"messages":[{"role": "user", "content": "Hello!"}]}, + "webhook": + }' + ``` + +And that's it! Now you can trigger your custom webhooks whenever you want in your LangGraph applications! \ No newline at end of file diff --git a/docs/docs/cloud/quick_start.md b/docs/docs/cloud/quick_start.md index c59a6fb85..755ce0df4 100644 --- a/docs/docs/cloud/quick_start.md +++ b/docs/docs/cloud/quick_start.md @@ -14,13 +14,25 @@ This tutorial will use: 1. Create a new application with the following directory and files: +=== "Python" + / |-- agent.py # code for your LangGraph agent |-- requirements.txt # Python packages required for your graph |-- langgraph.json # configuration file for LangGraph |-- .env # environment files with API keys -2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent, read more about it [here](..//concepts/agentic_concepts.md#react-agent). +=== "Javascript" + + / + |-- agent.ts # code for your LangGraph agent + |-- package.json # Javascript packages required for your graph + |-- langgraph.json # configuration file for LangGraph + |-- .env # environment files with API keys + +2. The `agent.py`/`agent.ts` file should contain code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent. You can read more about it [here](../concepts/agentic_concepts.md#react-agent). + +=== "Python" ```python from langchain_anthropic import ChatAnthropic @@ -34,14 +46,53 @@ This tutorial will use: graph = create_react_agent(model, tools) ``` -3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: +=== "Javascript" - langgraph - langchain_anthropic - tavily-python - langchain_community + ```ts + import { ChatAnthropic } from "@langchain/anthropic"; + import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; -4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`. + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + }); + + const tools = [ + new TavilySearchResults({ maxResults: 3, }), + ]; + + export const graph = createReactAgent({ llm: model, tools }); + ``` + +3. The `requirements.txt`/`package.json` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: + +=== "Python" + + ```python + langgraph + langchain_anthropic + tavily-python + langchain_community + ``` + +=== "Javascript" + + ```js + { + "name": "my-app", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "0.2.0", + "@langchain/openai": "^0.2.8" + } + } + ``` + +4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`/`agent.ts`. + +=== "Python" ```json { @@ -53,7 +104,21 @@ This tutorial will use: } ``` - Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). +=== "Javascript" + + ```json + { + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" + } + ``` + +Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). 5. The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step. NOTE: if you do add this, you should NOT check this into git. For this graph, we need two environment variables: @@ -206,36 +271,108 @@ export LANGSMITH_API_KEY=... The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on: -```python -from langgraph_sdk import get_client +=== "Python" -# Replace this with the URL of your own deployed graph -URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app" -client = get_client(url=URL) + ```python + from langgraph_sdk import get_client -# Search all hosted graphs -assistants = await client.assistants.search() -# In this example we select the first assistant since we are only hosting a single graph -assistant = assistants[0] + client = get_client(url=) + # get default assistant + assistants = await client.assistants.search() + assistant = [a for a in assistants if not a["config"]][0] + # create thread + thread = await client.threads.create() + print(thread) + ``` -# We create a thread for tracking the state of our run -thread = await client.threads.create() -``` +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // get default assistant + const assistants = await client.assistants.search(); + const assistant = assistants.find(a => !a.config); + // create thread + const thread = await client.threads.create(); + console.log(thread) + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` We can then execute a run on the thread: -```python -input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} +=== "Python" -async for chunk in client.runs.stream( - thread['thread_id'], - assistant["assistant_id"], - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) -``` + ```python + input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} + + async for chunk in client.runs.stream( + thread['thread_id'], + assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + if chunk.data and chunk.event != "metadata": + print(chunk.data) + ``` + +=== "Javascript" + + ```js + const input = { "messages":[{ "role": "user", "content": "Hello! My name is Bagatur and I am 26 years old." }] }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistant["assistant_id"], + { + input, + } + ); + for await (const chunk of streamResponse) { + if (chunk.data && chunk.event !== "metadata" ) { + console.log(chunk.data); + } + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": , + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}, + }" | sed 's/\r$//' | awk ' + /^event:/ { event = $2 } + /^data:/ { + json_data = substr($0, index($0, $2)) + + if (event != "metadata") { + print json_data + } + }' + ``` + + +Output: {'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} diff --git a/docs/docs/cloud/reference/api/api_ref.md b/docs/docs/cloud/reference/api/api_ref.md index 0cac27c63..7c2a085f2 100644 --- a/docs/docs/cloud/reference/api/api_ref.md +++ b/docs/docs/cloud/reference/api/api_ref.md @@ -3,3 +3,20 @@ The LangGraph Cloud API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`). Click here to view the API reference. + +## Authentication + +For deployments to LangGraph Cloud, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Cloud API. The value of the header should be set to a valid LangSmith API key for the organization where the API is deployed. + +Example `curl` command: +```shell +curl --request POST \ + --url http://localhost:8124/assistants/search \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: LANGSMITH_API_KEY' \ + --data '{ + "metadata": {}, + "limit": 10, + "offset": 0 +}' +``` diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index fb0b734fd..7dcb58c8b 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -401,7 +401,17 @@ def node_a(state, config): ... ``` -See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration +See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration. + +### Recursion Limit + +The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below: + +```python +graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}}) +``` + +Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works. ## Breakpoints @@ -427,13 +437,202 @@ It's often nice to be able to visualize graphs, especially as they get more comp ## Streaming -LangGraph is built with first class support for streaming. There are several different streaming modes that LangGraph supports: +LangGraph is built with first class support for streaming. There are several different ways to stream back results + +### `.stream` and `.astream` + +`.stream` and `.astream` are sync and async methods for streaming back results. +There are several different modes you can specify when calling these methods (e.g. `graph.stream(..., mode="...")): - [`"values"`](../how-tos/stream-values.ipynb): This streams the full value of the state after each step of the graph. -- [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately. +- [`"updates"`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately. - `"debug"`: This streams as much information as possible throughout the execution of the graph. +The below visualization shows the difference between the `values` and `updates` modes: + +![values vs updates](../static/values_vs_updates.png) + + +### `.astream_events` (for streaming tokens of LLM calls) + In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb). +This is a standard method on all [LangChain objects](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`. + +All events have (among other things) `event`, `name`, and `data` fields. What do these mean? + +- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events). +- `name`: This is the name of event. +- `data`: This is the data associated with the event. + +What types of things cause events to be emitted? + +* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field +* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field +* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events + +Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`. + +To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph: + +```python +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START, END + +model = ChatOpenAI(model="gpt-3.5-turbo") + + +def call_model(state: MessagesState): + response = model.invoke(state['messages']) + return {"messages": response} + +workflow = StateGraph(MessagesState) +workflow.add_node(call_model) +workflow.add_edge(START, "call_model") +workflow.add_edge("call_model", END) +app = workflow.compile() + +inputs = [{"role": "user", "content": "hi!"}] +async for event in app.astream_events({"messages": inputs}, version="v2"): + kind = event["event"] + print(f"{kind}: {event['name']}") +``` +```shell +on_chain_start: LangGraph +on_chain_start: __start__ +on_chain_end: __start__ +on_chain_start: call_model +on_chat_model_start: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_end: ChatOpenAI +on_chain_start: ChannelWrite +on_chain_end: ChannelWrite +on_chain_stream: call_model +on_chain_end: call_model +on_chain_stream: LangGraph +on_chain_end: LangGraph +``` + +We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input). +We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`), +stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there, +we write the results back to the channel (`ChannelWrite`) and then finish the `call_model` node and then the graph as a whole. + +This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain? +Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event +since it is needed for streaming tokens from an LLM response. + +These events look like: + +```shell +{'event': 'on_chat_model_stream', + 'name': 'ChatOpenAI', + 'run_id': '3fdbf494-acce-402e-9b50-4eab46403859', + 'tags': ['seq:step:1'], + 'metadata': {'langgraph_step': 1, + 'langgraph_node': 'call_model', + 'langgraph_triggers': ['start:call_model'], + 'langgraph_task_idx': 0, + 'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c', + 'checkpoint_ns': 'call_model', + 'ls_provider': 'openai', + 'ls_model_name': 'gpt-3.5-turbo', + 'ls_model_type': 'chat', + 'ls_temperature': 0.7}, + 'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')}, + 'parent_ids': []} +``` +We can see that we have the event type and name (which we knew from before). + +We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information +which tells us which node this model was invoked inside of. + +Finally, `data` is a really important field. This contains the actual data for this event! Which in this case +is an AIMessageChunk. This contains the `content` for the message, as well as an `id`. +This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps +us track which chunks are part of the same message (so we can show them together in the UI). + +This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a +guide for that [here](../how-tos/streaming-tokens.ipynb). + + !!! warning "ASYNC IN PYTHON<=3.10" - You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). \ No newline at end of file + You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). + +#### Only stream tokens from specific nodes/LLMs + + +There are certain cases where you have multiple nodes in your graph that make LLM calls, and you do not wish to stream the tokens from every single LLM call. For example, you may use one LLM as a planner for the next steps to take, and another LLM somewhere else in the graph that actually responds to the user. In that case, you most likely WON'T want to stream tokens from the planner LLM but WILL want to stream them from the respond to user LLM. Below we show two different ways of doing this, one by streaming from specific nodes only and the second by streaming from specific LLMs only. + +First, let's define our graph: + +```python +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START, END + +model_1 = ChatOpenAI(model="gpt-3.5-turbo", name="model_1") +model_2 = ChatOpenAI(model="gpt-3.5-turbo", name="model_2") + +def call_first_model(state: MessagesState): + response = model_1.invoke(state['messages']) + return {"messages": response} + +def call_second_model(state: MessagesState): + response = model_2.invoke(state['messages']) + return {"messages": response} + +workflow = StateGraph(MessagesState) +workflow.add_node(call_first_model) +workflow.add_node(call_second_model) +workflow.add_edge(START, "call_first_model") +workflow.add_edge("call_first_model", "call_second_model") +workflow.add_edge("call_second_model", END) +app = workflow.compile() +``` + +**Streaming from specific node** + +In the case that we only want the output from a single node, we can use the event metadata to filter node names: + +```python +inputs = [{"role": "user", "content": "hi!"}] + +async for event in app.astream_events({"messages": inputs}, version="v2"): + # Get chat model tokens from a particular node + if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == "call_second_model": + print(event["data"]["chunk"].content, end="|", flush=True) +``` + +```shell +|Hello|!| How| can| I| help| you| today|?|| +``` + +As we can see only the response from the second LLM was streamed (you can tell because we only received a single response, if we had streamed both we would have received two "Hello! How can I help you today?" messages). + +**Streaming from specific LLM** + +Sometimes you might want to stream from specific LLMs instead of specific nodes. This could be the case if you have multiple LLM calls inside a single node, and only want to stream the output of a specific one or if you use the same LLM in different nodes and want to stream it's output anytime it is called. We can do this by using the `name` parameter for LLMs and events: + +```python +inputs = [{"role": "user", "content": "hi!"}] +async for event in app.astream_events({"messages": inputs}, version="v2"): + # Get chat model tokens from a particular LLM inside a particular node + if event["event"] == "on_chat_model_stream" and event['name'] == "model_2": + print(event["data"]["chunk"].content, end="|", flush=True) +``` + +```shell +|Hello|!| How| can| I| assist| you| today|?|| +``` + +As expected, we only see a single LLM response since the response from `model_1` was not streamed. \ No newline at end of file diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 09d5f2e8f..966b77221 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -12,10 +12,9 @@ Welcome to the LangGraph how-to guides! These guides provide practical, step-by- LangGraph is known for being a highly controllable agent framework. These how-to guides show how to achieve that controllability. -- [How to create subgraphs](subgraph.ipynb) - [How to create branches for parallel execution](branching.ipynb) - [How to create map-reduce branches for parallel execution](map-reduce.ipynb) - +- [How to control graph recursion limit](recursion-limit.ipynb) ## Persistence @@ -28,6 +27,7 @@ LangGraph makes it easy to persist state across graph runs. The guide below show - [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb) - [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb) - [How to create a custom checkpointer using Redis](persistence_redis.ipynb) +- [How to share state between threads](memory/shared-state.ipynb) ## Human in the Loop @@ -35,6 +35,7 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea These guides cover common examples of that. - [How to add breakpoints](human_in_the_loop/breakpoints.ipynb) +- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb) - [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb) - [How to wait for user input](human_in_the_loop/wait-user-input.ipynb) - [How to view and update past graph state](human_in_the_loop/time-travel.ipynb) @@ -54,6 +55,7 @@ These guides show how to use different streaming modes. - [How to stream events from within a tool](streaming-events-from-within-tools.ipynb) - [How to stream events from within a tool without LangChain models](streaming-events-from-within-tools-without-langchain.ipynb) - [How to stream events from the final node](streaming-from-final-node.ipynb) +- [How to stream from subgraphs](streaming-subgraphs.ipynb) ## Tool calling @@ -63,6 +65,12 @@ These guides show how to use different streaming modes. - [How to pass config to tools](pass-config-to-tools.ipynb) - [How to handle large numbers of tools](many-tools.ipynb) +## Subgraphs + +- [How to create subgraphs](subgraph.ipynb) +- [How to manage state in subgraphs](subgraphs-manage-state.ipynb) +- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb) + ## State Management - [Use Pydantic model as state](state-model.ipynb) @@ -78,6 +86,7 @@ These guides show how to use different streaming modes. - [How to use a Pydantic model as your state](state-model.ipynb) - [How to use a context object in state](state-context-key.ipynb) - [How to add node retries](node-retries.ipynb) +- [How to force function calling agent to structure output](react-agent-structured-output.ipynb) ## Prebuilt ReAct Agent diff --git a/docs/docs/static/values_vs_updates.png b/docs/docs/static/values_vs_updates.png new file mode 100644 index 000000000..dba7a4dc9 Binary files /dev/null and b/docs/docs/static/values_vs_updates.png differ diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3bc3f3d24..3b6962a76 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -126,9 +126,9 @@ nav: - "How-to Guides": - "how-tos/index.md" - Controllability: - - Create subgraphs: how-tos/subgraph.ipynb - Create branches for parallel execution: how-tos/branching.ipynb - Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb + - Control graph recursion limit: how-tos/recursion-limit.ipynb - Persistence: - Add persistence ("memory"): how-tos/persistence.ipynb - Manage conversation history: how-tos/memory/manage-conversation-history.ipynb @@ -137,8 +137,10 @@ nav: - Use Postgres checkpointer for persistence: how-tos/persistence_postgres.ipynb - Create custom checkpointer using MongoDB: how-tos/persistence_mongodb.ipynb - Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb + - Share state between threads: how-tos/memory/shared-state.ipynb - Human-in-the-loop: - Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb + - Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb - Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb - View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb - Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb @@ -153,12 +155,17 @@ nav: - Stream events from within tools: how-tos/streaming-events-from-within-tools.ipynb - Stream events from within tools without LangChain models: how-tos/streaming-events-from-within-tools-without-langchain.ipynb - Stream events from the final node: how-tos/streaming-from-final-node.ipynb + - Stream from subgraphs: how-tos/streaming-subgraphs.ipynb - Tool calling: - Call tools using ToolNode: how-tos/tool-calling.ipynb - Handle tool calling errors: how-tos/tool-calling-errors.ipynb - Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb - Pass config to tools: how-tos/pass-config-to-tools.ipynb - Handle many tools: how-tos/many-tools.ipynb + - Subgraphs: + - Create subgraphs: how-tos/subgraph.ipynb + - Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb + - Transform inputs and outputs of a subgraph: how-tos/subgraph-transform-state.ipynb - State Management: - Use Pydantic model as state: how-tos/state-model.ipynb - Use a context object in state: how-tos/state-context-key.ipynb @@ -169,11 +176,12 @@ nav: - Visualize your graph: how-tos/visualization.ipynb - Add runtime configuration: how-tos/configuration.ipynb - Add node retries: how-tos/node-retries.ipynb + - How to return structured output from a ReAct agent: how-tos/react-agent-structured-output.ipynb - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb - Add a system prompt to a ReAct agent: how-tos/create-react-agent-system-prompt.ipynb - - Add human-in-the-Loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb + - Add Human-in-the-loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb - "Conceptual Guides": - "concepts/index.md" - LangGraph for Agentic Applications: concepts/high_level.md @@ -194,11 +202,12 @@ nav: - Setup: - Setup App: "cloud/deployment/setup.md" - Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md" + - Setup App (JavaScript): "cloud/deployment/setup_javascript.md" - Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md" + - Customize Dockerfile: "cloud/deployment/custom_docker.md" - Test App Locally: "cloud/deployment/test_locally.md" - Deployment: - Deploy to Cloud: "cloud/deployment/cloud.md" - - Self-Host: "cloud/deployment/self_hosted.md" - Streaming: - Stream Values: "cloud/how-tos/stream_values.md" - Stream Updates: "cloud/how-tos/stream_updates.md" @@ -223,16 +232,17 @@ nav: - Invoke graph in LangGraph Studio: "cloud/how-tos/invoke_studio.md" - Interact with threads in LangGraph Studio: "cloud/how-tos/threads_studio.md" - Different Types of Runs: - - Run an Agent in the Background: "cloud/how-tos/cloud_examples/background_run.ipynb" - - Run Multiple Agents in Same Thread: "cloud/how-tos/cloud_examples/same-thread.ipynb" - - Create Cron Jobs: "cloud/how-tos/cloud_examples/cron_jobs.ipynb" - - Create Stateless Runs: "cloud/how-tos/cloud_examples/stateless_runs.ipynb" + - Run an Agent in the Background: "cloud/how-tos/background_run.md" + - Run Multiple Agents in Same Thread: "cloud/how-tos/same-thread.md" + - Create Cron Jobs: "cloud/how-tos/cron_jobs.md" + - Create Stateless Runs: "cloud/how-tos/stateless_runs.md" - Other: - - Configure Agents: "cloud/how-tos/cloud_examples/configuration_cloud.ipynb" + - Configure Agents: "cloud/how-tos/configuration_cloud.md" - Convert LangGraph calls to LangGraph Cloud calls: "cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb" - - Integrate Webhooks: 'cloud/how-tos/cloud_examples/webhooks.ipynb' + - Integrate Webhooks: 'cloud/how-tos/webhooks.md' - Copy Threads: 'cloud/how-tos/copy_threads.md' - Check Status of Threads: "cloud/how-tos/check_thread_status.md" + - Share State Between Threads: "cloud/how-tos/shared_state.md" - Conceptual Guides: - API Concepts: "cloud/concepts/api.md" - Cloud Concepts: "cloud/concepts/cloud.md" diff --git a/examples/async.ipynb b/examples/async.ipynb index f124292de..e3e3e27b0 100644 --- a/examples/async.ipynb +++ b/examples/async.ipynb @@ -37,7 +37,10 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] }, { "cell_type": "markdown", @@ -53,7 +56,18 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -69,7 +83,10 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -95,7 +112,22 @@ "id": "6768a3ab", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "# Add messages essentially does this with more\n", + "# robust handling\n", + "# def add_messages(left: list, right: list):\n", + "# return left + right\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] }, { "cell_type": "markdown", @@ -115,7 +147,19 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder, but don't tell the LLM that...\n", + " return [\"The answer to your question lies within.\"]\n", + "\n", + "\n", + "tools = [search]" + ] }, { "cell_type": "markdown", @@ -132,7 +176,11 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] + "source": [ + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tool_node = ToolNode(tools)" + ] }, { "cell_type": "markdown", @@ -156,7 +204,11 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": ["from langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-haiku-20240307\")"] + "source": [ + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "model = ChatAnthropic(model=\"claude-3-haiku-20240307\")" + ] }, { "cell_type": "markdown", @@ -174,7 +226,9 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": ["model = model.bind_tools(tools)"] + "source": [ + "model = model.bind_tools(tools)" + ] }, { "cell_type": "markdown", @@ -213,7 +267,29 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no tool call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\nasync def call_model(state: State):\n messages = state[\"messages\"]\n response = await model.ainvoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] + "source": [ + "from typing import Literal\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no tool call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "async def call_model(state: State):\n", + " messages = state[\"messages\"]\n", + " response = await model.ainvoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}" + ] }, { "cell_type": "markdown", @@ -231,7 +307,50 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(State)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", @@ -250,7 +369,11 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(app.get_graph().draw_mermaid_png()))" + ] }, { "cell_type": "markdown", @@ -283,7 +406,12 @@ "output_type": "execute_result" } ], - "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nawait app.ainvoke(inputs)"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "await app.ainvoke(inputs)" + ] }, { "cell_type": "markdown", @@ -352,7 +480,16 @@ ] } ], - "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream(inputs, stream_mode=\"updates\"):\n # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value[\"messages\"][-1].pretty_print())\n print(\"\\n---\\n\")"] + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream(inputs, stream_mode=\"updates\"):\n", + " # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value[\"messages\"][-1].pretty_print())\n", + " print(\"\\n---\\n\")" + ] }, { "cell_type": "markdown", @@ -409,15 +546,20 @@ ] } ], - "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream_log(inputs, include_types=[\"llm\"]):\n # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n for op in output.ops:\n if op[\"path\"] == \"/streamed_output/-\":\n # this is the output from .stream()\n ...\n elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n \"/streamed_output/-\"\n ):\n # because we chose to only include LLMs, these are LLM tokens\n print(op[\"value\"].content, end=\"|\")"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"].content, end=\"|\")" + ] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/high-level-tools.ipynb b/examples/chat_agent_executor_with_function_calling/high-level-tools.ipynb index bf1f9c029..516704843 100644 --- a/examples/chat_agent_executor_with_function_calling/high-level-tools.ipynb +++ b/examples/chat_agent_executor_with_function_calling/high-level-tools.ipynb @@ -103,14 +103,6 @@ " print(list(s.values())[0])\n", " print(\"----\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index c41653c50..266da1ce9 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -235,7 +235,7 @@ " # Call the chat bot\n", " chat_bot_response = my_chat_bot(messages)\n", " # Respond with an AI Message\n", - " return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}" + " return {\"messages\": [AIMessage(content=chat_bot_response[\"content\"])]}" ] }, { @@ -270,7 +270,7 @@ " # Call the simulated user\n", " response = simulated_user.invoke({\"messages\": new_messages})\n", " # This response is an AI message - we need to flip this to be a human message\n", - " return {\"messages\":[HumanMessage(content=response.content)]}" + " return {\"messages\": [HumanMessage(content=response.content)]}" ] }, { @@ -331,6 +331,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "graph_builder.add_node(\"user\", simulated_user_node)\n", "graph_builder.add_node(\"chat_bot\", chat_bot_node)\n", @@ -398,14 +399,6 @@ " print(chunk)\n", " print(\"----\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb index 47b0e0a2d..d0aaf00ee 100644 --- a/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb @@ -359,14 +359,6 @@ " evaluation=evaluation,\n", ")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26735ed2-766d-4e0a-a185-b2295a0615b8", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb index bc1248cda..ad375f02f 100644 --- a/examples/chatbots/information-gather-prompting.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 21, "id": "5f795b78-004d-40ca-95d6-069f67e4f9c9", "metadata": {}, "outputs": [], @@ -77,7 +77,11 @@ "llm = ChatOpenAI(temperature=0)\n", "llm_with_tool = llm.bind_tools([PromptInstructions])\n", "\n", - "chain = get_messages_info | llm_with_tool" + "\n", + "def info_chain(state):\n", + " messages = get_messages_info(state[\"messages\"])\n", + " response = llm_with_tool.invoke(messages)\n", + " return {\"messages\": [response]}" ] }, { @@ -93,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "ca9a0234-bbeb-4bff-8276-8dde499c3390", "metadata": {}, "outputs": [], @@ -121,7 +125,10 @@ " return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs\n", "\n", "\n", - "prompt_gen_chain = get_prompt_messages | llm" + "def prompt_gen_chain(state):\n", + " messages = get_prompt_messages(state[\"messages\"])\n", + " response = llm.invoke(messages)\n", + " return {\"messages\": [response]}" ] }, { @@ -140,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "74f29e15-20e2-420c-a450-84e929f16e4e", "metadata": {}, "outputs": [], @@ -150,7 +157,8 @@ "from langgraph.graph import END\n", "\n", "\n", - "def get_state(messages) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n", + "def get_state(state) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n", + " messages = state[\"messages\"]\n", " if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:\n", " return \"add_tool_message\"\n", " elif not isinstance(messages[-1], HumanMessage):\n", @@ -171,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "59d9d6b4-dce4-43cc-9a1a-61a7912ed5b8", "metadata": {}, "outputs": [], @@ -182,20 +190,27 @@ "from typing import Annotated\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "memory = MemorySaver()\n", "workflow = StateGraph(State)\n", - "workflow.add_node(\"info\", chain)\n", + "workflow.add_node(\"info\", info_chain)\n", "workflow.add_node(\"prompt\", prompt_gen_chain)\n", "\n", "\n", "@workflow.add_node\n", - "def add_tool_message(state: list):\n", - " return ToolMessage(\n", - " content=\"Prompt generated!\", tool_call_id=state[-1].tool_calls[0][\"id\"]\n", - " )\n", + "def add_tool_message(state: State):\n", + " return {\n", + " \"messages\": [\n", + " ToolMessage(\n", + " content=\"Prompt generated!\",\n", + " tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n", + " )\n", + " ]\n", + " }\n", "\n", "\n", "workflow.add_conditional_edges(\"info\", get_state)\n", @@ -207,13 +222,13 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 29, "id": "1b1613e0", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -240,63 +255,105 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 30, "id": "25793988-45a2-4e65-b33c-64e72aadb10e", "metadata": {}, "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): hi\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Hello! How can I assist you today?\n", + "Hello! How can I assist you today?\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): rag prompt\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Sure! I can help you with that. To create an extraction prompt, I need some information from you. Could you please provide the following details:\n", + "Sure! I can help you create a prompt template. To get started, could you please provide me with the following information:\n", "\n", "1. What is the objective of the prompt?\n", "2. What variables will be passed into the prompt template?\n", "3. Any constraints for what the output should NOT do?\n", "4. Any requirements that the output MUST adhere to?\n", "\n", - "Once I have this information, I can create the extraction prompt for you.\n", + "Once I have this information, I can assist you in creating the prompt template.\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): 1 rag, 2 none, 3 no, 4 no\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Great! To create an extraction prompt for filling out a CSAT (Customer Satisfaction) survey, I will need the following information:\n", - "\n", - "1. Objective: To gather feedback on customer satisfaction.\n", - "2. Variables: Customer name, Date of interaction, Service provided, Rating (scale of 1-5), Comments.\n", - "3. Constraints: The output should not include any personally identifiable information (PII) of the customer.\n", - "4. Requirements: The output must include a structured format with fields for each variable mentioned above.\n", - "\n", - "With this information, I will proceed to create the extraction prompt template for filling out a CSAT survey. Let's get started!\n", "Tool Calls:\n", - " PromptInstructions (call_aU48Bjo7X29tXfRtCcrXkrqq)\n", - " Call ID: call_aU48Bjo7X29tXfRtCcrXkrqq\n", + " PromptInstructions (call_7qkSORledsemoCnK8A3RKvAb)\n", + " Call ID: call_7qkSORledsemoCnK8A3RKvAb\n", " Args:\n", - " objective: To gather feedback on customer satisfaction.\n", - " variables: ['Customer name', 'Date of interaction', 'Service provided', 'Rating (scale of 1-5)', 'Comments']\n", - " constraints: ['The output should not include any personally identifiable information (PII) of the customer.']\n", - " requirements: ['The output must include a structured format with fields for each variable mentioned above.']\n", + " objective: rag\n", + " variables: ['none']\n", + " constraints: ['no']\n", + " requirements: ['no']\n", "=================================\u001b[1m Tool Message \u001b[0m=================================\n", "\n", "Prompt generated!\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Please provide feedback on your recent interaction with our service. Your input is valuable to us in improving our services.\n", - "\n", - "Customer name: \n", - "Date of interaction: \n", - "Service provided: \n", - "Rating (scale of 1-5): \n", - "Comments: \n", - "\n", - "Please note that the output should not include any personally identifiable information (PII) of the customer. Your feedback will be kept confidential and used for internal evaluation purposes only. Thank you for taking the time to share your thoughts with us.\n", - "Done!\n", + "Please write a response using the RAG (Red, Amber, Green) rating system.\n", + "Done!\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): red\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "I'm glad you found it helpful! If you need any more assistance or have any other requests, feel free to let me know. Have a great day!\n", + "Thank you for providing the response. If you need any more assistance, feel free to ask!\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): q\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "AI: Byebye\n" ] } @@ -312,9 +369,9 @@ " break\n", " output = None\n", " for output in graph.stream(\n", - " [HumanMessage(content=user)], config=config, stream_mode=\"updates\"\n", + " {\"messages\": [HumanMessage(content=user)]}, config=config, stream_mode=\"updates\"\n", " ):\n", - " last_message = next(iter(output.values()))\n", + " last_message = next(iter(output.values()))[\"messages\"][-1]\n", " last_message.pretty_print()\n", "\n", " if output and \"prompt\" in output:\n", @@ -344,7 +401,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/examples/cloud_examples/background_run.ipynb b/examples/cloud_examples/background_run.ipynb deleted file mode 100644 index 6279402fc..000000000 --- a/examples/cloud_examples/background_run.ipynb +++ /dev/null @@ -1,388 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to kick off background runs\n", - "\n", - "This guide covers how to kick off background runs for your agent.\n", - "This can be useful for long running jobs." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b8e6408a-b37e-428f-9567-077fa55d58e8", - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize the client\n", - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4947e9bc-111f-4991-8c41-1041da9bf0ba", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': 'e90fee30-be91-43aa-a33c-d54bd219072e',\n", - " 'graph_id': 'agent',\n", - " 'created_at': '2024-06-18T18:06:55.102231+00:00',\n", - " 'updated_at': '2024-06-18T18:06:55.102231+00:00',\n", - " 'config': {'configurable': {'model_name': 'anthropic'}},\n", - " 'metadata': {}}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# List available assistants\n", - "assistants = await client.assistants.search()\n", - "assistants[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "230c0464-a6e5-420f-9e38-ca514e5634ce", - "metadata": {}, - "outputs": [], - "source": [ - "# NOTE: we can use `assistant_id` UUID from the above response, or just pass graph ID instead when creating runs. we'll use graph ID here\n", - "assistant_id = \"agent\"" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "56aa5159-5583-4134-9210-709b969bda6f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'created_at': '2024-06-21T14:58:02.079462+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.079462+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a new thread\n", - "thread = await client.threads.create()\n", - "thread" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "147c3f98-f889-4f05-a090-6b31f2a0b291", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If we list runs on this thread, we can see it is empty\n", - "runs = await client.runs.list(thread[\"thread_id\"])\n", - "runs" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "8c7b44ef-4816-496d-88a1-2f7327cf576d", - "metadata": {}, - "outputs": [], - "source": [ - "# Let's kick off a run\n", - "input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n", - "run = await client.runs.create(thread[\"thread_id\"], assistant_id, input=input)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "d84b4d80-b0aa-4d9f-a05d-0744b2fe8f72", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'created_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'metadata': {},\n", - " 'status': 'pending',\n", - " 'kwargs': {'input': {'messages': [{'role': 'human',\n", - " 'content': 'what's the weather in sf'}]},\n", - " 'config': {'metadata': {'created_by': 'system'},\n", - " 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': None,\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n", - " 'webhook': None,\n", - " 'temporary': False,\n", - " 'stream_mode': ['events'],\n", - " 'feedback_keys': None,\n", - " 'interrupt_after': None,\n", - " 'interrupt_before': None},\n", - " 'multitask_strategy': 'reject'}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The first time we poll it, we can see `status=pending`\n", - "await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "3639da3c-bfe5-454c-ab1e-8ed7af394dfe", - "metadata": {}, - "outputs": [], - "source": [ - "# Wait until the run finishes\n", - "await client.runs.join(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "8fa206ed-515e-4607-9a80-bebafe76cc24", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'created_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'metadata': {},\n", - " 'status': 'success',\n", - " 'kwargs': {'input': {'messages': [{'role': 'human',\n", - " 'content': 'what's the weather in sf'}]},\n", - " 'config': {'metadata': {'created_by': 'system'},\n", - " 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': None,\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n", - " 'webhook': None,\n", - " 'temporary': False,\n", - " 'stream_mode': ['events'],\n", - " 'feedback_keys': None,\n", - " 'interrupt_after': None,\n", - " 'interrupt_before': None},\n", - " 'multitask_strategy': 'reject'}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Eventually, it should finish and we should see `status=success`\n", - "await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "8de4495f-7873-487c-b1a8-ad2a78a1ff35", - "metadata": {}, - "outputs": [], - "source": [ - "# We can get the final results\n", - "final_result = await client.threads.get_state(thread[\"thread_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "9da76fce-66e4-4f1b-8c24-09759889e50e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'values': {'messages': [{'content': 'what's the weather in sf',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'human',\n", - " 'name': None,\n", - " 'id': 'bfe07fff-cb40-40be-84d5-a061d2c40006',\n", - " 'example': False},\n", - " {'content': [{'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb',\n", - " 'input': {'query': 'weather in san francisco'},\n", - " 'name': 'tavily_search_results_json',\n", - " 'type': 'tool_use'}],\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-6d8665ca-a77d-4b44-9a7b-4e975b155fb1',\n", - " 'example': False,\n", - " 'tool_calls': [{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'weather in san francisco'},\n", - " 'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'}],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None},\n", - " {'content': '[{\"url\": \"https://www.timeanddate.com/weather/usa/san-francisco/historic\", \"content\": \"San Francisco Weather History for the Previous 24 Hours Show weather for: Previous 24 hours June 17, 2024 June 16, 2024 June 15, 2024 June 14, 2024 June 13, 2024 June 12, 2024 June 11, 2024 June 10, 2024 June 9, 2024 June 8, 2024 June 7, 2024 June 6, 2024 June 5, 2024 June 4, 2024 June 3, 2024 June 2, 2024\"}]',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'tool',\n", - " 'name': 'tavily_search_results_json',\n", - " 'id': '257a1f29-2f66-4f9e-b35d-c8818dbbaa3f',\n", - " 'tool_call_id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'},\n", - " {'content': [{'text': 'The search results provide historic weather data for San Francisco, but do not give the current weather conditions. To get the current weather forecast for San Francisco, I would need to refine my search query. Here is an updated search:',\n", - " 'type': 'text'},\n", - " {'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx',\n", - " 'input': {'query': 'san francisco weather forecast today'},\n", - " 'name': 'tavily_search_results_json',\n", - " 'type': 'tool_use'}],\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-ca41dbf8-7e89-4ff2-a245-87098d7928ba',\n", - " 'example': False,\n", - " 'tool_calls': [{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'san francisco weather forecast today'},\n", - " 'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'}],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None},\n", - " {'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1718981382, \\'localtime\\': \\'2024-06-21 7:49\\'}, \\'current\\': {\\'last_updated_epoch\\': 1718981100, \\'last_updated\\': \\'2024-06-21 07:45\\', \\'temp_c\\': 12.8, \\'temp_f\\': 55.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Overcast\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/122.png\\', \\'code\\': 1009}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 200, \\'wind_dir\\': \\'SSW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.84, \\'precip_mm\\': 0.01, \\'precip_in\\': 0.0, \\'humidity\\': 86, \\'cloud\\': 100, \\'feelslike_c\\': 12.2, \\'feelslike_f\\': 53.9, \\'windchill_c\\': 11.2, \\'windchill_f\\': 52.1, \\'heatindex_c\\': 12.0, \\'heatindex_f\\': 53.5, \\'dewpoint_c\\': 9.4, \\'dewpoint_f\\': 48.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 3.0, \\'gust_mph\\': 7.6, \\'gust_kph\\': 12.2}}\"}]',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'tool',\n", - " 'name': 'tavily_search_results_json',\n", - " 'id': 'c80a3720-6a9f-4ff0-9ce2-6112e66a6f81',\n", - " 'tool_call_id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'},\n", - " {'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n", - " 'example': False,\n", - " 'tool_calls': [],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None}]},\n", - " 'next': [],\n", - " 'config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': '1ef2fdea-f879-65a5-8005-443b6a4039aa'}},\n", - " 'metadata': {'step': 5,\n", - " 'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'source': 'loop',\n", - " 'writes': {'agent': {'messages': [{'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n", - " 'name': None,\n", - " 'type': 'ai',\n", - " 'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n", - " 'example': False,\n", - " 'tool_calls': [],\n", - " 'usage_metadata': None,\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'invalid_tool_calls': []}]}},\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'created_by': 'system',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n", - " 'created_at': '2024-06-21T14:58:14.591805+00:00',\n", - " 'parent_config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': '1ef2fdea-d44c-6fc4-8004-d2713436777d'}}}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "final_result" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "ddd6e698-4609-4389-b84a-bb8939fff08b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# We can get the content of the final message\n", - "final_result[\"values\"][\"messages\"][-1][\"content\"]" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "langgraph-example-dev", - "language": "python", - "name": "langgraph-example-dev" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/configuration_cloud.ipynb b/examples/cloud_examples/configuration_cloud.ipynb deleted file mode 100644 index e8f00027f..000000000 --- a/examples/cloud_examples/configuration_cloud.ipynb +++ /dev/null @@ -1,206 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "68c0837d-c40a-4209-9f88-5d08c00c31b0", - "metadata": {}, - "source": [ - "# How to create agents with configuration\n", - "\n", - "One of the benefits of LangGraph API is that it lets you create agents with different configurations.\n", - "This is useful when you want to:\n", - "\n", - "- Define a cognitive architecture once as a LangGraph\n", - "- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use)\n", - "- Let users create agents with arbitrary configurations, save them, and then use them in the future\n", - "\n", - "In this guide we will show how to do that for the default agent we have built in.\n", - "\n", - "If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like:\n", - "\n", - "```python\n", - "def call_model(state, config):\n", - " messages = state[\"messages\"]\n", - " model_name = config.get('configurable', {}).get(\"model_name\", \"anthropic\")\n", - " model = _get_model(model_name)\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "```\n", - "\n", - "We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found).\n", - "That means that by default we are using Anthropic as our model provider.\n", - "In this example we will see an example of how to create an example agent that is configured to use OpenAI.\n", - "\n", - "We've also communicated to the graph that it should expect configuration with this key. \n", - "We've done this by passing `config_schema` when constructing the graph, eg:\n", - "\n", - "```python\n", - "class GraphConfig(TypedDict):\n", - " model_name: Literal[\"anthropic\", \"openai\"]\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState, config_schema=GraphConfig)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "f69c9a4f-2ef9-4998-827b-fe86d12bfd76", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9a37bfb5-7331-4004-8054-508838e54f18", - "metadata": {}, - "outputs": [], - "source": [ - "# First, let's check what valid configuration can be\n", - "# We can do this by getting the default assistant\n", - "# There should always be a default assistant with no configuration\n", - "assistants = await client.assistants.search()\n", - "assistants = [a for a in assistants if not a[\"config\"]]\n", - "base_assistant = assistants[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "70193a08-127c-44b3-a102-10db260d7e3b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'model_name': {'title': 'Model Name',\n", - " 'enum': ['anthropic', 'openai'],\n", - " 'type': 'string'}}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# We can now call `.get_schemas` to get schemas associated with this graph\n", - "schemas = await client.assistants.get_schemas(\n", - " assistant_id=base_assistant[\"assistant_id\"]\n", - ")\n", - "# There are multiple types of schemas\n", - "# We can get the `config_schema` to look at the the configurable parameters\n", - "schemas[\"config_schema\"][\"definitions\"][\"Configurable\"][\"properties\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "99be5aee-9a6b-4515-b72f-ba135a893c65", - "metadata": {}, - "outputs": [], - "source": [ - "assistant = await client.assistants.create(\n", - " graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4f10d346-69e6-44f4-8ff0-ef539ba938df", - "metadata": {}, - "source": [ - "We can see that this assistant has saved the config" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "3898ca35-eb2c-4b12-97ea-e0cc6a7c6a2e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': '40a3a2bf-5319-4fae-a2ac-05e075615cdc',\n", - " 'graph_id': 'agent',\n", - " 'config': {'configurable': {'model_name': 'openai'}},\n", - " 'created_at': '2024-06-05T23:12:30.519458+00:00',\n", - " 'updated_at': '2024-06-05T23:12:30.519458+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assistant" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "68ed7a1b-74be-4560-8c55-c76d49d3d348", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "StreamPart(event='metadata', data={'run_id': '1ef23911-c23b-6d8c-b1dc-94bb982ca7b1'})\n", - "StreamPart(event='values', data={'messages': [{'role': 'user', 'content': 'who made you?'}]})\n", - "StreamPart(event='values', data={'messages': [{'content': 'who made you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'ed93c1c9-80d6-4f2b-a048-ef859ea533f9', 'example': False}, {'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-6560cd65-5c9c-434b-8835-0baadc684760', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]})\n", - "StreamPart(event='end', data=None)\n" - ] - } - ], - "source": [ - "thread = await client.threads.create()\n", - "input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n", - "async for event in client.runs.stream(\n", - " thread[\"thread_id\"], assistant[\"assistant_id\"], input=input\n", - "):\n", - " print(event)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "666d78f1-019a-433e-839e-52d2ebb3d9c8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/cron_jobs.ipynb b/examples/cloud_examples/cron_jobs.ipynb deleted file mode 100644 index 98b6a1b73..000000000 --- a/examples/cloud_examples/cron_jobs.ipynb +++ /dev/null @@ -1,132 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Cron Jobs\n", - "\n", - "Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph.\n", - "\n", - "## Setup\n", - "\n", - "First, let's setup our SDK client, assistant, and thread:" - ] - }, - { - "cell_type": "code", - "execution_count": 110, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()\n", - "assistants = await client.assistants.search()\n", - "assistants = [a for a in assistants if not a[\"config\"]]\n", - "assistant = assistants[0]\n", - "thread = await client.threads.create()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cron job on a thread \n", - "\n", - "To create a cron job associated with a specific thread, you can write:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This schedules a job to run at 15:27 (3:27PM) every day\n", - "cron_1 = await client.crons.create_for_thread(\n", - " thread[\"thread_id\"],\n", - " assistant[\"assistant_id\"],\n", - " schedule=\"27 15 * * *\",\n", - " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await client.crons.delete(cron_1[\"cron_id\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cron job stateless\n", - "\n", - "You can also create stateless cron jobs by using the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This schedules a job to run at 15:27 (3:27PM) every day\n", - "cron_2 = await client.crons.create(\n", - " assistant[\"assistant_id\"],\n", - " schedule=\"27 15 * * *\",\n", - " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Again, remember to delete your job once you are done with it!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await client.crons.delete(cron_2[\"cron_id\"])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/cloud_examples/img/webhook_results.png b/examples/cloud_examples/img/webhook_results.png deleted file mode 100644 index 81b9fe703..000000000 Binary files a/examples/cloud_examples/img/webhook_results.png and /dev/null differ diff --git a/examples/cloud_examples/same-thread.ipynb b/examples/cloud_examples/same-thread.ipynb deleted file mode 100644 index 30410566e..000000000 --- a/examples/cloud_examples/same-thread.ipynb +++ /dev/null @@ -1,200 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "68c0837d-c40a-4209-9f88-5d08c00c31b0", - "metadata": {}, - "source": [ - "# How to run multiple agents on the same thread\n", - "\n", - "In LangGraph Cloud, a thread is not explicitly associated with a particular agent.\n", - "This means that you can run multiple agents on the same thread, which allows a different\n", - "agent to continue from an initial agent's progress.\n", - "\n", - "In this example, we will create two agents and then call them both on the same thread.\n", - "You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread\n", - "by the first agent as context." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "e06be1f6-07a5-4e93-8497-02473fc65d4f", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()\n", - "\n", - "openai_assistant = await client.assistants.create(\n", - " graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n", - ")\n", - "\n", - "# There should always be a default assistant with no configuration\n", - "assistants = await client.assistants.search()\n", - "default_assistant = [a for a in assistants if not a[\"config\"]][0]" - ] - }, - { - "cell_type": "markdown", - "id": "4f10d346-69e6-44f4-8ff0-ef539ba938df", - "metadata": {}, - "source": [ - "We can see that these agents are different:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "3898ca35-eb2c-4b12-97ea-e0cc6a7c6a2e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': '13ecc353-a9a9-474b-a824-b6a343cd74b1',\n", - " 'graph_id': 'agent',\n", - " 'config': {'configurable': {'model_name': 'openai'}},\n", - " 'created_at': '2024-05-21T16:22:59.258447+00:00',\n", - " 'updated_at': '2024-05-21T16:22:59.258447+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "openai_assistant" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a8fa67b2-cb4f-43d3-a1fc-f8b3936c16b6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'graph_id': 'agent',\n", - " 'config': {},\n", - " 'created_at': '2024-05-18T00:19:39.688822+00:00',\n", - " 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n", - " 'metadata': {'created_by': 'system'}}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "default_assistant" - ] - }, - { - "cell_type": "markdown", - "id": "5e655e61-c2ee-488a-90f6-6189c84841da", - "metadata": {}, - "source": [ - "We can now run the OpenAI assistant on the thread first." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "68ed7a1b-74be-4560-8c55-c76d49d3d348", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "StreamPart(event='metadata', data={'run_id': 'f90b3029-8669-4d70-976c-b70368e355d8'})\n", - "StreamPart(event='updates', data={'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-9801a5ba-2f3c-43de-89cf-c740debf36fc', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}})\n", - "StreamPart(event='end', data=None)\n" - ] - } - ], - "source": [ - "thread = await client.threads.create()\n", - "input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n", - "async for event in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " openai_assistant[\"assistant_id\"],\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - "):\n", - " print(event)" - ] - }, - { - "cell_type": "markdown", - "id": "c53709e9-ddb2-4429-9042-456eb6c91244", - "metadata": {}, - "source": [ - "Now, we can run it on a second Anthropic-based assistant and see that this second assistant is aware of the initial question, and can answer the question, `and you?`:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "666d78f1-019a-433e-839e-52d2ebb3d9c8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "StreamPart(event='metadata', data={'run_id': 'c3521302-48ae-4c29-a0f2-5eb865cbc6d7'})\n", - "StreamPart(event='updates', data={'agent': {'messages': [{'content': \"I am an AI assistant created by Anthropic to be helpful, harmless, and honest. I don't actually have a physical form or visual representation - I exist as a language model trained to have natural conversations.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-4d05ffd7-0505-43e1-a068-0207c56b7665', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}})\n", - "StreamPart(event='end', data=None)\n" - ] - } - ], - "source": [ - "input = {\"messages\": [{\"role\": \"user\", \"content\": \"and you?\"}]}\n", - "async for event in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " default_assistant[\"assistant_id\"],\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - "):\n", - " print(event)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c26df68-c447-4a88-bc94-59df42b117b5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/stateless_runs.ipynb b/examples/cloud_examples/stateless_runs.ipynb deleted file mode 100644 index 5aa704e35..000000000 --- a/examples/cloud_examples/stateless_runs.ipynb +++ /dev/null @@ -1,152 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Stateless Runs\n", - "\n", - "Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you have your own database to save runs and don't need to use the built in persistent state, you can create stateless runs.\n", - "\n", - "## Setup\n", - "\n", - "First, let's setup our client" - ] - }, - { - "cell_type": "code", - "execution_count": 106, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()\n", - "assistants = await client.assistants.search()\n", - "assistants = [a for a in assistants if not a[\"config\"]]\n", - "assistant = assistants[0]\n", - "thread = await client.threads.create()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stateless streaming\n", - "\n", - "We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`:" - ] - }, - { - "cell_type": "code", - "execution_count": 107, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'agent': {'messages': [{'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" - ] - } - ], - "source": [ - "input = {\n", - " \"messages\": [\n", - " {\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}\n", - " ]\n", - "}\n", - "\n", - "\n", - "async for chunk in client.runs.stream(\n", - " # Don't pass in a thread_id and the stream will be stateless\n", - " None,\n", - " assistant[\"assistant_id\"], # graph_id\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - "):\n", - " if chunk.data and \"run_id\" not in chunk.data:\n", - " print(chunk.data)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Waiting for stateless results\n", - "\n", - "In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 108, - "metadata": {}, - "outputs": [], - "source": [ - "stateless_run_result = await client.runs.wait(\n", - " None,\n", - " assistant[\"assistant_id\"], # graph_id\n", - " input=input,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 109, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'human',\n", - " 'name': None,\n", - " 'id': '5e088543-62c2-43de-9d95-6086ad7f8b48',\n", - " 'example': False},\n", - " {'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.\",\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773',\n", - " 'example': False,\n", - " 'tool_calls': [],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None}]}" - ] - }, - "execution_count": 109, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "stateless_run_result" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/cloud_examples/webhooks.ipynb b/examples/cloud_examples/webhooks.ipynb deleted file mode 100644 index 682bcd8aa..000000000 --- a/examples/cloud_examples/webhooks.ipynb +++ /dev/null @@ -1,72 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Use Webhooks\n", - "\n", - "You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the \"webhook\" parameter.\n", - "\n", - "Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.\n", - "\n", - "The following endpoints accept `webhook` as a parameter: \n", - "\n", - "- Create Run -> POST /thread/{thread_id}/runs\n", - "- Create Thread Cron -> POST /thread/{thread_id}/runs/crons\n", - "- Stream Run -> POST /thread/{thread_id}/runs/stream\n", - "- Wait Run -> POST /thread/{thread_id}/runs/wait\n", - "- Create Cron -> POST /runs/crons\n", - "- Stream Run Stateless -> POST /runs/stream\n", - "- Wait Run Stateless -> POST /runs/wait\n", - "\n", - "The following example uses a url from a public website that allows users to create free webhooks, but you should pass in the webhook that you wish to use. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "curl --request POST \\\n", - " --url http://localhost:8123/threads/b76d1e94-f251-40e3-8933-796d775cdb4c/runs/stream \\\n", - " --header 'Content-Type: application/json' \\\n", - " --data '{\n", - " \"assistant_id\": \"fe096781-5601-53d2-b2f6-0d3403f7e9ca\",\n", - " \"input\" : {\"messages\":[{\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},\n", - " \"metadata\": {},\n", - " \"config\": {\n", - " \"configurable\": {}\n", - " },\n", - " \"multitask_strategy\": \"reject\",\n", - " \"stream_mode\": [\n", - " \"values\"\n", - " ],\n", - " \"webhook\": \"https://webhook.site/6ca33471-dd65-4103-a851-0a252dae0f2a\"\n", - "}'" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To check that this worked as intended, we can go to the website where our webhook was created and confirm that it received a POST request:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Webhook response](./img/webhook_results.png)" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/code_assistant/langgraph_code_assistant.ipynb b/examples/code_assistant/langgraph_code_assistant.ipynb index 2115a370f..1dab64f30 100644 --- a/examples/code_assistant/langgraph_code_assistant.ipynb +++ b/examples/code_assistant/langgraph_code_assistant.ipynb @@ -34,7 +34,9 @@ "id": "e3900420", "metadata": {}, "outputs": [], - "source": ["! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"] + "source": [ + "! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4" + ] }, { "cell_type": "markdown", @@ -52,7 +54,24 @@ "id": "c2eb35d1-4990-47dc-a5c4-208bae588a82", "metadata": {}, "outputs": [], - "source": ["from bs4 import BeautifulSoup as Soup\nfrom langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n\n# LCEL docs\nurl = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\nloader = RecursiveUrlLoader(\n url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n)\ndocs = loader.load()\n\n# Sort the list based on the URLs and get the text\nd_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\nd_reversed = list(reversed(d_sorted))\nconcatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n [doc.page_content for doc in d_reversed]\n)"] + "source": [ + "from bs4 import BeautifulSoup as Soup\n", + "from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n", + "\n", + "# LCEL docs\n", + "url = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\n", + "loader = RecursiveUrlLoader(\n", + " url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n", + ")\n", + "docs = loader.load()\n", + "\n", + "# Sort the list based on the URLs and get the text\n", + "d_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\n", + "d_reversed = list(reversed(d_sorted))\n", + "concatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n", + " [doc.page_content for doc in d_reversed]\n", + ")" + ] }, { "cell_type": "markdown", @@ -74,7 +93,45 @@ "id": "3ba3df70-f6b4-4ea5-a210-e10944960bc6", "metadata": {}, "outputs": [], - "source": ["from langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n### OpenAI\n\n# Grader prompt\ncode_gen_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n question based on the above provided documentation. Ensure any code you provide can be executed \\n \n with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\nexpt_llm = \"gpt-4-0125-preview\"\nllm = ChatOpenAI(temperature=0, model=expt_llm)\ncode_gen_chain = code_gen_prompt | llm.with_structured_output(code)\nquestion = \"How do I build a RAG chain in LCEL?\"\n# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})"] + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "### OpenAI\n", + "\n", + "# Grader prompt\n", + "code_gen_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n", + " Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n", + " question based on the above provided documentation. Ensure any code you provide can be executed \\n \n", + " with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n", + " Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n", + " ),\n", + " (\"placeholder\", \"{messages}\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Data model\n", + "class code(BaseModel):\n", + " \"\"\"Code output\"\"\"\n", + "\n", + " prefix: str = Field(description=\"Description of the problem and approach\")\n", + " imports: str = Field(description=\"Code block import statements\")\n", + " code: str = Field(description=\"Code block not including import statements\")\n", + " description = \"Schema for code solutions to questions about LCEL.\"\n", + "\n", + "\n", + "expt_llm = \"gpt-4-0125-preview\"\n", + "llm = ChatOpenAI(temperature=0, model=expt_llm)\n", + "code_gen_chain = code_gen_prompt | llm.with_structured_output(code)\n", + "question = \"How do I build a RAG chain in LCEL?\"\n", + "# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})" + ] }, { "cell_type": "code", @@ -82,7 +139,118 @@ "id": "cd30b67d-96db-4e51-a540-ae23fcc1f878", "metadata": {}, "outputs": [], - "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Anthropic\n\n# Prompt to enforce tool use\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\" You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n Invoke the code tool to structure the output correctly. \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\n# expt_llm = \"claude-3-haiku-20240307\"\nexpt_llm = \"claude-3-opus-20240229\"\nllm = ChatAnthropic(\n model=expt_llm,\n default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n)\n\nstructured_llm_claude = llm.with_structured_output(code, include_raw=True)\n\n\n# Optional: Check for errors in case tool use is flaky\ndef check_claude_output(tool_output):\n \"\"\"Check for parse error or failure to call the tool\"\"\"\n\n # Error with parsing\n if tool_output[\"parsing_error\"]:\n # Report back output and parsing errors\n print(\"Parsing error!\")\n raw_output = str(tool_output[\"raw\"].content)\n error = tool_output[\"parsing_error\"]\n raise ValueError(\n f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n )\n\n # Tool was not invoked\n elif not tool_output[\"parsed\"]:\n print(\"Failed to invoke tool!\")\n raise ValueError(\n \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n )\n return tool_output\n\n\n# Chain with output check\ncode_chain_claude_raw = (\n code_gen_prompt_claude | structured_llm_claude | check_claude_output\n)\n\n\ndef insert_errors(inputs):\n \"\"\"Insert errors for tool parsing in the messages\"\"\"\n\n # Get errors\n error = inputs[\"error\"]\n messages = inputs[\"messages\"]\n messages += [\n (\n \"assistant\",\n f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n )\n ]\n return {\n \"messages\": messages,\n \"context\": inputs[\"context\"],\n }\n\n\n# This will be run as a fallback chain\nfallback_chain = insert_errors | code_chain_claude_raw\nN = 3 # Max re-tries\ncode_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n fallbacks=[fallback_chain] * N, exception_key=\"error\"\n)\n\n\ndef parse_output(solution):\n \"\"\"When we add 'include_raw=True' to structured output,\n it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n\n return solution[\"parsed\"]\n\n\n# Optional: With re-try to correct for failure to invoke tool\ncode_gen_chain = code_gen_chain_re_try | parse_output\n\n# No re-try\ncode_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output"] + "source": [ + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "### Anthropic\n", + "\n", + "# Prompt to enforce tool use\n", + "code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\" You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n", + " Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n", + " above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n", + " defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n", + " Invoke the code tool to structure the output correctly. \\n Here is the user question:\"\"\",\n", + " ),\n", + " (\"placeholder\", \"{messages}\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Data model\n", + "class code(BaseModel):\n", + " \"\"\"Code output\"\"\"\n", + "\n", + " prefix: str = Field(description=\"Description of the problem and approach\")\n", + " imports: str = Field(description=\"Code block import statements\")\n", + " code: str = Field(description=\"Code block not including import statements\")\n", + " description = \"Schema for code solutions to questions about LCEL.\"\n", + "\n", + "\n", + "# LLM\n", + "# expt_llm = \"claude-3-haiku-20240307\"\n", + "expt_llm = \"claude-3-opus-20240229\"\n", + "llm = ChatAnthropic(\n", + " model=expt_llm,\n", + " default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n", + ")\n", + "\n", + "structured_llm_claude = llm.with_structured_output(code, include_raw=True)\n", + "\n", + "\n", + "# Optional: Check for errors in case tool use is flaky\n", + "def check_claude_output(tool_output):\n", + " \"\"\"Check for parse error or failure to call the tool\"\"\"\n", + "\n", + " # Error with parsing\n", + " if tool_output[\"parsing_error\"]:\n", + " # Report back output and parsing errors\n", + " print(\"Parsing error!\")\n", + " raw_output = str(tool_output[\"raw\"].content)\n", + " error = tool_output[\"parsing_error\"]\n", + " raise ValueError(\n", + " f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n", + " )\n", + "\n", + " # Tool was not invoked\n", + " elif not tool_output[\"parsed\"]:\n", + " print(\"Failed to invoke tool!\")\n", + " raise ValueError(\n", + " \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n", + " )\n", + " return tool_output\n", + "\n", + "\n", + "# Chain with output check\n", + "code_chain_claude_raw = (\n", + " code_gen_prompt_claude | structured_llm_claude | check_claude_output\n", + ")\n", + "\n", + "\n", + "def insert_errors(inputs):\n", + " \"\"\"Insert errors for tool parsing in the messages\"\"\"\n", + "\n", + " # Get errors\n", + " error = inputs[\"error\"]\n", + " messages = inputs[\"messages\"]\n", + " messages += [\n", + " (\n", + " \"assistant\",\n", + " f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n", + " )\n", + " ]\n", + " return {\n", + " \"messages\": messages,\n", + " \"context\": inputs[\"context\"],\n", + " }\n", + "\n", + "\n", + "# This will be run as a fallback chain\n", + "fallback_chain = insert_errors | code_chain_claude_raw\n", + "N = 3 # Max re-tries\n", + "code_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n", + " fallbacks=[fallback_chain] * N, exception_key=\"error\"\n", + ")\n", + "\n", + "\n", + "def parse_output(solution):\n", + " \"\"\"When we add 'include_raw=True' to structured output,\n", + " it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n", + "\n", + " return solution[\"parsed\"]\n", + "\n", + "\n", + "# Optional: With re-try to correct for failure to invoke tool\n", + "code_gen_chain = code_gen_chain_re_try | parse_output\n", + "\n", + "# No re-try\n", + "code_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output" + ] }, { "cell_type": "code", @@ -92,7 +260,14 @@ "scrolled": true }, "outputs": [], - "source": ["# Test\nquestion = \"How do I build a RAG chain in LCEL?\"\nsolution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n)\nsolution"] + "source": [ + "# Test\n", + "question = \"How do I build a RAG chain in LCEL?\"\n", + "solution = code_gen_chain.invoke(\n", + " {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n", + ")\n", + "solution" + ] }, { "cell_type": "markdown", @@ -110,7 +285,26 @@ "id": "c185f1a2-e943-4bed-b833-4243c9c64092", "metadata": {}, "outputs": [], - "source": ["from typing import List, TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: List\n generation: str\n iterations: int"] + "source": [ + "from typing import List, TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " error : Binary flag for control flow to indicate whether test error was tripped\n", + " messages : With user question, error messages, reasoning\n", + " generation : Code solution\n", + " iterations : Number of tries\n", + " \"\"\"\n", + "\n", + " error: str\n", + " messages: List\n", + " generation: str\n", + " iterations: int" + ] }, { "cell_type": "markdown", @@ -128,7 +322,177 @@ "id": "b70e8301-63ae-4f7e-ad8f-c9a052fe3566", "metadata": {}, "outputs": [], - "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameter\n\n# Max tries\nmax_iterations = 3\n# Reflect\n# flag = 'reflect'\nflag = \"do not reflect\"\n\n### Nodes\n\n\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n error = state[\"error\"]\n\n # We have been routed back to generation with an error\n if error == \"yes\":\n messages += [\n (\n \"user\",\n \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n\n # Solution\n code_solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [\n (\n \"assistant\",\n f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n exec(imports + \"\\n\" + code)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\ndef reflect(state: GraphState):\n \"\"\"\n Reflect on errors\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n code_solution = state[\"generation\"]\n\n # Prompt reflection\n\n # Add reflection\n reflections = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\n### Edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n if flag == \"reflect\":\n return \"reflect\"\n else:\n return \"generate\""] + "source": [ + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "### Parameter\n", + "\n", + "# Max tries\n", + "max_iterations = 3\n", + "# Reflect\n", + "# flag = 'reflect'\n", + "flag = \"do not reflect\"\n", + "\n", + "### Nodes\n", + "\n", + "\n", + "def generate(state: GraphState):\n", + " \"\"\"\n", + " Generate a code solution\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation\n", + " \"\"\"\n", + "\n", + " print(\"---GENERATING CODE SOLUTION---\")\n", + "\n", + " # State\n", + " messages = state[\"messages\"]\n", + " iterations = state[\"iterations\"]\n", + " error = state[\"error\"]\n", + "\n", + " # We have been routed back to generation with an error\n", + " if error == \"yes\":\n", + " messages += [\n", + " (\n", + " \"user\",\n", + " \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n", + " )\n", + " ]\n", + "\n", + " # Solution\n", + " code_solution = code_gen_chain.invoke(\n", + " {\"context\": concatenated_content, \"messages\": messages}\n", + " )\n", + " messages += [\n", + " (\n", + " \"assistant\",\n", + " f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n", + " )\n", + " ]\n", + "\n", + " # Increment\n", + " iterations = iterations + 1\n", + " return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n", + "\n", + "\n", + "def code_check(state: GraphState):\n", + " \"\"\"\n", + " Check code\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, error\n", + " \"\"\"\n", + "\n", + " print(\"---CHECKING CODE---\")\n", + "\n", + " # State\n", + " messages = state[\"messages\"]\n", + " code_solution = state[\"generation\"]\n", + " iterations = state[\"iterations\"]\n", + "\n", + " # Get solution components\n", + " imports = code_solution.imports\n", + " code = code_solution.code\n", + "\n", + " # Check imports\n", + " try:\n", + " exec(imports)\n", + " except Exception as e:\n", + " print(\"---CODE IMPORT CHECK: FAILED---\")\n", + " error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n", + " messages += error_message\n", + " return {\n", + " \"generation\": code_solution,\n", + " \"messages\": messages,\n", + " \"iterations\": iterations,\n", + " \"error\": \"yes\",\n", + " }\n", + "\n", + " # Check execution\n", + " try:\n", + " exec(imports + \"\\n\" + code)\n", + " except Exception as e:\n", + " print(\"---CODE BLOCK CHECK: FAILED---\")\n", + " error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n", + " messages += error_message\n", + " return {\n", + " \"generation\": code_solution,\n", + " \"messages\": messages,\n", + " \"iterations\": iterations,\n", + " \"error\": \"yes\",\n", + " }\n", + "\n", + " # No errors\n", + " print(\"---NO CODE TEST FAILURES---\")\n", + " return {\n", + " \"generation\": code_solution,\n", + " \"messages\": messages,\n", + " \"iterations\": iterations,\n", + " \"error\": \"no\",\n", + " }\n", + "\n", + "\n", + "def reflect(state: GraphState):\n", + " \"\"\"\n", + " Reflect on errors\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation\n", + " \"\"\"\n", + "\n", + " print(\"---GENERATING CODE SOLUTION---\")\n", + "\n", + " # State\n", + " messages = state[\"messages\"]\n", + " iterations = state[\"iterations\"]\n", + " code_solution = state[\"generation\"]\n", + "\n", + " # Prompt reflection\n", + "\n", + " # Add reflection\n", + " reflections = code_gen_chain.invoke(\n", + " {\"context\": concatenated_content, \"messages\": messages}\n", + " )\n", + " messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n", + " return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n", + "\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def decide_to_finish(state: GraphState):\n", + " \"\"\"\n", + " Determines whether to finish.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Next node to call\n", + " \"\"\"\n", + " error = state[\"error\"]\n", + " iterations = state[\"iterations\"]\n", + "\n", + " if error == \"no\" or iterations == max_iterations:\n", + " print(\"---DECISION: FINISH---\")\n", + " return \"end\"\n", + " else:\n", + " print(\"---DECISION: RE-TRY SOLUTION---\")\n", + " if flag == \"reflect\":\n", + " return \"reflect\"\n", + " else:\n", + " return \"generate\"" + ] }, { "cell_type": "code", @@ -136,7 +500,31 @@ "id": "f66b4e00-4731-42c8-bc38-72dd0ff7c92c", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"generate\", generate) # generation solution\nworkflow.add_node(\"check_code\", code_check) # check code\nworkflow.add_node(\"reflect\", reflect) # reflect\n\n# Build graph\nworkflow.add_edge(START, \"generate\")\nworkflow.add_edge(\"generate\", \"check_code\")\nworkflow.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"reflect\": \"reflect\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"reflect\", \"generate\")\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "workflow = StateGraph(GraphState)\n", + "\n", + "# Define the nodes\n", + "workflow.add_node(\"generate\", generate) # generation solution\n", + "workflow.add_node(\"check_code\", code_check) # check code\n", + "workflow.add_node(\"reflect\", reflect) # reflect\n", + "\n", + "# Build graph\n", + "workflow.add_edge(START, \"generate\")\n", + "workflow.add_edge(\"generate\", \"check_code\")\n", + "workflow.add_conditional_edges(\n", + " \"check_code\",\n", + " decide_to_finish,\n", + " {\n", + " \"end\": END,\n", + " \"reflect\": \"reflect\",\n", + " \"generate\": \"generate\",\n", + " },\n", + ")\n", + "workflow.add_edge(\"reflect\", \"generate\")\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", @@ -144,7 +532,10 @@ "id": "9bcaafe4-ddcf-4fab-8620-2d9b6c508f98", "metadata": {}, "outputs": [], - "source": ["question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\napp.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})"] + "source": [ + "question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n", + "app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})" + ] }, { "cell_type": "markdown", @@ -172,7 +563,11 @@ "id": "678e8954-56b5-4cc6-be26-f7f2a060b242", "metadata": {}, "outputs": [], - "source": ["import langsmith\n\nclient = langsmith.Client()"] + "source": [ + "import langsmith\n", + "\n", + "client = langsmith.Client()" + ] }, { "cell_type": "code", @@ -180,7 +575,13 @@ "id": "ef7cf662-7a6f-4dee-965c-6309d4045feb", "metadata": {}, "outputs": [], - "source": ["# Clone the dataset to your tenant to use it\npublic_dataset = (\n \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n)\nclient.clone_public_dataset(public_dataset)"] + "source": [ + "# Clone the dataset to your tenant to use it\n", + "public_dataset = (\n", + " \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n", + ")\n", + "client.clone_public_dataset(public_dataset)" + ] }, { "cell_type": "markdown", @@ -196,7 +597,28 @@ "id": "455a34ea-52cb-4ae5-9f4a-7e4a08cd0c09", "metadata": {}, "outputs": [], - "source": ["from langsmith.schemas import Example, Run\n\n\ndef check_import(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n try:\n exec(imports)\n return {\"key\": \"import_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"import_check\", \"score\": 0}\n\n\ndef check_execution(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n code = run.outputs.get(\"code\")\n try:\n exec(imports + \"\\n\" + code)\n return {\"key\": \"code_execution_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"code_execution_check\", \"score\": 0}"] + "source": [ + "from langsmith.schemas import Example, Run\n", + "\n", + "\n", + "def check_import(run: Run, example: Example) -> dict:\n", + " imports = run.outputs.get(\"imports\")\n", + " try:\n", + " exec(imports)\n", + " return {\"key\": \"import_check\", \"score\": 1}\n", + " except Exception:\n", + " return {\"key\": \"import_check\", \"score\": 0}\n", + "\n", + "\n", + "def check_execution(run: Run, example: Example) -> dict:\n", + " imports = run.outputs.get(\"imports\")\n", + " code = run.outputs.get(\"code\")\n", + " try:\n", + " exec(imports + \"\\n\" + code)\n", + " return {\"key\": \"code_execution_check\", \"score\": 1}\n", + " except Exception:\n", + " return {\"key\": \"code_execution_check\", \"score\": 0}" + ] }, { "cell_type": "markdown", @@ -212,7 +634,22 @@ "id": "c8fa6bcb-b245-4422-b79a-582cd8a7d7ea", "metadata": {}, "outputs": [], - "source": ["def predict_base_case(example: dict):\n \"\"\"Context stuffing\"\"\"\n solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n )\n solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n\n\ndef predict_langgraph(example: dict):\n \"\"\"LangGraph\"\"\"\n graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n solution = graph[\"generation\"]\n return {\"imports\": solution.imports, \"code\": solution.code}"] + "source": [ + "def predict_base_case(example: dict):\n", + " \"\"\"Context stuffing\"\"\"\n", + " solution = code_gen_chain.invoke(\n", + " {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n", + " )\n", + " solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n", + " return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n", + "\n", + "\n", + "def predict_langgraph(example: dict):\n", + " \"\"\"LangGraph\"\"\"\n", + " graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n", + " solution = graph[\"generation\"]\n", + " return {\"imports\": solution.imports, \"code\": solution.code}" + ] }, { "cell_type": "code", @@ -220,7 +657,15 @@ "id": "d9c57468-97f6-47d6-a5e9-c09b53bfdd83", "metadata": {}, "outputs": [], - "source": ["from langsmith.evaluation import evaluate\n\n# Evaluator\ncode_evalulator = [check_import, check_execution]\n\n# Dataset\ndataset_name = \"test-LCEL-code-gen\""] + "source": [ + "from langsmith.evaluation import evaluate\n", + "\n", + "# Evaluator\n", + "code_evalulator = [check_import, check_execution]\n", + "\n", + "# Dataset\n", + "dataset_name = \"test-LCEL-code-gen\"" + ] }, { "cell_type": "code", @@ -228,7 +673,19 @@ "id": "2dacccf0-d73f-4017-aaf0-9806ffe5bd2c", "metadata": {}, "outputs": [], - "source": ["# Run base case\nexperiment_results_ = evaluate(\n predict_base_case,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n },\n)"] + "source": [ + "# Run base case\n", + "experiment_results_ = evaluate(\n", + " predict_base_case,\n", + " data=dataset_name,\n", + " evaluators=code_evalulator,\n", + " experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n", + " max_concurrency=2,\n", + " metadata={\n", + " \"llm\": expt_llm,\n", + " },\n", + ")" + ] }, { "cell_type": "code", @@ -236,7 +693,20 @@ "id": "71d90f9e-9dad-410c-a709-093d275029ae", "metadata": {}, "outputs": [], - "source": ["# Run with langgraph\nexperiment_results = evaluate(\n predict_langgraph,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n \"feedback\": flag,\n },\n)"] + "source": [ + "# Run with langgraph\n", + "experiment_results = evaluate(\n", + " predict_langgraph,\n", + " data=dataset_name,\n", + " evaluators=code_evalulator,\n", + " experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n", + " max_concurrency=2,\n", + " metadata={\n", + " \"llm\": expt_llm,\n", + " \"feedback\": flag,\n", + " },\n", + ")" + ] }, { "cell_type": "markdown", @@ -251,14 +721,6 @@ "\n", "https://smith.langchain.com/public/78a3d858-c811-4e46-91cb-0f10ef56260b/d" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a42333c3-c098-4576-ae2a-0258de64ece2", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/configuration.ipynb b/examples/configuration.ipynb index 4a2817c21..80f6ba25f 100644 --- a/examples/configuration.ipynb +++ b/examples/configuration.ipynb @@ -24,15 +24,43 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "id": "816523d0-0b59-47cf-9f4c-4838024efe22", "metadata": {}, "outputs": [], - "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.messages import BaseMessage, HumanMessage\n\nfrom langgraph.graph import END, StateGraph, START\n\nmodel = ChatAnthropic(model_name=\"claude-2.1\")\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n\n\ndef _call_model(state):\n response = model.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "import operator\n", + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-2.1\")\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + "\n", + "\n", + "def _call_model(state):\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "id": "070f11a6-2441-4db5-9df6-e318f110e281", "metadata": {}, "outputs": [ @@ -40,15 +68,17 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01YZj7CVCUSc76faX4VM9i5d', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-d343db34-598c-46a2-93d6-ffa886d9b264-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_012SakNGNitBcKJgc9yZ1Asv', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-9e375cd7-ae84-4db2-981c-c7e18ecabddf-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 8, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "markdown", @@ -58,17 +88,44 @@ "## Configure the graph\n", "\n", "Great! Now let's suppose that we want to extend this example so the user is able to choose from multiple llms.\n", - "We can easily do that by passing in a config.\n", + "We can easily do that by passing in a config. Any configuration information needs to be passed inside `configurable` key as shown below.\n", "This config is meant to contain things are not part of the input (and therefore that we don't want to track as part of the state)." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "id": "c01f1e7c-8e8b-4e26-98f7-56ac225077b4", "metadata": {}, "outputs": [], - "source": ["from langchain_openai import ChatOpenAI\n\nopenai_model = ChatOpenAI()\n\nmodels = {\n \"anthropic\": model,\n \"openai\": openai_model,\n}\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n response = m.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from typing import Optional\n", + "from langchain_core.runnables.config import RunnableConfig\n", + "\n", + "openai_model = ChatOpenAI()\n", + "\n", + "models = {\n", + " \"anthropic\": model,\n", + " \"openai\": openai_model,\n", + "}\n", + "\n", + "def _call_model(state: AgentState, config: RunnableConfig):\n", + " # Access the config through the configurable key\n", + " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", + " model = models[model_name]\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "markdown", @@ -80,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "id": "ef50f048-fc43-40c0-b713-346408fcf052", "metadata": {}, "outputs": [ @@ -88,15 +145,17 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01EedReFyXmonWXPKhYre7Jb', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-1c6feaa0-bd6f-433a-8264-209d72c85db7-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_0133PAX5DyoUYL1gZiGR8NXs', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-03e8bd8b-fa09-4258-920d-8f53a7b91fcc-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 12, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "markdown", @@ -108,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "id": "f2f7c74b-9fb0-41c6-9728-dcf9d8a3c397", "metadata": {}, "outputs": [ @@ -116,15 +175,18 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'stop', 'logprobs': None}, id='run-d41ffb62-e164-45a1-862c-d288c6ad100a-0')]}" + " AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-6d0c7c25-03de-49d6-b3be-ff0858d17122-0', usage_metadata={'input_tokens': 8, 'output_tokens': 9, 'total_tokens': 17})]}" ] }, - "execution_count": 13, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], - "source": ["config = {\"configurable\": {\"model\": \"openai\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] + "source": [ + "config = {\"configurable\": {\"model\": \"openai\"}}\n", + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" + ] }, { "cell_type": "markdown", @@ -136,15 +198,44 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 9, "id": "f0393a43-9fbe-4056-972f-3e91ea329041", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import SystemMessage\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n messages = state[\"messages\"]\n if \"system_message\" in config[\"configurable\"]:\n messages = [\n SystemMessage(content=config[\"configurable\"][\"system_message\"])\n ] + messages\n response = m.invoke(messages)\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "from langchain_core.messages import SystemMessage\n", + "\n", + "# We can define a config schema to specify the configuration options for the graph\n", + "# A config schema is useful for indicating which fields are available in the configurable dict inside the config\n", + "class ConfigSchema(TypedDict):\n", + " model: Optional[str]\n", + " system_message: Optional[str]\n", + "\n", + "def _call_model(state: AgentState, config: RunnableConfig):\n", + " # Access the config through the configurable key\n", + " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", + " model = models[model_name]\n", + " messages = state[\"messages\"]\n", + " if \"system_message\" in config[\"configurable\"]:\n", + " messages = [\n", + " SystemMessage(content=config[\"configurable\"][\"system_message\"])\n", + " ] + messages\n", + " response = model.invoke(messages)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph - note that we pass in the configuration schema here, but it is not necessary\n", + "workflow = StateGraph(AgentState, ConfigSchema)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 10, "id": "718685f7-4cdd-4181-9fc8-e7762d584727", "metadata": {}, "outputs": [ @@ -152,19 +243,21 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01Ts56eVLSrUbzVMbzLnXc3M', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-f75a4389-b72e-4d47-8f3e-bedc6a060f66-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_01TVJvxCXsCT9JVe7A4iUUi9', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-627eb685-c4d7-481d-9095-c0a1822e8c10-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 19, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 11, "id": "e043a719-f197-46ef-9d45-84740a39aeb0", "metadata": {}, "outputs": [ @@ -172,23 +265,18 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Ciao!', response_metadata={'id': 'msg_01RzFCii8WhbbkFm16nUquxk', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-9492f0e4-f223-41c2-81a6-6f0cb6a14fe6-0')]}" + " AIMessage(content='Ciao!', response_metadata={'id': 'msg_01CpBD1cMCYvvPX2cogUawJj', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-6ef2fea6-9bfa-4266-bd05-263160a1db7b-0', usage_metadata={'input_tokens': 14, 'output_tokens': 7, 'total_tokens': 21})]}" ] }, - "execution_count": 20, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], - "source": ["config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5c5f7f4-4b0e-4cde-93a6-c1c6329b8591", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\n", + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" + ] } ], "metadata": { @@ -207,7 +295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/create-react-agent-hitl.ipynb b/examples/create-react-agent-hitl.ipynb index 39392f205..235f2b057 100644 --- a/examples/create-react-agent-hitl.ipynb +++ b/examples/create-react-agent-hitl.ipynb @@ -73,7 +73,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "7a154152-973e-4b5d-aa13-48c617744a4c", "metadata": {}, "outputs": [], @@ -92,14 +92,14 @@ "\n", "\n", "@tool\n", - "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", - " \"\"\"Use this to get weather information.\"\"\"\n", - " if city == \"nyc\":\n", + "def get_weather(location: str):\n", + " \"\"\"Use this to get weather information from a given location.\"\"\"\n", + " if location.lower() in [\"nyc\", \"new york\"]:\n", " return \"It might be cloudy in nyc\"\n", - " elif city == \"sf\":\n", + " elif location.lower() in [\"sf\", \"san francisco\"]:\n", " return \"It's always sunny in sf\"\n", " else:\n", - " raise AssertionError(\"Unknown city\")\n", + " raise AssertionError(\"Unknown Location\")\n", "\n", "\n", "tools = [get_weather]\n", @@ -144,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6", "metadata": {}, "outputs": [ @@ -154,26 +154,35 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "What's the weather in SF?\n", + "what is the weather in SF?\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "Tool Calls:\n", - " get_weather (call_0OMmuTLec9t8kxMVkllZCSxo)\n", - " Call ID: call_0OMmuTLec9t8kxMVkllZCSxo\n", + " get_weather (call_TcDfLuoCKLmQ7eG71SedxLZ6)\n", + " Call ID: call_TcDfLuoCKLmQ7eG71SedxLZ6\n", " Args:\n", - " city: sf\n" + " location: San Francisco, CA\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", "config = {\"configurable\": {\"thread_id\": \"42\"}}\n", - "inputs = {\"messages\": [(\"user\", \"What's the weather in SF?\")]}\n", + "inputs = {\"messages\": [(\"user\", \"what is the weather in SF?\")]}\n", "\n", "print_stream(graph.stream(inputs, config, stream_mode=\"values\"))" ] }, + { + "cell_type": "markdown", + "id": "ca40a719", + "metadata": {}, + "source": [ + "We can verify that our graph stopped at the right place:" + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", "metadata": {}, "outputs": [ @@ -190,9 +199,87 @@ "print(\"Next step: \", snapshot.next)" ] }, + { + "cell_type": "markdown", + "id": "7de6ca78", + "metadata": {}, + "source": [ + "Now we can either approve or edit the tool call before proceeding to the next node. If we wanted to approve the tool call, we would simply continue streaming the graph with `None` input. If we wanted to edit the tool call we need to update the state to have the correct tool call, and then after the update has been applied we can continue.\n", + "\n", + "We can try resuming and we will see an error arise:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "740bbaeb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: get_weather\n", + "\n", + "Error: AssertionError('Unknown Location')\n", + " Please fix your mistakes.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "It seems there was an issue with the location provided. Let's try specifying \"San Francisco, California\" more clearly.\n", + "Tool Calls:\n", + " get_weather (call_TZm9HCShGNEreglVJcmUdXqG)\n", + " Call ID: call_TZm9HCShGNEreglVJcmUdXqG\n", + " Args:\n", + " location: San Francisco, California\n" + ] + } + ], + "source": [ + "print_stream(graph.stream(None, config, stream_mode=\"values\"))" + ] + }, + { + "cell_type": "markdown", + "id": "c1cf5950", + "metadata": {}, + "source": [ + "This error arose because our tool argument of \"San Francisco, CA\" is not a location our tool recognizes.\n", + "\n", + "Let's show how we would edit the tool call to search for \"San Francisco\" instead of \"San Francisco, CA\" - since our tool as written treats \"San Francisco, CA\" as an unknown location. We will update the state and then resume streaming the graph and should see no errors arise:" + ] + }, { "cell_type": "code", "execution_count": 6, + "id": "1c81ed9f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '42',\n", + " 'checkpoint_ns': '',\n", + " 'checkpoint_id': '1ef66368-9772-67ea-8004-07c779869a0a'}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config)\n", + "\n", + "last_message = state.values['messages'][-1]\n", + "last_message.tool_calls[0]['args'] = {\"location\": \"San Francisco\"}\n", + "\n", + "graph.update_state(config, {\"messages\": [ last_message]})" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "83148e08-63e8-49e5-a08b-02dc907bed1d", "metadata": {}, "outputs": [ @@ -206,7 +293,7 @@ "It's always sunny in sf\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "The weather in San Francisco is currently sunny.\n" + "The weather in San Francisco is currently sunny. Enjoy the sunshine!\n" ] } ], @@ -215,12 +302,12 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "id": "6f6f8965-b016-4e25-be63-31c00fc0a6de", + "cell_type": "markdown", + "id": "8202a5f9", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "Fantastic! Our graph updated properly to query the weather in San Francisco and got the correct \"It's always sunny in sf\" response from the tool, and then responded to the user accordingly." + ] } ], "metadata": { @@ -239,7 +326,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/create-react-agent-memory.ipynb b/examples/create-react-agent-memory.ipynb index 2611bda69..fbafb7c91 100644 --- a/examples/create-react-agent-memory.ipynb +++ b/examples/create-react-agent-memory.ipynb @@ -221,14 +221,6 @@ "inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n", "print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/create-react-agent-system-prompt.ipynb b/examples/create-react-agent-system-prompt.ipynb index c258a74c5..b3bd0213e 100644 --- a/examples/create-react-agent-system-prompt.ipynb +++ b/examples/create-react-agent-system-prompt.ipynb @@ -173,14 +173,6 @@ "\n", "print_stream(graph.stream(inputs, stream_mode=\"values\"))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/customer-support/customer-support.ipynb b/examples/customer-support/customer-support.ipynb index cf451188f..28c107273 100644 --- a/examples/customer-support/customer-support.ipynb +++ b/examples/customer-support/customer-support.ipynb @@ -99,46 +99,50 @@ " # Backup - we will use this to \"reset\" our DB in each section\n", " shutil.copy(local_file, backup_file)\n", "# Convert the flights to present time for our tutorial\n", - "conn = sqlite3.connect(local_file)\n", - "cursor = conn.cursor()\n", + "def update_dates(file):\n", + " shutil.copy(backup_file, file)\n", + " conn = sqlite3.connect(file)\n", + " cursor = conn.cursor()\n", "\n", - "tables = pd.read_sql(\n", - " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n", - ").name.tolist()\n", - "tdf = {}\n", - "for t in tables:\n", - " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n", + " tables = pd.read_sql(\n", + " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n", + " ).name.tolist()\n", + " tdf = {}\n", + " for t in tables:\n", + " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n", "\n", - "example_time = pd.to_datetime(\n", - " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n", - ").max()\n", - "current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n", - "time_diff = current_time - example_time\n", + " example_time = pd.to_datetime(\n", + " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n", + " ).max()\n", + " current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n", + " time_diff = current_time - example_time\n", "\n", - "tdf[\"bookings\"][\"book_date\"] = (\n", - " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n", - " + time_diff\n", - ")\n", - "\n", - "datetime_columns = [\n", - " \"scheduled_departure\",\n", - " \"scheduled_arrival\",\n", - " \"actual_departure\",\n", - " \"actual_arrival\",\n", - "]\n", - "for column in datetime_columns:\n", - " tdf[\"flights\"][column] = (\n", - " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n", + " tdf[\"bookings\"][\"book_date\"] = (\n", + " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n", + " + time_diff\n", " )\n", "\n", - "for table_name, df in tdf.items():\n", - " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n", - "del df\n", - "del tdf\n", - "conn.commit()\n", - "conn.close()\n", + " datetime_columns = [\n", + " \"scheduled_departure\",\n", + " \"scheduled_arrival\",\n", + " \"actual_departure\",\n", + " \"actual_arrival\",\n", + " ]\n", + " for column in datetime_columns:\n", + " tdf[\"flights\"][column] = (\n", + " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n", + " )\n", "\n", - "db = local_file # We'll be using this local file as our DB in this tutorial" + " for table_name, df in tdf.items():\n", + " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n", + " del df\n", + " del tdf\n", + " conn.commit()\n", + " conn.close()\n", + "\n", + " return file\n", + "\n", + "db = update_dates(local_file)" ] }, { @@ -225,7 +229,14 @@ "\n", "Define the (`fetch_user_flight_information`) tool to let the agent see the current user's flight information. Then define tools to search for flights and manage the passenger's bookings stored in the SQL database.\n", "\n", - "We use `ensure_config` to pass in the `passenger_id` in via configurable parameters. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information." + "We the can [access the RunnableConfig](https://python.langchain.com/v0.2/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n", + "\n", + "
\n", + "

Compatibility

\n", + "

\n", + " This tutorial expects `langchain-core>=0.2.16` to use the injected RunnableConfig. Prior to that, you'd use `ensure_config` to collect the config from context.\n", + "

\n", + "
\n" ] }, { @@ -240,18 +251,17 @@ "from typing import Optional\n", "\n", "import pytz\n", - "from langchain_core.runnables import ensure_config\n", + "from langchain_core.runnables import RunnableConfig\n", "\n", "\n", "@tool\n", - "def fetch_user_flight_information() -> list[dict]:\n", + "def fetch_user_flight_information(config: RunnableConfig) -> list[dict]:\n", " \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n", "\n", " Returns:\n", " A list of dictionaries where each dictionary contains the ticket details,\n", " associated flight details, and the seat assignments for each ticket belonging to the user.\n", " \"\"\"\n", - " config = ensure_config() # Fetch from the context\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -328,9 +338,10 @@ "\n", "\n", "@tool\n", - "def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n", + "def update_ticket_to_new_flight(\n", + " ticket_no: str, new_flight_id: int, *, config: RunnableConfig\n", + ") -> str:\n", " \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n", - " config = ensure_config()\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -396,9 +407,8 @@ "\n", "\n", "@tool\n", - "def cancel_ticket(ticket_no: str) -> str:\n", + "def cancel_ticket(ticket_no: str, *, config: RunnableConfig) -> str:\n", " \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n", - " config = ensure_config()\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -1744,7 +1754,7 @@ "]\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -2298,7 +2308,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -2902,7 +2912,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -4324,7 +4334,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -4407,7 +4417,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/examples/docs/quickstart.ipynb b/examples/docs/quickstart.ipynb index e8d621a80..add27212f 100644 --- a/examples/docs/quickstart.ipynb +++ b/examples/docs/quickstart.ipynb @@ -12,30 +12,55 @@ "execution_count": 13, "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-openai"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain-openai" + ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 1, "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\nif not os.environ.get(\"OPENAI_API_KEY\"):\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 2, "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, MessageGraph\n\nmodel = ChatOpenAI(temperature=0)\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model)\ngraph.add_edge(\"oracle\", END)\n\ngraph.add_edge(START, \"oracle\")\n\nrunnable = graph.compile()"] + "source": [ + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import START, END, MessageGraph\n", + "\n", + "model = ChatOpenAI(temperature=0)\n", + "\n", + "graph = MessageGraph()\n", + "\n", + "graph.add_node(\"oracle\", model)\n", + "graph.add_edge(\"oracle\", END)\n", + "\n", + "graph.add_edge(START, \"oracle\")\n", + "\n", + "runnable = graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -44,42 +69,90 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is 1 + 1?', id='bb29f237-0e4d-4354-92e1-d46434c67fe7'),\n", - " AIMessage(content='1 + 1 equals 2.', response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 15, 'total_tokens': 23}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-2ff0112a-9402-44a1-a992-c44fb49fa894-0', usage_metadata={'input_tokens': 15, 'output_tokens': 8, 'total_tokens': 23})]" + "[HumanMessage(content='What is 1 + 1?', id='28f82989-8a35-4c1e-b12d-aa1b54c2b5ea'),\n", + " AIMessage(content='1 + 1 equals 2.', response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 15, 'total_tokens': 23}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-aebd1367-b64d-4c25-971e-db7c88d55aac-0', usage_metadata={'input_tokens': 15, 'output_tokens': 8, 'total_tokens': 23})]" ] }, - "execution_count": 17, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is 1 + 1?\"))"] + "source": [ + "runnable.invoke(HumanMessage(\"What is 1 + 1?\"))" + ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 5, "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\nfrom langchain_core.tools import tool\n\nfrom langgraph.graph import END, START\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef multiply(first_number: int, second_number: int):\n \"\"\"Multiplies two numbers together.\"\"\"\n return first_number * second_number\n\n\nmodel = ChatOpenAI(temperature=0)\nmodel_with_tools = model.bind_tools(tools=[multiply])\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model_with_tools)\n\ntool_node = ToolNode([multiply])\ngraph.add_node(\"multiply\", tool_node)\ngraph.add_edge(START, \"oracle\")\ngraph.add_edge(\"multiply\", END)\n\n\ndef router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n if len(tool_calls):\n return \"multiply\"\n else:\n return END\n\n\ngraph.add_conditional_edges(\"oracle\", router)\nrunnable = graph.compile()"] + "source": [ + "from typing import Literal\n", + "\n", + "from langchain_core.tools import tool\n", + "\n", + "from langgraph.graph import END, START\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_number: int, second_number: int):\n", + " \"\"\"Multiplies two numbers together.\"\"\"\n", + " return first_number * second_number\n", + "\n", + "\n", + "model = ChatOpenAI(temperature=0)\n", + "model_with_tools = model.bind_tools(tools=[multiply])\n", + "\n", + "graph = MessageGraph()\n", + "\n", + "graph.add_node(\"oracle\", model_with_tools)\n", + "\n", + "tool_node = ToolNode([multiply])\n", + "graph.add_node(\"multiply\", tool_node)\n", + "graph.add_edge(START, \"oracle\")\n", + "graph.add_edge(\"multiply\", END)\n", + "\n", + "\n", + "def router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n", + " tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n", + " if len(tool_calls):\n", + " return \"multiply\"\n", + " else:\n", + " return END\n", + "\n", + "\n", + "graph.add_conditional_edges(\"oracle\", router)\n", + "runnable = graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -88,53 +161,56 @@ "output_type": "display_data" } ], - "source": ["try:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "try:\n", + " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is 123 * 456?', id='fa2dbb36-c61b-4ce1-892d-c08f3e741035'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ZpoO6ClLFKkppN9Y8GEelZH1', 'function': {'arguments': '{\"first_number\":123,\"second_number\":456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 57, 'total_tokens': 76}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-ee9faec5-3526-45d5-89b1-5292755a6893-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_ZpoO6ClLFKkppN9Y8GEelZH1'}], usage_metadata={'input_tokens': 57, 'output_tokens': 19, 'total_tokens': 76}),\n", - " ToolMessage(content='56088', name='multiply', id='b9992170-ca76-4256-8560-29b329c1b56e', tool_call_id='call_ZpoO6ClLFKkppN9Y8GEelZH1')]" + "[HumanMessage(content='What is 123 * 456?', id='81692a54-acd4-4ef7-9ccf-49b2efc0b9b1'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_XneG8vpjfal3lKO4q4fmfUYc', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2fe18a05-45bf-4cf6-ba29-c5956fc928bf-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_XneG8vpjfal3lKO4q4fmfUYc', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 34, 'total_tokens': 91}),\n", + " ToolMessage(content='56088', name='multiply', id='d494f0c9-daa8-4da2-aa4b-e9d4ace87912', tool_call_id='call_XneG8vpjfal3lKO4q4fmfUYc')]" ] }, - "execution_count": 20, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is 123 * 456?\"))"] + "source": [ + "runnable.invoke(HumanMessage(\"What is 123 * 456?\"))" + ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is your name?', id='09f03ac4-ca68-4464-9ec3-2c393699b3bb'),\n", - " AIMessage(content='My name is Assistant. How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 54, 'total_tokens': 67}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-a21b2f58-3fa6-428f-99e5-9c4e071a9319-0', usage_metadata={'input_tokens': 54, 'output_tokens': 13, 'total_tokens': 67})]" + "[HumanMessage(content='What is your name?', id='184ed583-58f1-4d4c-a428-d573b5a56286'),\n", + " AIMessage(content='My name is Assistant. How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 54, 'total_tokens': 67}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-38dcd15d-8bdf-491a-b7ca-771bb64bb824-0', usage_metadata={'input_tokens': 54, 'output_tokens': 13, 'total_tokens': 67})]" ] }, - "execution_count": 21, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is your name?\"))"] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "runnable.invoke(HumanMessage(\"What is your name?\"))" + ] } ], "metadata": { diff --git a/examples/extraction/retries.ipynb b/examples/extraction/retries.ipynb index 3e0dcce31..4ebf0c913 100644 --- a/examples/extraction/retries.ipynb +++ b/examples/extraction/retries.ipynb @@ -1010,14 +1010,6 @@ "\n", "If you notice high retry rates (using an observability tool like LangSmith), you can set up a rule to send the failure cases to a dataset alongside the corrected values and then automatically program those into your prompts or schemas (or use them as few-shots to have semantically relevant demonstrations)." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0ae295b1-da58-4cc9-834b-70e1466f8695", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/human_in_the_loop/dynamic_breakpoints.ipynb b/examples/human_in_the_loop/dynamic_breakpoints.ipynb new file mode 100644 index 000000000..f43049d28 --- /dev/null +++ b/examples/human_in_the_loop/dynamic_breakpoints.ipynb @@ -0,0 +1,417 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ee54cde3-7e4d-43f4-b921-e7141ea0f19e", + "metadata": {}, + "source": [ + "# How to add dynamic breakpoints" + ] + }, + { + "cell_type": "markdown", + "id": "607849c6-4b8c-4e06-ad9c-758bb5a08e86", + "metadata": {}, + "source": [ + "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).\n", + "\n", + "In LangGraph you can add breakpoints before / after a node is executed. But oftentimes it may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. When doing so, it may also be helpful to include information about **why** that interrupt was raised.\n", + "\n", + "This guide shows how you can dynamically interrupt the graph using `NodeInterupt` -- a special exception that can be raised from inside a node. Let's see it in action!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2013d058-c245-498e-ba05-5af99b9b8a1b", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U langgraph" + ] + }, + { + "cell_type": "markdown", + "id": "e9aa244f-1dd9-450e-9526-b1a28b30f84f", + "metadata": {}, + "source": [ + "### Define the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9a14c8b2-5c25-4201-93ea-e5358ee99bcb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from typing import TypedDict\n", + "from IPython.display import Image, display\n", + "\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.errors import NodeInterrupt\n", + "\n", + "\n", + "class State(TypedDict):\n", + " input: str\n", + "\n", + "\n", + "def step_1(state: State) -> State:\n", + " print(\"---Step 1---\")\n", + " return state\n", + "\n", + "\n", + "def step_2(state: State) -> State:\n", + " # Let's optionally raise a NodeInterrupt\n", + " # if the length of the input is longer than 5 characters\n", + " if len(state['input']) > 5:\n", + " raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n", + " \n", + " print(\"---Step 2---\")\n", + " return state\n", + "\n", + "def step_3(state: State) -> State:\n", + " print(\"---Step 3---\")\n", + " return state\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"step_1\", step_1)\n", + "builder.add_node(\"step_2\", step_2)\n", + "builder.add_node(\"step_3\", step_3)\n", + "builder.add_edge(START, \"step_1\")\n", + "builder.add_edge(\"step_1\", \"step_2\")\n", + "builder.add_edge(\"step_2\", \"step_3\")\n", + "builder.add_edge(\"step_3\", END)\n", + "\n", + "# Set up memory\n", + "memory = MemorySaver()\n", + "\n", + "# Compile the graph with memory\n", + "graph = builder.compile(checkpointer=memory)\n", + "\n", + "# View\n", + "display(Image(graph.get_graph().draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "id": "ad5521e1-0e58-42c5-9282-ff96f24ee6f6", + "metadata": {}, + "source": [ + "### Run the graph with dynamic interrupt" + ] + }, + { + "cell_type": "markdown", + "id": "83692c63-5c65-4562-9c65-5ad1935e339f", + "metadata": {}, + "source": [ + "First, let's run the graph with an input that <= 5 characters long. This should safely ignore the interrupt condition we defined and return the original input at the end of the graph execution." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b2d281f1-3349-4378-8918-7665fa7a7457", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello'}\n", + "---Step 1---\n", + "{'input': 'hello'}\n", + "---Step 2---\n", + "{'input': 'hello'}\n", + "---Step 3---\n", + "{'input': 'hello'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "2b66b926-47eb-401b-b37b-d80269d7214c", + "metadata": {}, + "source": [ + "If we inspect the graph at this point, we can see that there are no more tasks left to run and that the graph indeed finished execution." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4eac1455-e7ef-4a32-8c14-0d5789409689", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "()\n", + "()\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f8e03817-2135-4fb3-b881-fd6d2c378ccf", + "metadata": {}, + "source": [ + "Now, let's run the graph with an input that's longer than 5 characters. This should trigger the dynamic interrupt we defined via raising a `NodeInterrupt` error inside the `step_2` node." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c06192ad-13a4-4d2e-8e30-f1c08578fe77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "173fd4f1-db97-44bb-a9e5-435ed042e3a3", + "metadata": {}, + "source": [ + "We can see that the graph now stopped while executing `step_2`. If we inspect the graph state at this point, we can see the information on what node is set to execute next (`step_2`), as well as what node raised the interrupt (also `step_2`), and additional information about the interrupt." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2058593c-178e-4a23-a4c4-860d4a9c2198", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "fc36d1be-ae2e-49c8-a17f-2b27be09618a", + "metadata": {}, + "source": [ + "If we try to resume the graph from the breakpoint, we will simply interrupt again as our inputs & graph state haven't changed." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "872e7a69-9784-4f81-90c6-6b6af2fa6480", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: to resume the graph from a dynamic interrupt we use the same syntax as with regular interrupts -- we pass None as the input\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3275f899-7039-4029-8814-0bb5c33fabfe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "a5862dea-2af2-48cb-9889-979b6c6af6aa", + "metadata": {}, + "source": [ + "### Update the graph state" + ] + }, + { + "cell_type": "markdown", + "id": "c8724ef6-877a-44b9-b96a-ae81efa2d9e4", + "metadata": {}, + "source": [ + "To get around it, we can do several things. \n", + "\n", + "First, we could simply run the graph on a different thread with a shorter input, like we did in the beginning. Alternatively, if we want to resume the graph execution from the breakpoint, we can update the state to have an input that's shorter than 5 characters (the condition for our interrupt)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ba8dc8d-b90e-45f5-92cd-2192fc66f270", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 2---\n", + "{'input': 'foo'}\n", + "---Step 3---\n", + "{'input': 'foo'}\n", + "()\n", + "{'input': 'foo'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will be applied as of the last successful node before the interrupt, i.e. `step_1`, right before the node with an interrupt\n", + "graph.update_state(config=thread_config, values={\"input\": \"foo\"})\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + }, + { + "cell_type": "markdown", + "id": "6f16980e-aef4-45c9-85eb-955568a93c5b", + "metadata": {}, + "source": [ + "You can also update the state **as node `step_2`** (interrupted node) which would skip over that node altogether" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9a48e564-d979-4ac2-b815-c667345a9f07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "17f973ab-00ce-4f16-a452-641e76625fde", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 3---\n", + "{'input': 'hello world'}\n", + "()\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will skip the node `step_2` altogether\n", + "graph.update_state(config=thread_config, values=None, as_node=\"step_2\")\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb index 7f29740a3..17468b017 100644 --- a/examples/human_in_the_loop/edit-graph-state.ipynb +++ b/examples/human_in_the_loop/edit-graph-state.ipynb @@ -541,14 +541,6 @@ "for event in app.stream(None, thread, stream_mode=\"values\"):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78780afe-409d-46cd-a734-e82538cdd8de", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/human_in_the_loop/review-tool-calls.ipynb b/examples/human_in_the_loop/review-tool-calls.ipynb index 5a4aeaf56..9bf173583 100644 --- a/examples/human_in_the_loop/review-tool-calls.ipynb +++ b/examples/human_in_the_loop/review-tool-calls.ipynb @@ -141,15 +141,18 @@ " print(\"----\")\n", " return \"Sunny!\"\n", "\n", - "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools([weather_search])\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools(\n", + " [weather_search]\n", + ")\n", + "\n", "\n", "class State(MessagesState):\n", " \"\"\"Simple state.\"\"\"\n", "\n", + "\n", "def call_llm(state):\n", - " return {\n", - " \"messages\": [model.invoke(state['messages'])]\n", - " }\n", + " return {\"messages\": [model.invoke(state[\"messages\"])]}\n", "\n", "\n", "def human_review_node(state):\n", @@ -159,28 +162,30 @@ "def run_tool(state):\n", " new_messages = []\n", " tools = {\"weather_search\": weather_search}\n", - " tool_calls = state['messages'][-1].tool_calls\n", + " tool_calls = state[\"messages\"][-1].tool_calls\n", " for tool_call in tool_calls:\n", - " tool = tools[tool_call['name']]\n", - " result = tool.invoke(tool_call['args'])\n", - " new_messages.append({\n", - " \"role\": \"tool\",\n", - " \"name\": tool_call['name'],\n", - " \"content\": result,\n", - " \"tool_call_id\": tool_call['id']\n", - " })\n", + " tool = tools[tool_call[\"name\"]]\n", + " result = tool.invoke(tool_call[\"args\"])\n", + " new_messages.append(\n", + " {\n", + " \"role\": \"tool\",\n", + " \"name\": tool_call[\"name\"],\n", + " \"content\": result,\n", + " \"tool_call_id\": tool_call[\"id\"],\n", + " }\n", + " )\n", " return {\"messages\": new_messages}\n", "\n", "\n", "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state['messages'][-1].tool_calls) == 0:\n", + " if len(state[\"messages\"][-1].tool_calls) == 0:\n", " return END\n", " else:\n", " return \"human_review_node\"\n", "\n", "\n", "def route_after_human(state) -> Literal[\"run_tool\", \"call_llm\"]:\n", - " if isinstance(state['messages'][-1], AIMessage):\n", + " if isinstance(state[\"messages\"][-1], AIMessage):\n", " return \"run_tool\"\n", " else:\n", " return \"call_llm\"\n", @@ -460,35 +465,35 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "current_content = state.values['messages'][-1].content\n", - "current_id = state.values['messages'][-1].id\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "current_content = state.values[\"messages\"][-1].content\n", + "current_id = state.values[\"messages\"][-1].id\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"assistant\", \n", + " \"role\": \"assistant\",\n", " \"content\": current_content,\n", " \"tool_calls\": [\n", " {\n", " \"id\": tool_call_id,\n", " \"name\": \"weather_search\",\n", - " \"args\": {\"city\": \"San Francisco, USA\"}\n", + " \"args\": {\"city\": \"San Francisco, USA\"},\n", " }\n", " ],\n", " # This is important - this needs to be the same as the message you replacing!\n", " # Otherwise, it will show up as a separate message\n", - " \"id\": current_id\n", + " \"id\": current_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", @@ -595,26 +600,26 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"tool\", \n", + " \"role\": \"tool\",\n", " # This is our natural language feedback\n", " \"content\": \"User requested changes: pass in the country as well\",\n", " \"name\": \"weather_search\",\n", - " \"tool_call_id\": tool_call_id\n", + " \"tool_call_id\": tool_call_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", @@ -675,7 +680,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/human_in_the_loop/time-travel.ipynb b/examples/human_in_the_loop/time-travel.ipynb index 765a1b221..04231347f 100644 --- a/examples/human_in_the_loop/time-travel.ipynb +++ b/examples/human_in_the_loop/time-travel.ipynb @@ -40,7 +40,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic" + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_openai" ] }, { @@ -48,7 +49,7 @@ "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", "metadata": {}, "source": [ - "Next, we need to set API keys for Anthropic (the LLM we will use)" + "Next, we need to set API keys for OpenAI (the LLM we will use)" ] }, { @@ -58,7 +59,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "ANTHROPIC_API_KEY: ········\n" @@ -66,7 +67,16 @@ } ], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -84,7 +94,8 @@ "metadata": {}, "outputs": [], "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")" + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" ] }, { @@ -94,18 +105,18 @@ "source": [ "## Build the agent\n", "\n", - "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)." + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and fake tools (just for demo purposes)." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 42, "id": "f5319e01", "metadata": {}, "outputs": [], "source": [ "# Set up the tool\n", - "from langchain_anthropic import ChatAnthropic\n", + "from langchain_openai import ChatOpenAI\n", "from langchain_core.tools import tool\n", "from langgraph.graph import MessagesState, START\n", "from langgraph.prebuilt import ToolNode\n", @@ -114,27 +125,28 @@ "\n", "\n", "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", + "def play_song_on_spotify(song: str):\n", + " \"\"\"Play a song on Spotify\"\"\"\n", + " # Call the spotify API ...\n", + " return f\"Successfully played {song} on Spotify!\"\n", "\n", + "@tool\n", + "def play_song_on_apple(song: str):\n", + " \"\"\"Play a song on Apple Music\"\"\"\n", + " # Call the apple music API ...\n", + " return f\"Successfully played {song} on Apple Music!\"\n", "\n", - "tools = [search]\n", + "tools = [play_song_on_apple,play_song_on_spotify]\n", "tool_node = ToolNode(tools)\n", "\n", "# Set up the model\n", "\n", - "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "model = model.bind_tools(tools)\n", + "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", + "model = model.bind_tools(tools, parallel_tool_calls=False)\n", "\n", "\n", "# Define nodes and conditional edges\n", "\n", - "\n", "# Define the function that determines whether to continue or not\n", "def should_continue(state):\n", " messages = state[\"messages\"]\n", @@ -210,12 +222,12 @@ "source": [ "## Interacting with the Agent\n", "\n", - "We can now interact with the agent. Let's ask it for the weather in SF.\n" + "We can now interact with the agent. Let's ask it to play Taylor Swift's most popular song:\n" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 43, "id": "cfd140f0-a5a6-4697-8115-322242f197b5", "metadata": {}, "outputs": [ @@ -225,38 +237,30 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "Use the search tool to look up the weather in SF\n", + "Can you play Taylor Swift's most popular song?\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n", "Tool Calls:\n", - " search (toolu_01Bpq6yiKqk9moPuGYKdLr8r)\n", - " Call ID: toolu_01Bpq6yiKqk9moPuGYKdLr8r\n", + " play_song_on_apple (call_uhGY6Fv6Mr4ZOhSokintuoD7)\n", + " Call ID: call_uhGY6Fv6Mr4ZOhSokintuoD7\n", " Args:\n", - " query: weather in San Francisco\n", + " song: Anti-Hero by Taylor Swift\n", "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: search\n", + "Name: play_song_on_apple\n", "\n", - "[\"It's sunny in San Francisco, but you better look out if you're a Gemini \\ud83d\\ude08.\"]\n", + "Succesfully played Anti-Hero by Taylor Swift on Apple Music!\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Based on the search results, I can provide you with information about the weather in San Francisco:\n", - "\n", - "The current weather in San Francisco is sunny. This is great news for residents and visitors who want to enjoy outdoor activities or explore the city.\n", - "\n", - "However, there's an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you're a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges despite the good weather. Of course, this is not a scientific weather prediction and is likely just a fun addition to the report.\n", - "\n", - "To summarize:\n", - "1. The weather in San Francisco is currently sunny.\n", - "2. It's a good day for outdoor activities.\n", - "3. There's a playful astrological warning for Geminis, but this shouldn't be taken seriously in terms of actual weather conditions.\n", - "\n", - "Is there anything else you'd like to know about the weather in San Francisco or any other location?\n" + "I've successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!\n" ] } ], "source": [ - "from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\ninput_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()" + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "input_message = HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" ] }, { @@ -271,7 +275,31 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 44, + "id": "777538a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}),\n", + " ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'),\n", + " AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "app.get_state(config).values['messages']" + ] + }, + { + "cell_type": "code", + "execution_count": 45, "id": "8578a66d-6489-4e03-8c23-fd0530278455", "metadata": {}, "outputs": [ @@ -279,21 +307,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef355ac-b80d-6e18-bfff-c903ebc4bdfd'}}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [HumanMessage(content='Use the search tool to look up the weather in SF')]}}, created_at='2024-06-28T14:29:14.932371+00:00', parent_config=None)\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7'), AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-364f-6228-8003-dd67a426334e'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='I\\'ve successfully played \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-bfee6b28-9f16-49cc-8d28-bfb5a5b9aea1-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}}, 'step': 3, 'parents': {}}, created_at='2024-09-05T21:37:39.955948+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, tasks=())\n", "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef355ac-b810-60dc-8000-a9c67d8cc5e0'}}, metadata={'source': 'loop', 'step': 0, 'writes': None}, created_at='2024-06-28T14:29:14.933257+00:00', parent_config=None)\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102}), ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-318f-6dc8-8002-dbdf9aaeac83'}}, metadata={'source': 'loop', 'writes': {'action': {'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', id='43a39ca7-326a-4033-8607-bf061615ed6b', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}}, 'step': 2, 'parents': {}}, created_at='2024-09-05T21:37:39.458185+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, tasks=(PregelTask(id='3a4c5ddb-14b2-5def-a766-02ddc32948ba', name='agent', error=None, interrupts=(), state=None),))\n", "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_011ae64fY2jEcfS8kgrt4Fn9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 81}}, id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r'}], usage_metadata={'input_tokens': 363, 'output_tokens': 81, 'total_tokens': 444})]}, next=('action',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef355ac-c6b1-6028-8001-82bd095f9a87'}}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_011ae64fY2jEcfS8kgrt4Fn9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 81}}, id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r'}], usage_metadata={'input_tokens': 363, 'output_tokens': 81, 'total_tokens': 444})]}}}, created_at='2024-06-28T14:29:16.467180+00:00', parent_config=None)\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}, next=('action',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-3185-663e-8001-12b1ec3114b8'}}, metadata={'source': 'loop', 'writes': {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}}, 'step': 1, 'parents': {}}, created_at='2024-09-05T21:37:39.453898+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, tasks=(PregelTask(id='01f1dc72-5a39-5876-97a6-abdc12f70c2a', name='action', error=None, interrupts=(), state=None),))\n", "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_011ae64fY2jEcfS8kgrt4Fn9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 81}}, id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r'}], usage_metadata={'input_tokens': 363, 'output_tokens': 81, 'total_tokens': 444}), ToolMessage(content='[\"It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini \\\\ud83d\\\\ude08.\"]', name='search', id='b5aa87cb-335a-4ee0-8809-381e33d0f02e', tool_call_id='toolu_01Bpq6yiKqk9moPuGYKdLr8r')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef355ac-c6ba-63a8-8002-48d076c3c4b7'}}, metadata={'source': 'loop', 'step': 2, 'writes': {'action': {'messages': [ToolMessage(content='[\"It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini \\\\ud83d\\\\ude08.\"]', name='search', id='b5aa87cb-335a-4ee0-8809-381e33d0f02e', tool_call_id='toolu_01Bpq6yiKqk9moPuGYKdLr8r')]}}}, created_at='2024-06-28T14:29:16.470958+00:00', parent_config=None)\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b8-6370-8000-f9f6e7ca1b06'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-09-05T21:37:38.635849+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, tasks=(PregelTask(id='348e1ba7-95c6-5b89-80c9-1fc4720e35ef', name='agent', error=None, interrupts=(), state=None),))\n", "--\n", - "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_011ae64fY2jEcfS8kgrt4Fn9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 81}}, id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r'}], usage_metadata={'input_tokens': 363, 'output_tokens': 81, 'total_tokens': 444}), ToolMessage(content='[\"It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini \\\\ud83d\\\\ude08.\"]', name='search', id='b5aa87cb-335a-4ee0-8809-381e33d0f02e', tool_call_id='toolu_01Bpq6yiKqk9moPuGYKdLr8r'), AIMessage(content='Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This is great news for residents and visitors who want to enjoy outdoor activities or explore the city.\\n\\nHowever, there\\'s an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you\\'re a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges despite the good weather. Of course, this is not a scientific weather prediction and is likely just a fun addition to the report.\\n\\nTo summarize:\\n1. The weather in San Francisco is currently sunny.\\n2. It\\'s a good day for outdoor activities.\\n3. There\\'s a playful astrological warning for Geminis, but this shouldn\\'t be taken seriously in terms of actual weather conditions.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other location?', response_metadata={'id': 'msg_01NWeLrkQRLiGsVsxnepzq3p', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 486, 'output_tokens': 217}}, id='run-09f1b7d5-50ec-4f00-a31b-c4dec858b312-0', usage_metadata={'input_tokens': 486, 'output_tokens': 217, 'total_tokens': 703})]}, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef355ac-f0f5-6942-8003-b79974988738'}}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content='Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This is great news for residents and visitors who want to enjoy outdoor activities or explore the city.\\n\\nHowever, there\\'s an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you\\'re a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges despite the good weather. Of course, this is not a scientific weather prediction and is likely just a fun addition to the report.\\n\\nTo summarize:\\n1. The weather in San Francisco is currently sunny.\\n2. It\\'s a good day for outdoor activities.\\n3. There\\'s a playful astrological warning for Geminis, but this shouldn\\'t be taken seriously in terms of actual weather conditions.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other location?', response_metadata={'id': 'msg_01NWeLrkQRLiGsVsxnepzq3p', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 486, 'output_tokens': 217}}, id='run-09f1b7d5-50ec-4f00-a31b-c4dec858b312-0', usage_metadata={'input_tokens': 486, 'output_tokens': 217, 'total_tokens': 703})]}}}, created_at='2024-06-28T14:29:20.899258+00:00', parent_config=None)\n", + "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6bcf1-29b3-6514-bfff-fe07fb36f14f'}}, metadata={'source': 'input', 'writes': {'__start__': {'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\")]}}, 'step': -1, 'parents': {}}, created_at='2024-09-05T21:37:38.633849+00:00', parent_config=None, tasks=(PregelTask(id='f1cfbb8c-7792-5cf9-9d28-ae3ac7724cf3', name='__start__', error=None, interrupts=(), state=None),))\n", "--\n" ] } ], "source": [ - "all_states = []\nfor state in app.get_state_history(config):\n print(state)\n all_states.append(state)\n print(\"--\")" + "all_states = []\n", + "for state in app.get_state_history(config):\n", + " print(state)\n", + " all_states.append(state)\n", + " print(\"--\")" ] }, { @@ -308,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 46, "id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699", "metadata": {}, "outputs": [], @@ -318,18 +350,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 47, "id": "21e7fc18-6fd9-4e11-a84b-e0325c9640c8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577'),\n", - " AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_011ae64fY2jEcfS8kgrt4Fn9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 81}}, id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01Bpq6yiKqk9moPuGYKdLr8r'}], usage_metadata={'input_tokens': 363, 'output_tokens': 81, 'total_tokens': 444})]}" + "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'function': {'arguments': '{\"song\":\"Anti-Hero by Taylor Swift\"}', 'name': 'play_song_on_apple'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 80, 'total_tokens': 102}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0', tool_calls=[{'name': 'play_song_on_apple', 'args': {'song': 'Anti-Hero by Taylor Swift'}, 'id': 'call_uhGY6Fv6Mr4ZOhSokintuoD7', 'type': 'tool_call'}], usage_metadata={'input_tokens': 80, 'output_tokens': 22, 'total_tokens': 102})]}" ] }, - "execution_count": 8, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -340,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 48, "id": "d4b01634-0041-4632-8d1f-5464580e54f5", "metadata": {}, "outputs": [ @@ -350,7 +382,7 @@ "('action',)" ] }, - "execution_count": 9, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -369,7 +401,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 49, "id": "e986f94f-706f-4b6f-b3c4-f95483b9e9b8", "metadata": {}, "outputs": [ @@ -377,13 +409,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [ToolMessage(content='[\"It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini \\\\ud83d\\\\ude08.\"]', name='search', tool_call_id='toolu_01Bpq6yiKqk9moPuGYKdLr8r')]}\n", - "{'messages': [AIMessage(content='Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This is great news for residents and visitors who want to enjoy outdoor activities or explore the city.\\n\\nHowever, there\\'s an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you\\'re a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges despite the good weather. Of course, this is not a scientific weather prediction and is likely just a fun addition to the weather report.\\n\\nTo summarize:\\n1. The weather in San Francisco is currently sunny.\\n2. It\\'s a good day for outdoor activities.\\n3. There\\'s a playful astrological reference for Geminis, but this shouldn\\'t be taken as actual weather information.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other location?', response_metadata={'id': 'msg_01S4uzzxbGwvsk1vfoLJAD7Z', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 486, 'output_tokens': 214}}, id='run-570bd10f-4007-4b33-8c29-44b9a19b6978-0', usage_metadata={'input_tokens': 486, 'output_tokens': 214, 'total_tokens': 700})]}\n" + "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Apple Music!', name='play_song_on_apple', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", + "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Apple Music! Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 126, 'total_tokens': 146}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-dc338bbd-d623-40bb-b824-5d2307954b57-0', usage_metadata={'input_tokens': 126, 'output_tokens': 20, 'total_tokens': 146})]}\n" ] } ], "source": [ - "for event in app.stream(None, to_replay.config):\n for v in event.values():\n print(v)" + "for event in app.stream(None, to_replay.config):\n", + " for v in event.values():\n", + " print(v)" ] }, { @@ -395,12 +429,12 @@ "\n", "Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n", "\n", - "Let's show how to do this to edit the state at a particular point in time. Let's update the state to change the input to the tool" + "Let's show how to do this to edit the state at a particular point in time. Let's update the state to instead of playing the song on Apple to play it on Spotify:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 52, "id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f", "metadata": {}, "outputs": [], @@ -409,13 +443,14 @@ "# This is the one with the tool calls that we want to update\n", "last_message = to_replay.values[\"messages\"][-1]\n", "\n", - "# Let's now update the args for that tool call\n", - "last_message.tool_calls[0][\"args\"] = {\"query\": \"current weather in SF\"}\n", + "\n", + "# Let's now update the tool we are calling\n", + "last_message.tool_calls[0]['name'] = 'play_song_on_spotify'\n", "\n", "branch_config = app.update_state(\n", " to_replay.config,\n", " {\"messages\": [last_message]},\n", - ")" + ")\n" ] }, { @@ -428,7 +463,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 53, "id": "9a92d3da-62e2-45a2-8545-e4f6a64e0ffe", "metadata": {}, "outputs": [ @@ -436,13 +471,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [ToolMessage(content='[\"It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini \\\\ud83d\\\\ude08.\"]', name='search', tool_call_id='toolu_01Bpq6yiKqk9moPuGYKdLr8r')]}\n", - "{'messages': [AIMessage(content=\"Based on the search results, I can provide you with information about the current weather in San Francisco (SF):\\n\\nThe weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine, which is great for outdoor activities or simply enjoying the city's beautiful views.\\n\\nIt's worth noting that San Francisco's weather can be quite variable, even within the city itself, due to its unique geography and microclimates. While it's sunny now, it's always a good idea to be prepared for potential changes, as the city is known for its foggy conditions, especially in certain areas and during specific times of the day.\\n\\nThe search result also includes a playful reference to astrology, mentioning Geminis. However, this is likely just a humorous addition and not related to the actual weather conditions.\\n\\nIs there any specific information about the weather in San Francisco that you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\", response_metadata={'id': 'msg_01AoFmmzZxbMLuu3npVXJKG7', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 486, 'output_tokens': 211}}, id='run-ccb9e786-c23e-4017-bf29-8405f17cec9f-0', usage_metadata={'input_tokens': 486, 'output_tokens': 211, 'total_tokens': 697})]}\n" + "{'messages': [ToolMessage(content='Succesfully played Anti-Hero by Taylor Swift on Spotify!', name='play_song_on_spotify', tool_call_id='call_uhGY6Fv6Mr4ZOhSokintuoD7')]}\n", + "{'messages': [AIMessage(content='I\\'ve started playing \"Anti-Hero\" by Taylor Swift on Spotify. Enjoy the music!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 125, 'total_tokens': 144}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_483d39d857', 'finish_reason': 'stop', 'logprobs': None}, id='run-7d8d5094-7029-4da3-9e0e-ef9d18b63615-0', usage_metadata={'input_tokens': 125, 'output_tokens': 19, 'total_tokens': 144})]}\n" ] } ], "source": [ - "for event in app.stream(None, branch_config):\n for v in event.values():\n print(v)" + "for event in app.stream(None, branch_config):\n", + " for v in event.values():\n", + " print(v)" ] }, { @@ -455,7 +492,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 54, "id": "01abb480-df55-4eba-a2be-cf9372b60b54", "metadata": {}, "outputs": [], @@ -467,7 +504,7 @@ "last_message = to_replay.values[\"messages\"][-1]\n", "\n", "# Let's now get the ID for the last message, and create a new message with that ID.\n", - "new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n", + "new_message = AIMessage(content=\"It's quiet hours so I can't play any music right now!\", id=last_message.id)\n", "\n", "branch_config = app.update_state(\n", " to_replay.config,\n", @@ -477,7 +514,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 55, "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", "metadata": {}, "outputs": [], @@ -487,18 +524,18 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 56, "id": "5198f9c1-d2d4-458a-993d-3caa55810b1e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='9558b3e7-fa30-4e8b-9587-d58ae3491577'),\n", - " AIMessage(content='its warm!', id='run-cfef25ca-d1be-4e79-8798-3bb9a7002287-0')]}" + "{'messages': [HumanMessage(content=\"Can you play Taylor Swift's most popular song?\", id='7e32f0f3-75f5-48e1-a4ae-d38ccc15973b'),\n", + " AIMessage(content=\"It's quiet hours so I can't play any music right now!\", id='run-af077bc4-f03c-4afe-8d92-78bdae394412-0')]}" ] }, - "execution_count": 15, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -509,7 +546,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 57, "id": "5d89d55d-db84-4c2d-828b-64a29a69947b", "metadata": {}, "outputs": [ @@ -519,7 +556,7 @@ "()" ] }, - "execution_count": 16, + "execution_count": 57, "metadata": {}, "output_type": "execute_result" } @@ -535,16 +572,6 @@ "source": [ "You can see the snapshot was updated and now correctly reflects that there is no next step." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea", - "metadata": {}, - "outputs": [], - "source": [ - "" - ] } ], "metadata": { @@ -563,7 +590,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb index eda7f0e6b..5b4ea5e86 100644 --- a/examples/human_in_the_loop/wait-user-input.ipynb +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -55,7 +55,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "ANTHROPIC_API_KEY: ········\n" @@ -227,7 +227,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "Tell me how you want to update the state: go to step 3!\n" @@ -636,14 +636,6 @@ "for event in app.stream(None, config, stream_mode=\"values\"):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb index 16779ba6e..36c926156 100644 --- a/examples/input_output_schema.ipynb +++ b/examples/input_output_schema.ipynb @@ -33,15 +33,19 @@ "from langgraph.graph import StateGraph, START, END\n", "from typing import TypedDict\n", "\n", + "\n", "class InputState(TypedDict):\n", " question: str\n", "\n", + "\n", "class OutputState(TypedDict):\n", " answer: str\n", "\n", + "\n", "def answer_node(state: InputState):\n", " return {\"answer\": \"bye\"}\n", "\n", + "\n", "graph = StateGraph(input=InputState, output=OutputState)\n", "graph.add_node(answer_node)\n", "graph.add_edge(START, \"answer_node\")\n", @@ -58,14 +62,6 @@ "source": [ "Notice that the output of invoke only includes the output schema." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b952a554-f2a4-4be3-81ab-2e08f0f441c2", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index 3d0287d05..8aa4bd007 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -1265,7 +1265,6 @@ "\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", "from typing_extensions import TypedDict\n", "\n", "from langgraph.checkpoint.memory import MemorySaver\n", @@ -1504,7 +1503,7 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from langgraph.graph import StateGraph\n", "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", "\n", "\n", "class State(TypedDict):\n", @@ -1583,7 +1582,6 @@ "\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", "from typing_extensions import TypedDict\n", "\n", "from langgraph.checkpoint.memory import MemorySaver\n", @@ -1698,7 +1696,7 @@ } ], "source": [ - "from langchain_core.messages import AIMessage\n", + "from langchain_core.messages import AIMessage, ToolMessage\n", "\n", "answer = (\n", " \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n", @@ -2082,7 +2080,6 @@ "\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", "from typing_extensions import TypedDict\n", "\n", "from langgraph.checkpoint.memory import MemorySaver\n", diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index bfe5736d8..3ae67e33b 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "abbd6948-e9a3-47ca-89c7-7ac2fc5eca8b", "metadata": {}, "outputs": [], @@ -73,7 +73,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 47, "id": "e7476bb2-1a51-42f6-b7ae-82a0300bbf84", "metadata": {}, "outputs": [], @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 78, "id": "15dd9639-691f-4906-9012-83fd6e9ac126", "metadata": {}, "outputs": [ @@ -211,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 79, "id": "45689d40-d8df-4316-a121-6ea9c87d2efe", "metadata": {}, "outputs": [], @@ -270,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 80, "id": "bbdcb57b-5362-4b9e-88db-fb3fae443fb0", "metadata": {}, "outputs": [], @@ -282,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 81, "id": "730490c6-6e3a-4173-82a1-9eb9d5eeff20", "metadata": {}, "outputs": [ @@ -292,7 +292,7 @@ "text": [ "description='tavily_search_results_json(query=\"the search query\") - a search engine.' max_results=1 {'query': 'current temperature in San Francisco'}\n", "---\n", - "name='math' description='math(problem: str, context: Optional[List[str]] = None, config: Optional[langchain_core.runnables.config.RunnableConfig] = None) - math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema= func=.calculate_expression at 0x10f354ea0> {'problem': 'raise $0 to the 3rd power', 'context': ['$0']}\n", + "name='math' description='math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema= func=.calculate_expression at 0x14e1049a0> {'problem': 'x^3', 'context': ['$1']}\n", "---\n", "join ()\n", "---\n" @@ -331,7 +331,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 82, "id": "c1fbafdd-42d4-4575-8466-e5951cee71f4", "metadata": { "jp-MarkdownHeadingCollapsed": true @@ -493,7 +493,7 @@ " }\n", " tool_messages = [\n", " FunctionMessage(\n", - " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n", + " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}, tool_call_id = k\n", " )\n", " for k, (name, task_args, obs) in new_observations.items()\n", " ]\n", @@ -502,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 83, "id": "052f6b16-103a-40e9-94dd-8fcc37e77ba4", "metadata": {}, "outputs": [], @@ -526,7 +526,7 @@ " \"tasks\": tasks,\n", " }\n", " )\n", - " return {\"messages\":[scheduled_tasks]}" + " return {\"messages\": scheduled_tasks}" ] }, { @@ -541,29 +541,29 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 84, "id": "55142257-2674-4a47-988e-0d2810917329", "metadata": {}, "outputs": [], "source": [ - "tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])" + "tool_messages = plan_and_schedule.invoke({\"messages\":[HumanMessage(content=example_question)]})['messages']" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 85, "id": "a98e0525-2fcf-4fa1-baf6-79858bb8a6bd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[FunctionMessage(content='[]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'),\n", - " FunctionMessage(content='ValueError(\\'Failed to evaluate \"N/A\". Raised error: KeyError(\\\\\\'A\\\\\\'). Please try again with a valid numerical expression\\')', additional_kwargs={'idx': 1}, name='math'),\n", - " FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]" + "[FunctionMessage(content=\"[{'url': 'https://www.wunderground.com/weather/us/ca/san-francisco', 'content': 'Current Weather for Popular Cities . San Francisco, CA 82 ° F Sunny; Manhattan, NY warning 84 ° F Sunny; Schiller Park, IL (60176) warning 97 ° F Mostly Cloudy; Boston, MA warning 74 ° F ...'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in San Francisco'}}, name='tavily_search_results_json', tool_call_id=1),\n", + " FunctionMessage(content='551368', additional_kwargs={'idx': 2, 'args': {'problem': 'x ** 3', 'context': ['$1']}}, name='math', tool_call_id=2),\n", + " FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]" ] }, - "execution_count": 12, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -589,12 +589,11 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 86, "id": "942dab42-ad42-4ba2-90d5-49edbe4fae68", "metadata": {}, "outputs": [], "source": [ - "from langchain.chains.openai_functions import create_structured_output_runnable\n", "from langchain_core.messages import AIMessage\n", "from langchain_core.pydantic_v1 import BaseModel, Field\n", "\n", @@ -625,7 +624,7 @@ ") # You can optionally add examples\n", "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", "\n", - "runnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)" + "runnable = joiner_prompt | llm.with_structured_output(JoinOutputs)" ] }, { @@ -639,7 +638,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 87, "id": "951a33cf-2a05-4a33-899a-0ab1d97122fa", "metadata": {}, "outputs": [], @@ -647,13 +646,14 @@ "def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n", " response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n", " if isinstance(decision.action, Replan):\n", - " return response + [\n", + " return {\"messages\": response + [\n", " SystemMessage(\n", " content=f\"Context from last attempt: {decision.action.feedback}\"\n", " )\n", " ]\n", + " }\n", " else:\n", - " return {\"messages\":response + [AIMessage(content=decision.action.response)]}\n", + " return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n", "\n", "\n", "def select_recent_messages(state) -> dict:\n", @@ -671,7 +671,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 88, "id": "1e49d4b1-8266-4520-a566-1448b1c31c8f", "metadata": {}, "outputs": [], @@ -681,24 +681,24 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 89, "id": "31854dfd-b82f-4c24-9b58-6bae66777909", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[AIMessage(content='Thought: The search did not return any results, and the attempt to calculate the temperature in San Francisco raised to the 3rd power failed due to missing temperature information.'),\n", - " SystemMessage(content='Context from last attempt: I need to find the current temperature in San Francisco before calculating its value raised to the 3rd power.')]" + "{'messages': [AIMessage(content=\"Thought: We have the current temperature in San Francisco (82 °F) and have calculated the temperature raised to the 3rd power (551368). Therefore, we can provide an answer to the user's question.\"),\n", + " AIMessage(content='The temperature in San Francisco raised to the 3rd power is 551368.')]}" ] }, - "execution_count": 16, + "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "joiner.invoke(input_messages)" + "joiner.invoke({\"messages\":input_messages})" ] }, { @@ -717,7 +717,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 90, "id": "768b5f11-e3d2-47be-8143-a7dcd8765243", "metadata": {}, "outputs": [], @@ -726,9 +726,11 @@ "from langgraph.graph.message import add_messages\n", "from typing import Annotated\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "\n", "# 1. Define vertices\n", @@ -752,9 +754,9 @@ "\n", "\n", "graph_builder.add_conditional_edges(\n", - " start_key=\"join\",\n", + " \"join\",\n", " # Next, we pass in the function that will determine which node is called next.\n", - " condition=should_continue,\n", + " should_continue,\n", ")\n", "graph_builder.add_edge(START, \"plan_and_schedule\")\n", "chain = graph_builder.compile()" @@ -772,7 +774,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 91, "id": "5bc4584a-e31c-4065-805e-76a6db30676a", "metadata": {}, "outputs": [ @@ -780,28 +782,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json')]}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.investopedia.com/articles/investing/011516/new-yorks-economy-6-industries-driving-gdp-growth.asp', 'content': 'The manufacturing sector is a leader in railroad rolling stock, as many of the earliest railroads were financed or founded in New York; garments, as New York City is the fashion capital of the U.S.; elevator parts; glass; and many other products.\\\\n Educational Services\\\\nThough not typically thought of as a leading industry, the educational sector in New York nonetheless has a substantial impact on the state and its residents, and in attracting new talent that eventually enters the New York business scene. New York has seen a large uptick in college attendees, both young and old, over the 21st century, and an increasing number of new employees in other New York sectors were educated in the state. New York City is the leading job hub for banking, finance, and communication in the U.S. New York is also a major manufacturing center and shipping port, and it has a thriving technological sector.\\\\n The state of New York has the third-largest economy in the United States with a gross domestic product (GDP) of $1.7 trillion, trailing only Texas and California.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'GDP of New York'}}, name='tavily_search_results_json', tool_call_id=1)]}}\n", "---\n", - "{'join': [AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget.\")]}\n", - "---\n", - "{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json')]}\n", - "---\n", - "{'join': [AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n", - "---\n", - "{'__end__': [HumanMessage(content=\"What's the GDP of New York?\"), FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget. - Begin counting at : 1\"), FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The information required to answer the user's question has been found. The GDP of New York is mentioned as $1.7 trillion, making it the third-largest economy in the United States.\", id='d656a605-e4c4-470d-9b29-31794f298a71'), AIMessage(content='The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.', id='5135758e-d01e-4360-bb6a-31025b723d8c')]}}\n", "---\n" ] } ], "source": [ - "for step in chain.stream({\"messages\":[HumanMessage(content=\"What's the GDP of New York?\")]}):\n", + "for step in chain.stream(\n", + " {\"messages\": [HumanMessage(content=\"What's the GDP of New York?\")]}\n", + "):\n", " print(step)\n", " print(\"---\")" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 92, "id": "b96efd08-5314-44f0-a694-3073b638adad", "metadata": {}, "outputs": [ @@ -809,13 +807,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.\n" + "The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" ] }, { @@ -830,7 +828,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 93, "id": "0b3a0916-d8ca-4092-b91c-d9e2b05259d8", "metadata": {}, "outputs": [ @@ -838,26 +836,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the World’s Oldest Parrot? Discover the World’s Oldest Parrot Advertisement of debate, so we’ll detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots’ Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='[{\\'url\\': \\'https://en.wikipedia.org/wiki/Cookie_(cockatoo)\\', \\'content\\': \\'He was one of the longest-lived birds on record[4] and was recognised by the Guinness World Records as the oldest living parrot in the world.[5]\\\\nThe next-oldest pink cockatoo to be found in a zoological setting was a 31-year-old female bird located at Paradise Wildlife Sanctuary, England.[3] Information published by the World Parrot Trust states longevity for Cookie\\\\\\'s species in captivity is on average 40–60 years.[6]\\\\nLife[edit]\\\\nCookie was Brookfield Zoo\\\\\\'s oldest resident and the last surviving member of the animal collection from the time of the zoo\\\\\\'s opening in 1934, having arrived from Taronga Zoo of Sydney, New South Wales, Australia, in the same year and judged to be one year old at the time.[7]\\\\nIn the 1950s an attempt was made to introduce Cookie to a female pink cockatoo, but Cookie rejected her as \"she was not nice to him\".[8]\\\\n In 2007, Cookie was diagnosed with, and placed on medication and nutritional supplements for, osteoarthritis and osteoporosis\\\\xa0– medical conditions which occur commonly in aging animals and humans alike,[7] although it is believed that the latter may also have been brought on as a result of being fed a seed-only diet for the first 40 years of his life, in the years before the dietary requirements of his species were fully understood.[9]\\\\nCookie was \"retired\" from exhibition at the zoo in 2009 (following a few months of weekend-only appearances) in order to preserve his health, after it was noticed by staff that his appetite, demeanor and stress levels improved markedly when not on public display. age.[11] A memorial at the zoo was unveiled in September 2017.[12]\\\\nIn 2020, Cookie became the subject of a poetry collection by Barbara Gregorich entitled Cookie the Cockatoo: Everything Changes.[13]\\\\nSee also[edit]\\\\nReferences[edit]\\\\nExternal links[edit] He was believed to be the oldest member of his species alive in captivity, at the age of 82 in June 2015,[1][2] having significantly exceeded the average lifespan for his kind.[3] He was moved to a permanent residence in the keepers\\\\\\' office of the zoo\\\\\\'s Perching Bird House, although he made occasional appearances for special events, such as his birthday celebration, which was held each June.[3]\\'}]', additional_kwargs={'idx': 1, 'args': {'query': 'oldest parrot alive'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='[{\\'url\\': \\'https://www.thesprucepets.com/how-long-do-parrots-and-other-pet-birds-live-1238433\\', \\'content\\': \"It\\'s possible that a pet bird can outlive its owners\\\\nThe Spruce / Adrienne Legault\\\\nParrots and other birds can live up to 10 to 50 years or more depending on the type and the conditions they live in. They vary in size from small birds that can fit in the palm of your hand to large birds the size of a cat and their lifespans are just as variable.\\\\n Also, for birds who live longer some owners have to make a plan of where the bird is going in the circumstance the bird outlives the owner.\\\\n In reality, there is a wide range in the age that pet birds might reach and certainly, some will live longer (or shorter amounts of time) than the ages listed.\\\\n Potential owners need to be aware of the longevity of their bird so they can be prepared to provide proper care for them for as long as they live.\\\\n\"}]', additional_kwargs={'idx': 2, 'args': {'query': 'average lifespan of a parrot'}}, name='tavily_search_results_json', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", "---\n", - "{'join': [AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison.')]}\n", - "---\n", - "{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json')]}\n", - "---\n", - "{'join': [AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n", - "---\n", - "{'__end__': [HumanMessage(content=\"What's the oldest parrot alive, and how much longer is that than the average?\"), FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the World’s Oldest Parrot? Discover the World’s Oldest Parrot Advertisement of debate, so we’ll detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots’ Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join'), AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison. - Begin counting at : 3'), FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json'), AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: We have information on Cookie, the cockatoo, who was recognized as the oldest living parrot at 82 years old in June 2015. This significantly exceeds the average lifespan for his kind, which is stated to be 40-60 years. The second source provides a general lifespan range for parrots and other birds, which is 10-50 years. However, this range varies significantly depending on the species and conditions. Since Cookie's specific lifespan far exceeds the average for his species and falls outside the general range for parrots, we can answer the user's question.\", id='51a280ac-2327-40c5-a27a-c821697d5a4b'), AIMessage(content='The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.', id='139ecedf-b090-4197-88c0-0fa39883b392')]}}\n", "---\n" ] } ], "source": [ - "steps = chain.stream(\n", + "steps = chain.stream({\"messages\":\n", " [\n", " HumanMessage(\n", " content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n", " )\n", - " ],\n", + " ]\n", + " },\n", " {\n", " \"recursion_limit\": 100,\n", " },\n", @@ -869,7 +862,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 94, "id": "6c65c414-7668-4fdf-ba97-f42f659b1317", "metadata": {}, "outputs": [ @@ -877,13 +870,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\n" + "The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" ] }, { @@ -896,7 +889,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 96, "id": "38d3ea91-59ba-4267-8060-ed75bbc840c6", "metadata": {}, "outputs": [ @@ -904,26 +897,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join')]}\n", - "{'join': [AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n", - "{'__end__': [HumanMessage(content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"), FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join'), AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n" + "{'plan_and_schedule': {'messages': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1, 'args': {'problem': '((3*(4+5)/0.5)+3245) + 8'}}, name='math', tool_call_id=1), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2, 'args': {'problem': '32/4.23'}}, name='math', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The calculations for both individual questions have been provided: 3307.0 for the first equation and 7.565011820330969 for the second. To answer the user's final question, we need to sum these two values.\", id='96eb85f5-831f-434e-83d8-59deeebce05d'), AIMessage(content='The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.', id='671a1a08-4725-4f98-997a-848815d61aa5')]}}\n" ] } ], "source": [ - "for step in chain.stream(\n", + "for step in chain.stream({\"messages\":\n", " [\n", " HumanMessage(\n", " content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n", " )\n", - " ]\n", + " ]}\n", "):\n", " print(step)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 97, "id": "a6cf5fe0-f178-4197-950f-257711bff8d2", "metadata": { "scrolled": true @@ -933,13 +925,53 @@ "name": "stdout", "output_type": "stream", "text": [ - "The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.\n" + "The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" + ] + }, + { + "cell_type": "markdown", + "id": "f9487866", + "metadata": {}, + "source": [ + "#### Complex Replanning Example\n", + "\n", + "This question is likely to prompt the Replan functionality, but it may need to be run multiple times to see this in action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "391d6931", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.timeanddate.com/weather/japan/tokyo', 'content': '88 / 84 °F. 13. 87 / 82 °F. 14. 84 / 80 °F. Detailed forecast for 14 days. Need some help? Current weather in Tokyo and forecast for today, tomorrow, and next 14 days.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in Tokyo'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='join', additional_kwargs={'idx': 2, 'args': ()}, name='join', tool_call_id=2)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides the current temperature in Tokyo but does not explicitly state which temperature (88 / 84 °F) corresponds to the current condition. It seems to be a range, possibly the day's high and low. Without a clear indication of the exact current temperature, it's challenging to provide a precise flashcard summary.\", id='8ef2a131-69db-4180-a76e-fd9d6f4037c1'), SystemMessage(content='Context from last attempt: The information provided does not explicitly state the current temperature in Tokyo; it provides a temperature range without specifying which is the current temperature. Need to find a source that gives the exact current temperature in Tokyo for a precise flashcard summary.', id='f5bd752c-b068-459a-8d9e-bd1f1b5fa4fe')]}}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='3cc41891-4f47-4453-8edf-b989926ab25e'), SystemMessage(content='Context from last attempt: The search did not provide an exact current temperature for Tokyo, making it impossible to create a precise flashcard. A source that explicitly states the current temperature is needed for an accurate response.', id='96290b41-a4c4-4ab5-829a-89cc31dfe6c8')]}}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 4, 'args': ()}, name='join', tool_call_id=4)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='4724b242-ddb8-47e6-b235-de25de54fe45'), AIMessage(content='I was unable to find the exact current temperature in Tokyo. However, the temperature range for today in Tokyo is between 88°F and 84°F. For the most accurate and up-to-date temperature, I recommend checking a reliable weather forecasting website or app.', id='40e29a47-a001-4f65-a18f-65c2931d1ae5')]}}\n" + ] + } + ], + "source": [ + "for step in chain.stream({\"messages\":\n", + " [\n", + " HumanMessage(\n", + " content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n", + " )\n", + " ]}\n", + "):\n", + " print(step)" ] }, { @@ -955,14 +987,6 @@ "2. Variable substitution is fragile in the example above. It could be made more robust by using a fine-tuned model and a more robust syntax (using e.g., Lark or a tool calling schema)\n", "3. The state can grow quite long if you require multiple re-planning runs. To handle, you could add a message compressor once you go above a certain token limit.\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "431217e6-4c00-409f-a2bd-40ebff902489", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -981,7 +1005,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/llm-compiler/math_tools.py b/examples/llm-compiler/math_tools.py index 8de7bfa2d..b9795d3e5 100644 --- a/examples/llm-compiler/math_tools.py +++ b/examples/llm-compiler/math_tools.py @@ -6,10 +6,10 @@ import numexpr from langchain.chains.openai_functions import create_structured_output_runnable from langchain_core.messages import SystemMessage from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.runnables import RunnableConfig from langchain_core.tools import StructuredTool from langchain_openai import ChatOpenAI +from pydantic import BaseModel, Field _MATH_DESCRIPTION = ( "math(problem: str, context: Optional[list[str]]) -> float:\n" @@ -114,7 +114,7 @@ def get_math_tool(llm: ChatOpenAI): MessagesPlaceholder(variable_name="context", optional=True), ] ) - extractor = create_structured_output_runnable(ExecuteCode, llm, prompt) + extractor = prompt | llm.with_structured_output(ExecuteCode) def calculate_expression( problem: str, diff --git a/examples/many-tools.ipynb b/examples/many-tools.ipynb index b415f5065..107c9921f 100644 --- a/examples/many-tools.ipynb +++ b/examples/many-tools.ipynb @@ -328,9 +328,9 @@ " \"set more_information_needed False and populate a blank string for the query.\"\n", " )\n", " input_messages = [system] + state[\"messages\"]\n", - " response = llm.bind_tools(\n", - " [QueryForTools], tool_choice=True\n", - " ).invoke(input_messages)\n", + " response = llm.bind_tools([QueryForTools], tool_choice=True).invoke(\n", + " input_messages\n", + " )\n", " query = response.tool_calls[0][\"args\"][\"query\"]\n", " tool_documents = vector_store.similarity_search(query)\n", " if hack_remove_tool_condition:\n", diff --git a/examples/map-reduce.ipynb b/examples/map-reduce.ipynb index 6fee482a4..055ca43b7 100644 --- a/examples/map-reduce.ipynb +++ b/examples/map-reduce.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "3eb04cd1", "metadata": {}, "outputs": [], @@ -36,10 +36,18 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "dc292321", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "ANTHROPIC_API_KEY: ········\n" + ] + } + ], "source": [ "import os\n", "import getpass\n", @@ -55,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 3, "id": "0f0f78e4-423d-4e2d-aa1a-01efaec4715f", "metadata": {}, "outputs": [], @@ -63,7 +71,7 @@ "import operator\n", "from typing import Annotated, TypedDict\n", "\n", - "from langchain_core.pydantic_v1 import BaseModel\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "from langgraph.constants import Send\n", @@ -87,7 +95,7 @@ "\n", "\n", "class BestJoke(BaseModel):\n", - " id: int\n", + " id: int = Field(description=\"Index of the best joke, starting with 0\")\n", "\n", "\n", "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", @@ -161,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "id": "37ed1f71-63db-416f-b715-4617b33d4b7f", "metadata": {}, "outputs": [ @@ -172,7 +180,7 @@ "" ] }, - "execution_count": 11, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -185,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "id": "fd90cace", "metadata": {}, "outputs": [ @@ -193,12 +201,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'generate_topics': {'subjects': ['lion', 'elephant', 'penguin', 'dolphin']}}\n", + "{'generate_topics': {'subjects': ['Lions', 'Elephants', 'Penguins', 'Dolphins']}}\n", "{'generate_joke': {'jokes': [\"Why don't elephants use computers? They're afraid of the mouse!\"]}}\n", - "{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? They're afraid of phishing!\"]}}\n", - "{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n", + "{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? Because they're afraid of phishing!\"]}}\n", "{'generate_joke': {'jokes': [\"Why don't you see penguins in Britain? Because they're afraid of Wales!\"]}}\n", - "{'best_joke': {'best_selected_joke': \"Why don't you see penguins in Britain? Because they're afraid of Wales!\"}}\n" + "{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n", + "{'best_joke': {'best_selected_joke': \"Why don't dolphins use smartphones? Because they're afraid of phishing!\"}}\n" ] } ], @@ -207,21 +215,13 @@ "for s in app.stream({\"topic\": \"animals\"}):\n", " print(s)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f28eaf56", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph", "language": "python", - "name": "python3" + "name": "langgraph" }, "language_info": { "codemirror_mode": { @@ -233,7 +233,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/memory/add-summary-conversation-history.ipynb b/examples/memory/add-summary-conversation-history.ipynb index 10584bc21..4b7f73955 100644 --- a/examples/memory/add-summary-conversation-history.ipynb +++ b/examples/memory/add-summary-conversation-history.ipynb @@ -508,14 +508,6 @@ "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n", " print_update(event)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67d26013-1362-4cee-b135-ab5c3c4eb3d0", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/memory/delete-messages.ipynb b/examples/memory/delete-messages.ipynb index dd4cab2eb..2cf6d1b5a 100644 --- a/examples/memory/delete-messages.ipynb +++ b/examples/memory/delete-messages.ipynb @@ -52,7 +52,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "ANTHROPIC_API_KEY: ········\n" @@ -466,14 +466,6 @@ "source": [ "Remember, when deleting messages you will want to make sure that the remaining message list is still valid. This message list **may actually not be** - this is because it currently starts with an AI message, which some models do not allow." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d7222cd-5767-42f0-bc69-10615127eba5", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/memory/manage-conversation-history.ipynb b/examples/memory/manage-conversation-history.ipynb index 066d38ad6..ac835673f 100644 --- a/examples/memory/manage-conversation-history.ipynb +++ b/examples/memory/manage-conversation-history.ipynb @@ -268,8 +268,8 @@ "\n", "\n", "def filter_messages(messages: list):\n", - " # This is very simple helper function which only ever uses the last two messages\n", - " return messages[-2:]\n", + " # This is very simple helper function which only ever uses the last message\n", + " return messages[-1:]\n", "\n", "\n", "# Define the function that calls the model\n", @@ -360,21 +360,13 @@ "- [How to filter messages](https://python.langchain.com/v0.2/docs/how_to/filter_messages/)\n", "- [How to trim messages](https://python.langchain.com/v0.2/docs/how_to/trim_messages/)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "686861bb-ec32-46f3-b7b3-fdac106f22f6", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph-example-dev", "language": "python", - "name": "python3" + "name": "langgraph-example-dev" }, "language_info": { "codemirror_mode": { @@ -386,7 +378,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/memory/shared-state.ipynb b/examples/memory/shared-state.ipynb new file mode 100644 index 000000000..ae236b77b --- /dev/null +++ b/examples/memory/shared-state.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7240d5b5-9dac-4070-8a9e-2350fb01e0be", + "metadata": {}, + "source": [ + "# How to share state between threads\n", + "\n", + "By default, state in a graph is scoped to that thread.\n", + "LangGraph also allows you to specify a \"scope\" for a given key/value pair that exists between threads. This can be useful for storing information that is shared between threads. For instance, you may want to store information about a user's preferences expressed in one thread, and then use that information in another thread.\n", + "\n", + "In this notebook we will go through an example of how to construct and use such a graph." + ] + }, + { + "cell_type": "markdown", + "id": "c4c550b5-1954-496b-8b9d-800361af17dc", + "metadata": {}, + "source": [ + "## Create graph\n", + "\n", + "In this example we will create a graph that will let us store information about a user's preferences. We will do so by defining a state key that will be scoped to a user_id, and allowing the model to populate this field as it deems fit (by providing the model with a tool to save information about the user).\n", + "\n", + " \n", + "
\n", + "

Typing shared state keys

\n", + "

\n", + " Shared state channels (keys) MUST be dictionaries (see `info` channel in the AgentState example below)\n", + "

\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a7f303d6-612e-4e34-bf36-29d4ed25d802", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph.graph import START, END\n", + "from langgraph.graph.message import MessagesState\n", + "from langgraph.graph.state import StateGraph\n", + "from langgraph.store.memory import MemoryStore\n", + "from langgraph.managed.shared_value import SharedValue\n", + "from typing import TypedDict, Annotated, Any\n", + "import uuid\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "class AgentState(MessagesState):\n", + " # We use an info key to track information\n", + " # This is scoped to a user_id, so it will be information specific to each user\n", + " info: Annotated[dict, SharedValue.on(\"user_id\")]\n", + "\n", + "\n", + "# We will give this as a tool to the agent\n", + "# This will let the agent call this tool to save a fact\n", + "class Info(TypedDict):\n", + " \"\"\"This tool should be called when you want to save a new fact about the user.\n", + " \n", + " Attributes:\n", + " fact (str): A fact about the user.\n", + " topic (str): The topic related the fact is about, i.e. Food, Location, Movies, etc.\n", + " \"\"\"\n", + " fact: str\n", + " topic: str\n", + "\n", + "\n", + "# This is the prompt we give the agent\n", + "# We will pass known info into the prompt\n", + "# We will tell it to use the Info tool to save more\n", + "prompt = \"\"\"You are helpful assistant.\n", + "\n", + "Here is what you know about the user:\n", + "\n", + "\n", + "{info}\n", + "\n", + "\n", + "Help out the user. If the user tells you any information about themselves, save the information using the `Info` tool.\n", + "\n", + "This means if the user provides any sort of fact about themselves, be it an opinion they have, a fact about themselves, etc. SAVE IT!\n", + "\"\"\"\n", + "\n", + "\n", + "# We give the model access to the Info tool\n", + "model = ChatOpenAI().bind_tools([Info])\n", + "\n", + "\n", + "# Our first node - this will call the model\n", + "def call_model(state):\n", + " # We get all facts and assemble them into a string\n", + " facts = [d['fact'] for d in state['info'].values()]\n", + " info = \"\\n\".join(facts)\n", + " # Format system prompt\n", + " system_msg = prompt.format(info=info)\n", + " # Call model\n", + " response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + state['messages'])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Routing function to decide what to do next\n", + "# If no tool calls, then we end\n", + "# If tool calls, then we update memory\n", + "def route(state):\n", + " if len(state['messages'][-1].tool_calls) == 0:\n", + " return END\n", + " else:\n", + " return \"update_memory\"\n", + "\n", + "\n", + "# This function is responsible for updating the memory\n", + "def update_memory(state):\n", + " tool_calls = []\n", + " memories = {}\n", + " # Each tool call is a new memory to save\n", + " for tc in state['messages'][-1].tool_calls:\n", + " # We append ToolMessages (to pass back to the LLM)\n", + " # This is needed because OpenAI requires each tool call be followed by a ToolMessage\n", + " tool_calls.append({\"role\": \"tool\", \"content\": \"Saved!\", \"tool_call_id\": tc['id']})\n", + " # We create a new memory from this tool call\n", + " memories[str(uuid.uuid4())] = {\"fact\": tc['args']['fact'], \"topic\": tc['args']['topic']}\n", + " # Return the messages and memories to update the state with\n", + " return {\"messages\": tool_calls, \"info\": memories}\n", + "\n", + "\n", + "# This is the in memory checkpointer we will use\n", + "# We need this because we want to enable threads (conversations)\n", + "memory = MemorySaver()\n", + "\n", + "# This is the in memory Key Value store\n", + "# This is needed to save the memories\n", + "kv = MemoryStore()\n", + "\n", + "# Construct this relatively simple graph\n", + "graph = StateGraph(AgentState)\n", + "graph.add_node(call_model)\n", + "graph.add_node(update_memory)\n", + "graph.add_edge(\"update_memory\", END)\n", + "graph.add_edge(START, \"call_model\")\n", + "graph.add_conditional_edges(\"call_model\", route)\n", + "graph = graph.compile(checkpointer=memory, store=kv)" + ] + }, + { + "cell_type": "markdown", + "id": "552d4e33-556d-4fa5-8094-2a076bc21529", + "metadata": {}, + "source": [ + "## Run graph on one thread\n", + "\n", + "We can now run the graph on one thread and give it some information" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "18bd8679-3a73-4033-bfb4-5093ac1f5d7f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'call_model': {'messages': [AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 171, 'total_tokens': 181}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-fbbb73a4-7c94-4db1-8761-44ea2fe9feaf-0', usage_metadata={'input_tokens': 171, 'output_tokens': 10, 'total_tokens': 181})]}}\n", + "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_zMUXZfhOCFYvZg5TwXyBzw16', 'function': {'arguments': '{\"fact\":\"I like pepperoni pizza\",\"topic\":\"Food\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 193, 'total_tokens': 214}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-7297f9fb-1d3e-480e-b125-ab269f648158-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'I like pepperoni pizza', 'topic': 'Food'}, 'id': 'call_zMUXZfhOCFYvZg5TwXyBzw16', 'type': 'tool_call'}], usage_metadata={'input_tokens': 193, 'output_tokens': 21, 'total_tokens': 214})]}}\n", + "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_zMUXZfhOCFYvZg5TwXyBzw16'}]}}\n", + "{'call_model': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_GjshujJAeqoTuuBeHCD5YTPQ', 'function': {'arguments': '{\"fact\":\"I just moved to SF\",\"topic\":\"Location\"}', 'name': 'Info'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 239, 'total_tokens': 260}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-4abea1d6-7ccb-49b4-b805-0e04ebb542e3-0', tool_calls=[{'name': 'Info', 'args': {'fact': 'I just moved to SF', 'topic': 'Location'}, 'id': 'call_GjshujJAeqoTuuBeHCD5YTPQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 239, 'output_tokens': 21, 'total_tokens': 260})]}}\n", + "{'update_memory': {'messages': [{'role': 'tool', 'content': 'Saved!', 'tool_call_id': 'call_GjshujJAeqoTuuBeHCD5YTPQ'}]}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n", + "\n", + "# First let's just say hi to the AI\n", + "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"hi\"}]}, config, stream_mode=\"updates\"):\n", + " print(update)\n", + "\n", + "# Let's continue the conversation (by passing the same config) and tell the AI we like pepperoni pizza\n", + "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"i like pepperoni pizza\"}]}, config, stream_mode=\"updates\"):\n", + " print(update)\n", + "\n", + "# Let's continue the conversation even further (by passing the same config) and tell the AI we live in SF\n", + "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"i also just moved to SF\"}]}, config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "id": "b8c416fa-086a-491d-a7d3-57091f6413e3", + "metadata": {}, + "source": [ + "## Run graph on a different thread\n", + "\n", + "We can now run the graph on a different thread and see that it remembers facts about the user (specifically that the user likes pepperoni pizza and lives in SF):" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e240f025-ff8b-4d17-beb7-2420c0575dd9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'call_model': {'messages': [AIMessage(content=\"Sure! Since you just moved to San Francisco, how about trying some popular local spots? Here are a few restaurant recommendations in SF:\\n\\n1. Tony's Pizza Napoletana - Known for their delicious pepperoni pizza!\\n2. The Slanted Door - A popular Vietnamese restaurant in the city.\\n3. Zuni Cafe - A classic American restaurant with a great ambiance.\\n4. Tartine Bakery - Perfect for a casual dinner with amazing baked goods.\\n5. State Bird Provisions - A unique dining experience with small plates and a lively atmosphere.\\n\\nFeel free to explore these options and enjoy your dinner! If you need more recommendations or information about a specific cuisine, let me know!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 138, 'prompt_tokens': 197, 'total_tokens': 335}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-de8ad08c-0810-4bb5-b2e8-d3dc89522f8e-0', usage_metadata={'input_tokens': 197, 'output_tokens': 138, 'total_tokens': 335})]}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n", + "\n", + "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\"}]}, config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "id": "091995d3", + "metadata": {}, + "source": [ + "Perfect! The AI recommended restaurants in SF, and included a pizza restaurant at the top of it's list.\n", + "\n", + "Notice that the `messages` in this new thread do NOT contain the messages from the previous thread since we didn't store them as shared values across the `user_id`. However, the `info` we saved in the previous thread was saved since we passed in the same `user_id` in this new thread.\n", + "\n", + "Let's now run the graph for another user to verify that the preferences of the first user are self contained:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f9bf2c15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'call_model': {'messages': [AIMessage(content='I can definitely help you with that! To provide you with personalized restaurant recommendations, could you please let me know your location or any specific preferences you have for dinner?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 185, 'total_tokens': 219}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-5a483acf-1289-4d7f-b707-97760a8c3620-0', usage_metadata={'input_tokens': 185, 'output_tokens': 34, 'total_tokens': 219})]}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n", + "\n", + "for update in graph.stream({\"messages\": [{\"role\": \"user\", \"content\": \"where and what should i eat for dinner? Can you list some restaurants?\"}]}, config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "id": "b7086cea", + "metadata": {}, + "source": [ + "Perfect! The graph has forgotten all of the previous preferences and has to ask the user for it's location and dietary preferences." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_agent/agent_supervisor.ipynb b/examples/multi_agent/agent_supervisor.ipynb index c6626d755..b1ecfe31b 100644 --- a/examples/multi_agent/agent_supervisor.ipynb +++ b/examples/multi_agent/agent_supervisor.ipynb @@ -26,7 +26,10 @@ "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas" + ] }, { "cell_type": "code", @@ -34,7 +37,24 @@ "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] }, { "cell_type": "markdown", @@ -48,45 +68,51 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "f04c6778-403b-4b49-9b93-678e910d5cec", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_experimental.tools import PythonREPLTool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# This executes code locally, which can be unsafe\npython_repl_tool = PythonREPLTool()"] + "source": [ + "from typing import Annotated\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_experimental.tools import PythonREPLTool\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "# This executes code locally, which can be unsafe\n", + "python_repl_tool = PythonREPLTool()" + ] }, { "cell_type": "markdown", "id": "d58d1e85-22d4-4c22-9062-72a346a0d709", "metadata": {}, "source": [ - "## Helper Utilities\n", - "\n", - "Define a helper function below, which make it easier to add new agent worker nodes." + "## Helper Utilities" ] }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c4823dd9-26bd-4e1a-8117-b97b2860211a", - "metadata": {}, - "outputs": [], - "source": ["from langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\n\ndef create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n # Each worker node will be given a name and some tools.\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_tools_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor"] - }, { "cell_type": "markdown", "id": "b7c302b0-cd57-4913-986f-5dc7d6d77386", "metadata": {}, "source": [ - "We can also define a function that we will use to be the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph" + "Define a helper function that we will use to create the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "80862241-a1a7-4726-bce5-f867b233832e", "metadata": {}, "outputs": [], - "source": ["def agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "def agent_node(state, agent, name):\n", + " result = agent.invoke(state)\n", + " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}" + ] }, { "cell_type": "markdown", @@ -100,11 +126,53 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "id": "311f0a58-b425-4496-adac-dc4cd8ffb912", "metadata": {}, "outputs": [], - "source": ["from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nmembers = [\"Researcher\", \"Coder\"]\nsystem_prompt = (\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\"\n)\n# Our team supervisor is an LLM node. It just picks the next agent to process\n# and decides when the work is completed\noptions = [\"FINISH\"] + members\n# Using openai function calling can make output parsing easier for us\nfunction_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n }\n },\n \"required\": [\"next\"],\n },\n}\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n).partial(options=str(options), members=\", \".join(members))\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_chain = (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n)"] + "source": [ + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "from pydantic import BaseModel\n", + "from typing import Literal\n", + "\n", + "members = [\"Researcher\", \"Coder\"]\n", + "system_prompt = (\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\"\n", + ")\n", + "# Our team supervisor is an LLM node. It just picks the next agent to process\n", + "# and decides when the work is completed\n", + "options = [\"FINISH\"] + members\n", + "\n", + "class routeResponse(BaseModel):\n", + " next: Literal[*options]\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + ").partial(options=str(options), members=\", \".join(members))\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "def supervisor_agent(state):\n", + " supervisor_chain = (\n", + " prompt\n", + " | llm.with_structured_output(routeResponse)\n", + " )\n", + " return supervisor_chain.invoke(state)" + ] }, { "cell_type": "markdown", @@ -118,11 +186,41 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8", "metadata": {}, "outputs": [], - "source": ["import functools\nimport operator\nfrom typing import Sequence, TypedDict\n\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\n# The agent state is the input to each node in the graph\nclass AgentState(TypedDict):\n # The annotation tells the graph that new messages will always\n # be added to the current states\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # The 'next' field indicates where to route to next\n next: str\n\n\nresearch_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\ncode_agent = create_agent(\n llm,\n [python_repl_tool],\n \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n)\ncode_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"Coder\", code_node)\nworkflow.add_node(\"supervisor\", supervisor_chain)"] + "source": [ + "import functools\n", + "import operator\n", + "from typing import Sequence, TypedDict\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# The agent state is the input to each node in the graph\n", + "class AgentState(TypedDict):\n", + " # The annotation tells the graph that new messages will always\n", + " # be added to the current states\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + " # The 'next' field indicates where to route to next\n", + " next: str\n", + "\n", + "\n", + "research_agent = create_react_agent(llm, tools=[tavily_tool])\n", + "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n", + "\n", + "# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n", + "code_agent = create_react_agent(llm, tools=[python_repl_tool])\n", + "code_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n", + "\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"Researcher\", research_node)\n", + "workflow.add_node(\"Coder\", code_node)\n", + "workflow.add_node(\"supervisor\", supervisor_agent)" + ] }, { "cell_type": "markdown", @@ -134,11 +232,24 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "id": "14778e86-077b-4e6a-893c-400e59b0cdbf", "metadata": {}, "outputs": [], - "source": ["for member in members:\n # We want our workers to ALWAYS \"report back\" to the supervisor when done\n workflow.add_edge(member, \"supervisor\")\n# The supervisor populates the \"next\" field in the graph state\n# which routes to a node or finishes\nconditional_map = {k: k for k in members}\nconditional_map[\"FINISH\"] = END\nworkflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n# Finally, add entrypoint\nworkflow.add_edge(START, \"supervisor\")\n\ngraph = workflow.compile()"] + "source": [ + "for member in members:\n", + " # We want our workers to ALWAYS \"report back\" to the supervisor when done\n", + " workflow.add_edge(member, \"supervisor\")\n", + "# The supervisor populates the \"next\" field in the graph state\n", + "# which routes to a node or finishes\n", + "conditional_map = {k: k for k in members}\n", + "conditional_map[\"FINISH\"] = END\n", + "workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n", + "# Finally, add entrypoint\n", + "workflow.add_edge(START, \"supervisor\")\n", + "\n", + "graph = workflow.compile()" + ] }, { "cell_type": "markdown", @@ -152,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "id": "56ba78e9-d9c1-457c-a073-d606d5d3e013", "metadata": {}, "outputs": [ @@ -161,32 +272,30 @@ "output_type": "stream", "text": [ "{'supervisor': {'next': 'Coder'}}\n", - "----\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Python REPL can execute arbitrary code. Use with caution.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'Coder': {'messages': [HumanMessage(content=\"The code `print('Hello, World!')` was executed, and the output is:\\n\\n```\\nHello, World!\\n```\", name='Coder')]}}\n", + "----\n", + "{'Coder': {'messages': [HumanMessage(content='The code to print \"Hello, World!\" to the terminal is:\\n\\n```python\\nprint(\\'Hello, World!\\')\\n```\\n\\nWhen executed, it prints:\\n```\\nHello, World!\\n```', name='Coder')]}}\n", "----\n", "{'supervisor': {'next': 'FINISH'}}\n", "----\n" ] } ], - "source": ["for s in graph.stream(\n {\n \"messages\": [\n HumanMessage(content=\"Code hello world and print it to the terminal\")\n ]\n }\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] + "source": [ + "for s in graph.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(content=\"Code hello world and print it to the terminal\")\n", + " ]\n", + " }\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"----\")" + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "45a92dfd-0e11-47f5-aad4-b68d24990e34", "metadata": {}, "outputs": [ @@ -196,22 +305,22 @@ "text": [ "{'supervisor': {'next': 'Researcher'}}\n", "----\n", - "{'Researcher': {'messages': [HumanMessage(content='**Research Report on Pikas**\\n\\nPikas are small mammals related to rabbits, known for their distinctive chirping sounds. They inhabit some of the most challenging environments, particularly boulder fields at high elevations, such as those found along the treeless slopes of the Southern Rockies, where they can be found at altitudes of up to 14,000 feet. Pikas are well-adapted to cold climates and typically do not fare well in warmer temperatures.\\n\\nRecent studies have shown that pikas are being impacted by climate change. Research by Peter Billman, a Ph.D. student from the University of Connecticut, indicates that pikas have moved upslope by approximately 1,160 feet. This upslope retreat is a direct response to changing climatic conditions, as pikas seek cooler temperatures at higher elevations.\\n\\nPikas are also known to be industrious foragers, particularly during the summer months when they gather vegetation to create haypiles for winter sustenance. Their behavior is encapsulated in the saying, \"making hay while the sun shines,\" reflecting their proactive approach to survival in harsh conditions.\\n\\nThe effects of climate change on pikas are not limited to the Southern Rockies. Studies published in Global Change Biology suggest that climate change is influencing pikas even in areas where they were previously thought to be less vulnerable, such as the Northern Rockies. These findings point to a broader trend of pikas moving to higher elevations, a behavior that may indicate a search for cooler, more suitable habitats.\\n\\nMoreover, researchers are exploring the possibility that pikas at lower elevations may have developed warm adaptations that could be beneficial for their future survival, given the ongoing climatic shifts. This line of research could help conservationists understand how pikas might cope with a warming world.\\n\\nIn conclusion, pikas are a species that not only fascinate with their unique behaviors and adaptations but also serve as indicators of environmental changes. Their upslope migration in response to climate change highlights the urgency for understanding and mitigating the effects of global warming on mountain ecosystems and the species that inhabit them.\\n\\n**Sources:**\\n- [Colorado Sun](https://coloradosun.com/2023/08/27/colorado-pika-population-climate-change/)\\n- [Wildlife.org](https://wildlife.org/climate-change-affects-pikas-even-in-unlikely-areas/)', name='Researcher')]}}\n", + "{'Researcher': {'messages': [HumanMessage(content='# Research Report on Pikas\\n\\nPikas, belonging to the genus Ochotona, are small, short-legged, and virtually tailless mammals that are often found in the mountains of western North America and across much of Asia. Despite their rodent-like appearance, pikas are not rodents but rather are part of the order Lagomorpha, which also includes rabbits and hares.\\n\\n## Behavior and Ecology\\nPikas are known for their unique behavior of not hibernating and remaining active throughout the winter. They navigate through tunnels under rocks and snow and rely on dried plants, which they have stored during warmer months in caches known as \"haypiles.\" This foraging strategy, termed \"haying,\" is crucial for their survival during the harsh winter months.\\n\\nPikas have a preference for cooler temperatures, typically foraging in temperatures below 25°C (77°F). They tend to avoid direct sunlight and stay in shaded regions when it gets warmer. A study has shown that for every 1°C (1.8°F) increase in ambient temperature, pikas can lose 3% of their foraging time, making them sensitive to climate change.\\n\\n## Distribution and Habitat\\nThe American pika (Ochotona princeps) and its relative, the collared pika (O. collaris), are found throughout the high mountainous regions of western North America. These species prefer cooler climates and have been observed to retreat to higher elevations as a response to increasing temperatures. Their current distribution is believed to be a result of a retreat from much larger ranges they occupied in the past, which included Western Europe and Eastern North America.\\n\\n## Conservation Status\\nThe International Union for Conservation of Nature and Natural Resources (IUCN) lists the American pika as a species of Least Concern but notes that populations are declining and unlikely to rebound due to habitat loss from extreme temperatures. The sensitivity of pikas to summer heat makes them an indicator species for the potential effects of climate change. Studies have shown that some populations are in decline, and there have been cases of local extirpation, particularly in the Great Basin.\\n\\n## Human Impact\\nHuman activity has impacted the ecosystems where pikas live, with recorded interactions dating back to the 1970s. Such interactions have been linked to pikas having reduced foraging time, limiting the amount of food they can stockpile for winter. Additionally, pikas have been considered pests in regions like the Tibetan plateau, where high densities of burrowing pikas are thought to reduce forage for domestic livestock and damage grasslands.\\n\\n## Conclusion\\nPikas are fascinating creatures with distinct adaptations that allow them to thrive in alpine environments. However, their future is uncertain due to the looming threats of climate change and habitat alteration. Conservation efforts, research, and monitoring are vital to ensure the survival of these unique mammals in a changing world.\\n\\n---\\n\\n**Sources:**\\n- [Wikipedia - Pika](https://en.wikipedia.org/wiki/Pika)\\n- [Treehugger - American Pika](https://www.treehugger.com/surprising-facts-about-american-pika-4864528)\\n- [National Park Service - Pikas at Rocky Mountain National Park](https://www.nps.gov/romo/learn/nature/pikas.htm)\\n- [Wikipedia - American Pika](https://en.wikipedia.org/wiki/American_pika)\\n- [Britannica - Pika](https://www.britannica.com/animal/pika)', name='Researcher')]}}\n", "----\n", "{'supervisor': {'next': 'FINISH'}}\n", "----\n" ] } ], - "source": ["for s in graph.stream(\n {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "for s in graph.stream(\n", + " {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n", + " {\"recursion_limit\": 100},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"----\")" + ] } ], "metadata": { @@ -230,7 +339,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/multi_agent/hierarchical_agent_teams.ipynb b/examples/multi_agent/hierarchical_agent_teams.ipynb index e539c91f9..43b889cca 100644 --- a/examples/multi_agent/hierarchical_agent_teams.ipynb +++ b/examples/multi_agent/hierarchical_agent_teams.ipynb @@ -40,7 +40,10 @@ } }, "outputs": [], - "source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai langchain_experimental"] + "source": [ + "# %%capture --no-stderr\n", + "# %pip install -U langgraph langchain langchain_openai langchain_experimental" + ] }, { "cell_type": "code", @@ -53,7 +56,25 @@ } }, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith.\n", + "# This will help you visualize and debug the control flow\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] }, { "cell_type": "markdown", @@ -73,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "4024eb89-843d-4cc3-ab3f-e1eb4d031179", "metadata": { "ExecuteTime": { @@ -81,8 +102,37 @@ "start_time": "2024-05-15T08:19:42.397083Z" } }, - "outputs": [], - "source": ["from typing import Annotated, List\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n\n@tool\ndef scrape_webpages(urls: List[str]) -> str:\n \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n loader = WebBaseLoader(urls)\n docs = loader.load()\n return \"\\n\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )"] + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "USER_AGENT environment variable not set, consider setting it to identify your requests.\n" + ] + } + ], + "source": [ + "from typing import Annotated, List\n", + "\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "\n", + "@tool\n", + "def scrape_webpages(urls: List[str]) -> str:\n", + " \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n", + " loader = WebBaseLoader(urls)\n", + " docs = loader.load()\n", + " return \"\\n\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )" + ] }, { "cell_type": "markdown", @@ -99,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "f20a18ca-2709-4c12-84f3-88678591a9fa", "metadata": { "ExecuteTime": { @@ -108,7 +158,99 @@ } }, "outputs": [], - "source": ["from pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Dict, Optional\n\nfrom langchain_experimental.utilities import PythonREPL\nfrom typing_extensions import TypedDict\n\n_TEMP_DIRECTORY = TemporaryDirectory()\nWORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n\n\n@tool\ndef create_outline(\n points: Annotated[List[str], \"List of main points or sections.\"],\n file_name: Annotated[str, \"File path to save the outline.\"],\n) -> Annotated[str, \"Path of the saved outline file.\"]:\n \"\"\"Create and save an outline.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n for i, point in enumerate(points):\n file.write(f\"{i + 1}. {point}\\n\")\n return f\"Outline saved to {file_name}\"\n\n\n@tool\ndef read_document(\n file_name: Annotated[str, \"File path to save the document.\"],\n start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n) -> str:\n \"\"\"Read the specified document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n if start is not None:\n start = 0\n return \"\\n\".join(lines[start:end])\n\n\n@tool\ndef write_document(\n content: Annotated[str, \"Text content to be written into the document.\"],\n file_name: Annotated[str, \"File path to save the document.\"],\n) -> Annotated[str, \"Path of the saved document file.\"]:\n \"\"\"Create and save a text document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.write(content)\n return f\"Document saved to {file_name}\"\n\n\n@tool\ndef edit_document(\n file_name: Annotated[str, \"Path of the document to be edited.\"],\n inserts: Annotated[\n Dict[int, str],\n \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n ],\n) -> Annotated[str, \"Path of the edited document file.\"]:\n \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n\n sorted_inserts = sorted(inserts.items())\n\n for line_number, text in sorted_inserts:\n if 1 <= line_number <= len(lines) + 1:\n lines.insert(line_number - 1, text + \"\\n\")\n else:\n return f\"Error: Line number {line_number} is out of range.\"\n\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.writelines(lines)\n\n return f\"Document edited and saved to {file_name}\"\n\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""] + "source": [ + "from pathlib import Path\n", + "from tempfile import TemporaryDirectory\n", + "from typing import Dict, Optional\n", + "\n", + "from langchain_experimental.utilities import PythonREPL\n", + "from typing_extensions import TypedDict\n", + "\n", + "_TEMP_DIRECTORY = TemporaryDirectory()\n", + "WORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n", + "\n", + "\n", + "@tool\n", + "def create_outline(\n", + " points: Annotated[List[str], \"List of main points or sections.\"],\n", + " file_name: Annotated[str, \"File path to save the outline.\"],\n", + ") -> Annotated[str, \"Path of the saved outline file.\"]:\n", + " \"\"\"Create and save an outline.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " for i, point in enumerate(points):\n", + " file.write(f\"{i + 1}. {point}\\n\")\n", + " return f\"Outline saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def read_document(\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + " start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n", + " end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n", + ") -> str:\n", + " \"\"\"Read the specified document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + " if start is not None:\n", + " start = 0\n", + " return \"\\n\".join(lines[start:end])\n", + "\n", + "\n", + "@tool\n", + "def write_document(\n", + " content: Annotated[str, \"Text content to be written into the document.\"],\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + ") -> Annotated[str, \"Path of the saved document file.\"]:\n", + " \"\"\"Create and save a text document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.write(content)\n", + " return f\"Document saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def edit_document(\n", + " file_name: Annotated[str, \"Path of the document to be edited.\"],\n", + " inserts: Annotated[\n", + " Dict[int, str],\n", + " \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n", + " ],\n", + ") -> Annotated[str, \"Path of the edited document file.\"]:\n", + " \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n", + "\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + "\n", + " sorted_inserts = sorted(inserts.items())\n", + "\n", + " for line_number, text in sorted_inserts:\n", + " if 1 <= line_number <= len(lines) + 1:\n", + " lines.insert(line_number - 1, text + \"\\n\")\n", + " else:\n", + " return f\"Error: Line number {line_number} is out of range.\"\n", + "\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.writelines(lines)\n", + "\n", + " return f\"Document edited and saved to {file_name}\"\n", + "\n", + "\n", + "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", + "\n", + "repl = PythonREPL()\n", + "\n", + "\n", + "@tool\n", + "def python_repl(\n", + " code: Annotated[str, \"The python code to execute to generate your chart.\"],\n", + "):\n", + " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", + " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", + " try:\n", + " result = repl.run(code)\n", + " except BaseException as e:\n", + " return f\"Failed to execute. Error: {repr(e)}\"\n", + " return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"" + ] }, { "cell_type": "markdown", @@ -127,7 +269,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "e09fb60f-1aac-455b-b67d-8d2e4ccfd747", "metadata": { "ExecuteTime": { @@ -136,7 +278,58 @@ } }, "outputs": [], - "source": ["from typing import List, Optional\n\nfrom langchain.agents import AgentExecutor, create_openai_functions_agent\nfrom langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(\n llm: ChatOpenAI,\n tools: list,\n system_prompt: str,\n) -> str:\n \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n \" Do not ask for clarification.\"\n \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_functions_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor\n\n\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n\n\ndef create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n \"\"\"An LLM-based router.\"\"\"\n options = [\"FINISH\"] + members\n function_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n },\n },\n \"required\": [\"next\"],\n },\n }\n prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n ).partial(options=str(options), team_members=\", \".join(members))\n return (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n )"] + "source": [ + "from typing import List, Optional\n", + "from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "\n", + "def agent_node(state, agent, name):\n", + " result = agent.invoke(state)\n", + " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}\n", + "\n", + "\n", + "def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n", + " \"\"\"An LLM-based router.\"\"\"\n", + " options = [\"FINISH\"] + members\n", + " function_def = {\n", + " \"name\": \"route\",\n", + " \"description\": \"Select the next role.\",\n", + " \"parameters\": {\n", + " \"title\": \"routeSchema\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"next\": {\n", + " \"title\": \"Next\",\n", + " \"anyOf\": [\n", + " {\"enum\": options},\n", + " ],\n", + " },\n", + " },\n", + " \"required\": [\"next\"],\n", + " },\n", + " }\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + " ).partial(options=str(options), team_members=\", \".join(members))\n", + " return (\n", + " prompt\n", + " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", + " | JsonOutputFunctionsParser()\n", + " )" + ] }, { "cell_type": "markdown", @@ -154,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "53db0c78-e357-48ba-ae5f-3fc04735a3b7", "metadata": { "ExecuteTime": { @@ -163,7 +356,44 @@ } }, "outputs": [], - "source": ["import functools\nimport operator\n\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\n\n# ResearchTeam graph state\nclass ResearchTeamState(TypedDict):\n # A message is added after each team member finishes\n messages: Annotated[List[BaseMessage], operator.add]\n # The team members are tracked so they are aware of\n # the others' skill-sets\n team_members: List[str]\n # Used to route work. The supervisor calls a function\n # that will update this every time it makes a decision\n next: str\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsearch_agent = create_agent(\n llm,\n [tavily_tool],\n \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n)\nsearch_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n\nresearch_agent = create_agent(\n llm,\n [scrape_webpages],\n \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n\nsupervisor_agent = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: Search, WebScraper. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"Search\", \"WebScraper\"],\n)"] + "source": [ + "import functools\n", + "import operator\n", + "\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# ResearchTeam graph state\n", + "class ResearchTeamState(TypedDict):\n", + " # A message is added after each team member finishes\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # The team members are tracked so they are aware of\n", + " # the others' skill-sets\n", + " team_members: List[str]\n", + " # Used to route work. The supervisor calls a function\n", + " # that will update this every time it makes a decision\n", + " next: str\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "search_agent = create_react_agent(llm, tools=[tavily_tool])\n", + "search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n", + "\n", + "research_agent = create_react_agent(llm, tools=[scrape_webpages])\n", + "research_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n", + "\n", + "supervisor_agent = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: Search, WebScraper. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"Search\", \"WebScraper\"],\n", + ")" + ] }, { "cell_type": "markdown", @@ -175,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "1a7a1260-d9f6-4011-b2b1-13fab5126997", "metadata": { "ExecuteTime": { @@ -184,11 +414,42 @@ } }, "outputs": [], - "source": ["research_graph = StateGraph(ResearchTeamState)\nresearch_graph.add_node(\"Search\", search_node)\nresearch_graph.add_node(\"WebScraper\", research_node)\nresearch_graph.add_node(\"supervisor\", supervisor_agent)\n\n# Define the control flow\nresearch_graph.add_edge(\"Search\", \"supervisor\")\nresearch_graph.add_edge(\"WebScraper\", \"supervisor\")\nresearch_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n)\n\n\nresearch_graph.add_edge(START, \"supervisor\")\nchain = research_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str):\n results = {\n \"messages\": [HumanMessage(content=message)],\n }\n return results\n\n\nresearch_chain = enter_chain | chain"] + "source": [ + "research_graph = StateGraph(ResearchTeamState)\n", + "research_graph.add_node(\"Search\", search_node)\n", + "research_graph.add_node(\"WebScraper\", research_node)\n", + "research_graph.add_node(\"supervisor\", supervisor_agent)\n", + "\n", + "# Define the control flow\n", + "research_graph.add_edge(\"Search\", \"supervisor\")\n", + "research_graph.add_edge(\"WebScraper\", \"supervisor\")\n", + "research_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n", + ")\n", + "\n", + "\n", + "research_graph.add_edge(START, \"supervisor\")\n", + "chain = research_graph.compile()\n", + "\n", + "\n", + "# The following functions interoperate between the top level graph state\n", + "# and the state of the research sub-graph\n", + "# this makes it so that the states of each graph don't get intermixed\n", + "def enter_chain(message: str):\n", + " results = {\n", + " \"messages\": [HumanMessage(content=message)],\n", + " }\n", + " return results\n", + "\n", + "\n", + "research_chain = enter_chain | chain" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "110f59bed6134685", "metadata": { "ExecuteTime": { @@ -199,7 +460,7 @@ "outputs": [ { "data": { - "image/jpeg": "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L7HktlprlFSV1BHOHEU9ypn007NHEeNG8BzddNRqOwgrZrhmMJwdcbYFm8Lv/V5f+dmf5KmWEs3hd/6vL/zsz/JUy9/7G/iXf8f/AKpedl/8H4u7REX0D54REQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQYV6+89f/uJP4SouxRodilnaRqDRQgg/wC7ClqogbVU8sL9dkjSx2nmI0K4mn4RW2kp4oIbreo4YmhjGCt5NaBoB2LnynJ4yq1FvOwmJx7nfkmUU2Mc7ijL/R+4ZegGN/VcP/2r5/o/cMh//AMb+q4f/tUpfcqofli9+u/Yn3KqH5Yvfrv2LzfRlz13zduuWOnuhqYII6aCOGFjYoo2hjGMGga0DQADzL0Wy+5VQ/LF79d+xPuVUPyxe/XfsWfof+7HZLT0ha5S1qKNOi3S1vFjhc++3+93SS4C611JugqOrb1cU7mM5AdugHNS79yqh+WL3679ieh/7sdknpC1ylH+QcIcHyy6y3O9YjZbtcZQ0SVVZQRyyvAAA1c4EnQAD9i17uAPDRwaHYDjhDRo0G2Q8hrroPF85P8AzUofcqofli9+u/Yn3KqH5Yvfrv2LSPsuuNkXvmprtid9PdDl8axSy4bbjb7DaqOzUJeZe5qGBsMe86au2tAGp0HP5l0fC7/1eX/nZn+Spl6/cqofli9+u/Yt3i+J0eJQVkdJLUzmrn7omlqpese5+xrO3zbWNH7F25HkeqVV11V52dGHHnE8fc5spyqi7bzKYbtERdryxERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBXfoIfATJ+frp/mnqxCrv0EPgJk/P10/wA09WIQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREFd+gh8BMn5+un+aerEKu/QQ+AmT8/XT/NPViEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEXE3LiWx8rorFQOu5adrquSTqKXX8V+hL/wArGkeTXtWsOaZa7mKOyx/imSZ2n7dB/wBlvoZj8UxHvn6OmnJ7tcYxSklFGvhnl3xa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EREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERB//9k=", + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAGWAYUDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAYHBAUIAwIBCf/EAFwQAAEDAwICAwkIDQkGBAUFAAEAAgMEBQYHERIhCBMxFBUWIkFRVpTTN1R0dZW00tQXIzI1OFVhcYGSk7LRMzZCUlNikbGzGCRDcnOiNEWDoQklJidjRoKFpMH/xAAbAQEBAAMBAQEAAAAAAAAAAAAAAQIDBAUGB//EADcRAQABAgEJBgYCAQQDAAAAAAABAhEDBBIUIVFSYZHREzFBcaHhIzNTorHBBYEVIjJC8JKy8f/aAAwDAQACEQMRAD8A/qmiIgIiICIiAiIgIiICIiAiIgIi+XvbGxz3uDGNG5c47ADzlB9LErLrRW4gVVZT0xPMddK1n+ZWhjbWZptP3RU2yxb/AGpkDurnrW/1nP8Auo4z2gNLXnkSQDwnLosDxy37mCx29rz91K6na6R/5XPILnH8pJXRmUUasSdeyOv/ANW0eLJ8KrJ+OKD1pn8U8KrJ+OKD1pn8V++C1l/FFB6sz+CeC1l/FFB6sz+CfB4+i6n54VWT8cUHrTP4p4VWT8cUHrTP4r98FrL+KKD1Zn8E8FrL+KKD1Zn8E+Dx9DU/PCqyfjig9aZ/FPCqyfjig9aZ/FfvgtZfxRQerM/gngtZfxRQerM/gnwePoajwqsp/wDOKD1pn8VsIKiKqjEkMrJoz2OjcHA/pC1/gtZT/wCUUHqzP4LAn0/sheZqGkFlrANm1drAp5Bz358I4Xc/I8OH5Etgz4zCakjRaK13WrpLiLTd+B1S5pfS1sbeGOrYO0Ef0JW+VvYR4zf6TY96tVVM0TaQREWCCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKM507uymtllO3BeK1tJMDv40IY+WVvLyOZE5n/71JlGcvb1F0xevPF1VNcwyQhu+wlhkhb+b7ZJGN/yrfgfMifHXbztq9VjvSUANAAGwHYAv1FB7jrrptaLhU0FfqFitFXUsroJ6aovVNHJDI0lrmPaXgtcCCCDzBC0InCrm7632236jy4XRWDIL9cqUUrrhU2qjZJT24VDiITM5z2u2IaXEsa7ZoJO2xWQ/pA6XROAfqTiDCQHbOvtKORG4P8AKeUEFVRqjZr1qVl9nyXTKwwy13HSto9R7PkFP3JJTNn/AN5gqYWu3qIwBI0N4X+MeRaQdwl+mutt9zDVbULGa7ErpT22w3IUlLcWRwCKNgpo5Npj17nl8hcXM4WbcDmcXC7iA3OF6827Lcup8brMaybErpWU0tZQMyKgbTtrooy0SGIte7xm8bCWP4XAO32UUgxXPsZ1J1Sgs9lDrbmRbW27J462EMttQ23tgAmgcesdtLEwgsa4bO57bEKA6U6L5Nj+pmmeQS6bjH32ilrKPIbxUXiCrrblUTU4HdTnB5dJH1jDzc7j+3cmANKCYXrpYm86KZPnWHYZkVRTUFpqayluFypYGUhmidwFjh3QHvDDu5xYCOGN4a4uHCre02yyszXEKG619juOP1MrG8VLcxCJHeKD1jRFJI3gdvy3dv5wFVWG6O5CehvU6c3CnjtWSVVjr7f1UsrHsjllM3BxPYXDbx2kkE9vnUlw/V6hxTFrZRalvtOmd6ihZDHQXq/UXFUsYxrTNGWyc2F3EBvz5cwN0FsIq/8A9oTSzbf7JeH7efv9S+0UgxTULFc87q8Gcms+RdycHdHemviqup4t+Hj6tx4d+F22/bwnzIPnPqZ8mL1dXAB3bbmmvpXHflJGC4DceRw4mn8jiFvKSqZW0sNRESYpmNkaT5iNwtTnFb3vw69TcLnvFJI2NjRuXvc0tY0DzlxA/StjaKHvZaaKj3Du54GRbjy8LQP/APF0T8mL7Z/S+DLREXOgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICw7xaae+2uqt9W0up6mMxv4TwuAI7WnyOHaCOYIBWYisTNM3jvEftV/ko6mO03uSOG5k8ME/3EdcPI6P+/sPGj7Qd9t27OO4db6V7i51NC5xO5JjBJK/LhbaS7Uj6Wtpoqunf91FMwOafNyK0AwGCn5UV4vVvj8kUVe6RjfzCXj2H5ByC3/Dr1zObPlq9vJlqlv+9tJ71g/Zj+C9442QsDI2tYwdjWjYBRjwIn9Kb9+3i9kngRP6U379vF7JOzw9/wBJLRtSlFFvAif0pv37eL2SqbSS85Bm2qerWPXHJ7qKDF7lS0lAYXxteWSQcbuMlh4jv2bAJ2eHv+klo2ugl5TUsNQQZYY5COQL2g7KN+BE/pTfv28Xsk8CJ/Sm/ft4vZJ2eHv+klo2pB3tpPesH7MfwRwpLZBLO7qaSFreKSQ7MaAPKT5go/4ET+lN+P8A68Xs17U+BWts0c1a6rvEsZBYbnUvnY0g7giMngBB57hu/Ic+QTMwo767+UdbJaHlHvmdwpanq3NsNHKJoTI0tdWTtPiSAH/hMO5aT924NcNmtaZJQiLXXXnWiNUQTIiItaCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgLnfo6e7/wBIj47t/wA1XRC536Onu/8ASI+O7f8ANUHRCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIC536Onu/9Ij47t/zVdELnfo6e7/0iPju3/NUHRCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAi+JZWQRPkke2ONgLnPedg0DtJPkChvhbfrqxtTabZRR0EgDoX3GokZLI09jjG1h4ARsQCeLY8w07hbsPCqxP8AatrpqihHfzMPeNj9am9mnfzMPeNj9am9mt2i17Y5wWTdFCO/mYe8bH61N7NO/mYe8bH61N7NNFr2xzgsm6KEd/Mw942P1qb2ad/Mw942P1qb2aaLXtjnBZN0UI7+Zh7xsfrU3s07+Zh7xsfrU3s00WvbHOCyvumxovPrfoFeLVb2PlvNrkbeLdCz/izRNeDHt5S6N8jQP6xav5NaAaQ1muOreP4fSh8cVZPxVlQwb9z0zPGlf2bbhoIG/IuLR5V/abv5mHvGx+tTezVO6P8AR5l0X1HzbMLLQ2Z9Zksoc2GSaQNoIy7jkiiIj34XSbO28gawD7nctFr2xzgs6NtFpo7BaaK2W+nZSUFFAymp6eMbNijY0NY0DzAAD9CzFCO/mYe8bH61N7NO/mYe8bH61N7NNFr2xzgsm6KEd/Mw942P1qb2ad/Mw942P1qb2aaLXtjnBZN0UI7+Zh7xsfrU3s07+Zh7xsfrU3s00WvbHOCybooR38zD3jY/WpvZp38zD3jY/WpvZpote2OcFk3RQjv5mHvGx+tTezWys2UVclwit14o4aOqnDjTy00xlhm4dyW7lrS14A4uE77jfYnY7Y1ZNXTF9U+UwWSVERcqCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiCPaiOLNP8mcDsRa6og/8ApOWNQgCipwAABG3kPzLI1G9z3KPiuq/0nLGpSW0EJaOJwiBA8/JejhfJ/ufxC+DTXfUXE8fvMNoumT2a23abbqqCruEUU8m/ZwxucHHfybBbWivNBcqqtpqSupqqpoZBDVQwTNe+neWhwZIAd2uLXNdsdjsQfKqG6KGF47lmjVqyy82m33vJb/UT3K6XGupmTTPquveOElwJaI+ENa0cm8PJRC7z5ljt86QuW4zlMdnix6vZcnWx9uinZXOitlPI5kr3+M1pa3hHV8JBJO55AY502ujrRfJkYJGsLmh7gSGk8yBtudv0j/Fcya0aw5RB3dcMJv8Adu67TYIrzWWS32SlqqSlLo3StNZUzOa4Ne0cmRHjAaXbHcBZVRBd846SWA3mjyWusDa7CJbgaalp6aVrWGopHPh3kiceF/GNz90OAcJbud7nC6xqlhbsifYBl9hN9ZJ1LrWLnD3U2T+oYuLiDvybbqULiS5Y7cc/i1wxC0aa+EtzumW1kVPktS6mipbY91PTAPMj3daHR7dYAxp34hsdyQrSwrIs3ZdNRqy6ZfUVFgwWs6iK3U9FT9ZXNioIZZGSyuY53CXO3Bbs/dzt3EbASKh0Si5ux/PdQLDbdK8xv2T096tucV1JSVVhjt8UMVCKyF0sJp5WjrHdWQ1rusc7iBcRwqN2bPtULngum+T+HwZLlGQmwz0Rs9KYYYnSVDBM08IcZR1APN3ASfuNgQbnDrVCQASTsB5SuXsg1Mz2ww5HizcpM93tWa2Syw3+S30/WyUlc2B5bJEGCMuaJXN3a1u4A7DzX7qhecttVs1Y09u+VT3uF2DVOQ0N4dRU8NVExpkjmppGsYI3NeGgBwY1wDn7HcBwZwv7KNR8SwgUhyPKLLYBWBzqY3S4Q03Xhu3EWcbhxAcTd9uzcedeWL6o4ZnFdJRY5l1iyCsijMz6e13KGpkZGCAXlrHEhu7mjfs3I86r7HcKYOjvF3/rPCyobYDPTVN0oqYPpWOpG7RR9XG0cLeHkSC4+UnYKo8Zyq7Y7gui+L4RaKikvF4xCK53C6WK3UMteYY4oG8DO6nxxnie/dznFxHCNmnckSapgdiIuZznOrMdHhlnu9RUYxcbrlktpbcayho3VNXb+4pZWyPijfJFHKHtI8U7bxtJaQSw6W8Z/qZi+I6oX+TOzcRp/dxTRU0topWC5whkEzhUOawEO4Z+AGLq9uHc777C5w6yWGLzbzdzaRXU3fQQCqND1zevEJdwiTg34uDiBHFttuNlRV0uuoGT6gat0tmzp9gpMWNGbbRd7KWaFzn0LJntme9heWF5PY4OG52O2wGDpbnoznWOy5hXMjtvfHTGkuFQ0naOEuq3ufsT/RB32J8iZw6PXyyRkoJY5rwCWktO+xB2I/xXL2nGreZV+pmJW+a/Xq/4rl0Fa2kulzsNLbouKKAzRzUgY4ylhDTynZzDmkErVaY5RfdF+i1kuZi91eRSQV9fBRWusp6dkMNQ+7SwCXijYx7uJ7w9wc/btDeEbbTPgdcLSX07ZBiBHb32PPzf7pUKpNK71qszPKOkyCiv1wxuppZTWVl/obZSOo527GPqe46h5cx3jNLXtJHiniPNW1fvv/iHxsfmtQt+FN5nyn8Syp70/REXksRERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBHdRvc9yj4rqv8AScvCi/8ABwf9Nv8Akt5erZHe7PX26VxbFVwSU73Ab7B7S0/5qEx3uqslPHSXS03I1ULRG6WhopaqKXblxtdG07A7b7EAjfZejgf68PMp77su+EAqejLYe/FyqbXkeV45bLnUurK6xWW7GmoZ5XHeR3CG8cfGfuure3dSKr0ZstbbNQ6F9VXiLOA8XJzZGcUXFStpj1JLPF8RgPjcXjbns5L9y/W7FNPrdFX5PU12PUEsogjqrnbainidIQXBgc9gHEQ1x27dmnzFRH/bM0aP/wCubf8A4O/gt3YV7pmzsbG9dG3H7zVVzxecgt1Jc6CC23WgoK1sUNyiij6phm2ZxB3B4pMbmbjkQQsu56B2uup8RNNkOQWm54zQm20t2t9TFHVTUxawOim3iLHg9Ww8mg7jcEKU0efUFwpIKqmoL3PTTsbLFLHZqote0jcOB6vmCCCvbwzp/wAVX75Eq/Zp2Fe7JmzseOF4Bb8FqMlmoJqmZ1/u8t5qhUua4Mmkjjjc1mzRszaJuwO53J5+b5xrT214vXZVUwOnqjklebhWxVRa+MPMMcJawBo8Thibydudyee3IZHhnT/iq/fIlX7NPDOn/FV++RKv2avY4m7JmzsQfFujfjuLXmyVbbtfrpb7C90llstyrhLRW1xaWgxN4A4lrXOa3rHP4QeWy2Fu0IsFsxPEceirLk6ixi7i9Ucj5YzJJMHzP4ZDwbFm87+QDTyHPt3lHhnT/iq/fIlX7NPDOn/FV++RKv2anYV7pmzsVVrLoRPf2V1Zjz7hJcr/AJTZLncnR1UURpIaUxROlgcQOEtij49iXEuHIdgUuxjQmw2FuSSXCuu2VXDIaXuC4XG/VIlnkpeFzRA3gaxrI/HedmtG5dudzspP4Z0/4qv3yJV+zWtturFgvNxuVBQC5VtdbJGxV1NT2uofJSvcOJrZGhm7CRzAO24TsK73zZM2djww3Smnw7CqvF/CC+Xq2zU5pInXWojllpoer6sRxubG3kG9nEHHzkrT3Do9Y/V4vhlqprlebTW4jSto7TfbfUsir4ohG2NzXO4Cxwe1reIFmx27Apj4Z0/4qv3yJV+zTwzp/wAVX75Eq/Zp2Fe7JmzsaGPR63mDEm1l6vd1qMbuUl1p6u4VbZpp5nxysIlcWc2bTO2a0NA2aBsBssW9aEWC+41n9kqKy5MpM0qjWXB8csYkieYoo9oSWENG0Lfug7mTz7NpR4Z0/wCKr98iVfs08M6f8VX75Eq/Zp2Fe7JmzsU/V9Hy4ZnqjqhX3i9ZDYMdvs1CyKCzXGKKK5QMoo4pWyjhc9o4mubyLCRvzI2Vg1OiONS5FZLtTMqrcLXaX2IUFHNw0tVQOaWinmYQeJrSeJuxBBHaRyW/8M6f8VX75Eq/Zp4Z0/4qv3yJV+zTsK92TNnYgeN9G2zY1d8WuLMlyevkxdxbZ4q6uZJFSQGJ0TqcMEYDmFjgN3bvHC3Z4255dD0ecbpKXKLZLW3avxjIe6DUY3V1LXUED55BJI+EBgexxfu4eOQ0uJAHkzsy15w3TuGmmymvqcdiqnOZBJdKCenbKRsSGl7BuRuOzzqL/wC2bo16dW//ALv4J2Fe6Zs7Ex0/0s8AKp8xy7KMjb1ApoYb7XtnjgYCD4rWsbu7kBxv4nbct+ZW9v33/wAQ+Nj81qF5tzWmc0EWq/7Eb87JVg/6ay7dTVOT3y2VZoqmht1tldUdZWRGJ88pjfG1rWO8YNAkc4uO3MNA33dtlFE4UTVVFotP4IiY704REXjMRERAREQEREBERAREQEREBERAREQEREBERAREQEUXzfVHENNaTunKcmtdgjI4mivq2RPf/wArSeJx/IAVTsnTMtuVyPp9L8GyrUybfhbWUNC6jtwd5nVM4Ab+qR2oOilj3C40lpo5auuqoaOliHFJPUSCNjB5y48gufRaukjqV/4274vpFbJP+FbYDd7k0eUOe/aH8m7eztWTb+hXhdwrIrhnl2yLU+5xnjbLk9zkkgjd/cgYWsa3+6Q4INhkvTM0ws9xda7NdKvO75/RteH0b7lK/wDM5n2v/vWqGo+veovLFdN7TgFuf9zc82rzLOW+cUtP4zHDzPdsrwxrEbHhlubb7BZqCyULeymt1MyCP9VgAW2QcOdLHox55kmg2TZDlepF3zm+2SJt1prPSU1PbrZEI3Dr3mLmXlkDpy08QcdtgCTwmPdCjoCm1uoc91Qt3++DaW24zWRfyJ/oy1LT/S8oiI5ci7n4rf6CIgIiICIiAiIgLnfo6e7/ANIj47t/zVdELnfo6e7/ANIj47t/zVB0QiIgIiICIiCMakabY9qziFdjOT26O5Wqrbs5jx40btvFkjd2te3fcOHYv5cZj0Acnw3pDYlh5irLzhOQXSOGG+0pZE9lMAZahj3O3YydkEczwCDx8G7WuO7R/W5EHO02m2venEb3YdqTbM9oYwertWc0PDOB5hVwEOe4+d4AWv8A9rbKtPvteq+j2SY1EzlJebDw3WgH99z2bGMfk3cV0yiCttOukjplqv1TMYzS1XCqk+5opJuoqj/6MnC//tVkqsNRejLpdqr1smSYVa6urk5uroIu5qknzmaItef0khVv/sp5np99s0r1kyGxwM5ssuSBt2odv6jA8Axt/KA4oOl0XNP2Wtf9M/FzTSuhzm3x8nXbA6wmXbz9yS+O9x/u8IW/xDpsaUZPX97bhfJsMvQIElryumdb5Yz5nOf9r3/JxoL3RY9DX0t0pIqqiqYaullHFHPA8PY8ecOHIhZCAiIgIiICIiAiIgIiICIiAiIgKB6t64YdofaqOuy65voW10hho4IaaSeWpkABLGNY0nfmO3Yc+1Txc79I0luvPR42JH/z2u7PgqDHHSM1P1E8TTbRi6RUj/uL1m87bZAB5HCDcySNP9077eRfv2DNZdRfG1B1ilsNE/7uzaf0oo2t84FXIDKR5NiF0eiCmsH6IGk+C1fd0OJ096u5PG+6X97rhUPf/X3lLg135WgK4oomQxtjjY2ONgDWsaNgAOwAL7RAREQEREBERAREQEREBERAXO/R093/AKRHx3b/AJquiFzv0dPd/wCkR8d2/wCaoOiEREBERAREQEREBERAREQFoMvwHGs/oO4smsFtv9LsQIrjSsnDfyt4gdj+Uc1v0Qc5V3QgxSy1ctfpzkmTaXXF54yLBcpHUsjv/wAkEhcHD+6CAvEU/Sa0x/k6jFtY7XH/AEZm957m8DzEfaRy8p3K6TRBzlTdNmwY5PHR6mYhlOl9Y5wZ193tz56F7j5I6iIHiG/l4QF0VDKyeJksbg+N7Q5rh5QewqiOnYS3onagkEj/AHanHL4VCrrsP3jt3waP90IM9ERAREQEREBERAREQEREBc7dI73eejv8fVvzVdErnbpHe7z0d/j6t+aoOiUREBERAREQEREBERARF8ve2Npc5wa0eUnYIPpF492Qf28f64TuyD+3j/XCtpHsi8e7IP7eP9cJ3ZB/bx/rhLSKo6UGvFV0c9NWZfT4u/Kom18VJU07azuUU8b2v2lc/q38uNrGbbdsg5+Q8G6X/wDxC58P1Jz6+0+nL7rUZpcKaojoI7wWup3Mi6oMBFO4yFxO/Y3zbFf0p1BxOy6lYRe8WvD45LbdqV9LNs4cTQ4cnt8zmnZwPkIC/m/0KuipXU3SZvkmVwR9waf1Z3dINo6qs3Pc7mb9rdh1wIPLaPf7pLSP6cWOqra2y2+ouVE223GanjkqaJk3XNp5S0F8Yk2bxhp3HFsN9t9h2LOXj3ZB/bx/rhO7IP7eP9cJaR7IvHuyD+3j/XCd2Qf28f64S0j2ReIq4Cf5aP8AXC9ktYERFAREQEREBERAREQUJ07fwTtQfg9P86hV2WH7x274NH+6FSfTt/BO1B+D0/zqFXZYfvHbvg0f7oQZ6IiAiIgIiICIiAiIgIiIC526R3u89Hf4+rfmq6JXO3SO93no7/H1b81QdEoiICIiAiIgIiICIiAqzt1qt+awyXe80UF0mlnmbCysjbKyCNsj2Naxrhs3xRzPaSTzI22sxV3gP814P+tUf6716GTTNNFVVOqbx++jKNUPr7H2Lejdo9Qi+in2PsW9G7R6hF9FVtVak59nua5LaNOaTHqe043Ui31t2yITyCprOBr3wwshc3YRhzQ57iebtg07br6ybXxunOp9txzL5qKjoZsaFzlkoaWoqZn1gnDHsiawOc6IND3fcbgDckBb9IxN6eaXnasf7H2Lejdo9Qi+in2PsW9G7R6hF9FaO965YRj+OWa/VV7661XlhkoJ6GknqzUNABLg2FjnbAEb7gbeXZRzOukrjeJU2A19EZb7aMrrHQxV1vpqiobHC2J73Pa2KJ5c/ia1vV8nc3HbxHbO3xN+eZedqf8A2PsW9G7R6hF9FPsfYt6N2j1CL6KgGUaoZTkOeR4fpzS2k1tPbIrvcrpkUc/UU0cxIghELCx5lfwucQ4t4Q3nuTsMfGekPS2m25FTakNpcav2OXOntVeaFstRTTOqGtdTSxbNLwyQO7HDxSDuU0jE3p5l52rH+x9i3o3aPUIvop9j7FvRu0eoRfRWosGtGGZJbr3W0t8jghsjesubbjDLRS0bNi4Pkjnax7WkAkOI2Ox2JWFZukBgV+orzVUl9IZaKGS51kdTRVFPMylYCXTNikja+RgA+6YHDsHaQnb4m/PMvO1JPsfYt6N2j1CL6KfY+xb0btHqEX0Vp8R1pwzOr2LTZLyKuufTmrhY6mmiZUwggGSF72NbM0Fw3dGXAbrU2jWaO6a53fAxSBtHS0QdT3HyTVkYjfUwA77EsiqaV23bzf27cnb4m/PMvO1Lhp/i4IIxu0Ajy9wxfRWXigbYcpfY6T7XbJaI1cNKPuKdzXtY4MH9Fp42nh7AQSANzvFdN8/uGYZZqHbK2Gmip8dvLLdSuga4OfGaaGUmQlxBdxSOG4AGwHLyqUW/3TaX4nn/ANaFWa6sSiqKpvFp9FiZnvTlEReOxEREBERAREQEREFCdO38E7UH4PT/ADqFXZYfvHbvg0f7oVJ9O38E7UH4PT/OoVdlh+8du+DR/uhBnoiICIiAiIgIiICIiAiIgLnbpHe7z0d/j6t+arolc7dI73eejv8AH1b81QdEoiICIiAiIgIiICIiAq7wH+a8H/WqP9d6sRV7g0boscjjcCHx1NSxwI22cKiQEfoIK78n+VX5x+18FL4/erzoBm2e2+4YbkeR2HIL1LkFrueOW81vjzsYJqeZrTvGWvZ4rneKQ7tGykljprteOkTa8kqrBcLTSz4N1UndUYc2mqHVjHmndI0lnWAbnhDjyG6uNFlZHIeMUGXY3gen1ouduzK2YmJ7465U+MUk7Lh15r5HUjJOrAmihdG57g5mwPibkNIXzimN5FielOnFVUYpkLn4nnNdWV9s7lfUVzKWV1ZwysaCTOAKmIl0Zdv42xOxXXyLHNHP1ddrrpzq/XagRYnkN6xbMbLRR1MVttz5q+31dPx9W2WmHjhro5SCQDwubsdu1Vvm9De4aK76mXyw3C0uyLO8bkorE+MOrhSUkzGR8UYJ2lkJeer35bgbrslafJsRtOY01FT3ek7rioq2C4wN6x7OCoheJIn+KRvwuAOx3B8oISabjljVbDMp16uWbZLj+M3q0UcFjoLbTUd3jfbaq8SQXBtXI1jXbPYAxvA17uHdz+XLmNzV4lZMwwnO7jaMV1IbkVPidyo6R+XTV8zi6eBwdTwR1Ery95LGb8DSDs3Yk7LqZEzRz7fqG7WC/aKXtlguldFYcfuZrYqOjkkkjd3DDwwuDWkh73M4WtPMuGwG6gVHp7qliOFYbnNYKG5XG3XoZLX2W22apN3f3c8trIC7rnB5ZFUOHCIh/IM/q8+v0VzRVGjNmuFszzV2prKCqpKeuyKOeklqIXMbUR9xU7eOMkeM3ia4bjcbgjyKwbf7ptL8Tz/60K2y1dtYX6lRPHMR2iUO/JxTR8P+PA7/AAW2nVTV5SsJuiIvKQREQEREBERAREQUJ07fwTtQfg9P86hV2WH7x274NH+6FSfTt/BO1B+D0/zqFXZYfvHbvg0f7oQZ6IiAiIgIiICIiAiIgIiIC526R3u89Hf4+rfmq6JXO3SO93no7/H1b81QdEoiICIiAiIgIiICIiAo3dcJira2WrobnXWSeY8UxoeqLJXbbcTmSMe3i228YAE7DcnYKSItlGJVhzemVibId4AV/pne/wBjQ/Vk8AK/0zvf7Gh+rKYot2k4nDlHRbyh3gBX+md7/Y0P1ZPACv8ATO9/saH6spiiaTicOUdC8od4AV/pne/2ND9WVVaU3fJc61P1VxyuyqvhosUuNLSUUlPTUgkkZJD1jjITCQTv2bAcl0Mud+jp7v8A0iPju3/NU0nE4co6F5Wv4AV/pne/2ND9WTwAr/TO9/saH6spiiaTicOUdC8od4AV/pne/wBjQ/Vk8AK/0zvf7Gh+rKYomk4nDlHQvKHtwGvDgTmV7IB7Opoef/8AWW7sGOUuPxS9U6WoqZyHT1dS4OmmIGw4iABsB2AAAc9hzK2qLCvHxK4zZnV5RH4S4iItCCIiAiIgIiICIiChOnb+CdqD8Hp/nUKuyw/eO3fBo/3QqT6dv4J2oPwen+dQq7LD947d8Gj/AHQgz0REBERAREQEREBERAREQFzt0jvd56O/x9W/NV0Sudukd7vPR3+Pq35qg6JREQEREBERAREQEREBERAREQEREBc79HT3f+kR8d2/5quiFzv0dPd/6RHx3b/mqDohERAREQEREBERAREQEREBERAREQUJ07fwTtQfg9P86hV2WH7x274NH+6FSfTt/BO1B+D0/wA6hV2WH7x274NH+6EGeiIgIiICIiAiIgIiICIiAudukd7vPR3+Pq35quiVzt0jvd56O/x9W/NUHRKIiAiIgIiICIiAiIgIiICIiAiIgLnfo6e7/wBIj47t/wA1Xt06NHqrWXo9XeitwfJdrNK290cDf+M+Fj2vj28pMckmw8ruFfyc0M0ortbNVMew+h42d8KgConY3fuenb40snm8VgcQD2nYeVB/eVFr8fsVFi9httmtkAprbbqaKjpoGncRxRtDGN/Q0ALYICIiAiIgIiICIiAiIgIiICIiChOnb+CdqD8Hp/nUKuyw/eO3fBo/3QqT6dv4J2oPwen+dQq7LD947d8Gj/dCDPREQEREBERAREQEREBERAXO3SO93no7/H1b81XRK526R3u89Hf4+rfmqDolERAREQRu9ZXUU1e+gtNvFzq4djUOln6iGHcAhrn8LiXEHfha07DYkjdu+u8KMt9HLP8ALUv1VYuOniuuUE9vfZ4J8p2iiA/9gB+hbG7XehsNtqbjc62nt1vpmGSerq5WxRRNHa5z3EBo/KSvVzMPDtTNET3d99nCYZao8Hh4UZb6OWf5al+qp4UZb6OWf5al+qrYMe2VjXscHscN2uadwR5wvpLYX04+7qX4Nb4UZb6OWf5al+qp4UZb6OWf5al+qrZLSZNnON4U2ndkOQWqwtqHFsJudbHTCUjtDeNw3PPyKfC+nHOrqX4Mnwoy30cs/wAtS/VU8KMt9HLP8tS/VVnU1TDWU8VRTysnglaHxyxuDmvaRuCCORB86+pJGQsc+RzWMaNy5x2ACtsL6cfd1L8Gv8KMt9HLP8tS/VU8KMt9HLP8tS/VVskS2F9OPu6l+DW+FGW+jln+WpfqqeFGW+jln+Wpfqq2EkjIWOfI5rGNG5c47ABaPK8/xfBGUzslyS0Y62pLhA6610VKJS3bi4escOLbcb7dm486nwvpxzq6l+DM8KMt9HLP8tS/VVReiPRyn0O1PzbM7VY7NPNkEhFJSd85I22yBzuOSFhFMeIOfwnsGwY0bdpN2Ytm2O5zRy1eN362ZBSwydVJPa6yOpYx+wPCXMcQDsQdj5wt0nwvpxzq6l+DW+FGW+jln+WpfqqeFGW+jln+Wpfqq2S+ZJGQxukkc1jGjdznHYAeclW2F9OPu6l+DX+FGW+jln+WpfqqeFGW+jln+Wpfqq9668UFsno4aytpqSatl6iljnlax08nCXcDAT4zuFrjsNzsCfIstPhfTj7upfg1vhRlvo5Z/lqX6qnhRlvo5Z/lqX6qtkiWwvpx93Uvwa3woy30cs/y1L9VTwoy30cs/wAtS/VVqbVqlhd9vT7PbcvsNwu8b3RvoKW5wyzte0kOaY2uLgRsdxty2UoU+F9OOdXUvwa3woy30cs/y1L9VTwoy30cs/y1L9VWyRW2F9OPu6l+DW+FGW+jln+WpfqqDKMsB3djlpLR2hl5kLv0A0wH/utkiWwvpx93VL8GwsF+hv1LI9sclNUQv6qopphs+J+wOx8hBBBDhyIPJbNQzEDtmmTt7B1VG7YeciQb/wCAH+Cma4ceiMPEtT3ap5xckREWhFCdO38E7UH4PT/OoVdlh+8du+DR/uhUn07fwTtQfg9P86hV2WH7x274NH+6EGeiIgIiICIiAiIgIiICIiAudukd7vPR3+Pq35quiVzt0jvd56O/x9W/NUHRKIiAiIggGOffTKPjaT/SiUE6Vx26N+om/wCJ5/8AJTvHPvplHxtJ/pRKGZ90fbHqLfqmvuV5yCnoa5sLblZKOv6uguAiO7BNGWk9mwPA5vEAN9162Ne+rZH4We9CfDbK7JrLQ2zIsiqcVxWpdQwWGJtpimobqXQt62CWqIL4qgycTWtJYCANg4laB+rGffYym1j8IoWWGO7mIYh3vi6o0La/uQgz7db1+wL9w7h32bwK3sk0Qt2W5hBe7pkGQ1VHDW09xZj7q1ve0VEAb1TxHwcQALWu4Q8NLuZB3K1cvRpxmW7uldc753gdc+/DsW7sb3rNX1nW8fV8HHw9Z9s6vj4OLnwrntUitMj1K1Ct+KanZtBloZR4dktRRU9k7205iqqWOSIuZLIW8e/DIQ0sLSNgSXbqUad47aM8111ircmttHeLjbKuitVJBcIGzCloTStkaGNcDwiR75HHbtI/IptctCLBdMLzbGJay5NoMtuE1yrpGSxiWOSXg4hESzYN+1t2Dg48zzK+c30JtWYZQ/JKO+5BiN8mp20lXW43XCmdWRNJLGyhzHtcW7nZwAcN9t+xW0ik9KsruWF5PS4jYKvubFWamXSyw0wY17G0baB9QadhcCWtZPxbcJBG22+24X70h8gyDKcd1os0l8npLdYL5jzKOKGngPDHKKR72EuYSR1snWbk77sA34SWm6Jej5i0eEWPGrY+4WNllrRcqC50FQO7Yqrx+OYySNeHueJJA7ja4EOPLkNsWDo342bFm1rr7le7uzLnwTXGprqxrpxLExrWSRvaxvAQWNcBzaCAAA0BombNrDS5tNmlrzzTHCaHPa+DvrS3eW43d9uon1NQYRA6IhvU9Wwt6wt8VmxHaCeajOP6vZnFnNhxG53eOtqKHNaiw19wipI4++VL3tfVRF7diI5ASwO6vh5s8xIW8zHQu/XbONNhTZRk01BZ4LsKzJO7qZtfC6ZtP1Tf5MNeDwPGwjIG3PbkVJY+jhjVNidHZ6WvvNLXUt2N9ZkDKsOuTq9wLX1D5Hsc17nMcWEOYWlvLbkEtNxUHSHyDIMpx3WizSXyekt1gvmPMo4oaeA8McopHvYS5hJHWydZuTvuwDfhJad3rTZ79bdTdE6CKCm1DvMbb3uMhfDRsqd4YzxPMUBY0taeXDHz4R5dyrDg6N+NmxZta6+5Xu7sy58E1xqa6sa6cSxMa1kkb2sbwEFjXAc2ggAANAaNvbtHaOlvWJXeuyC+3y5413Z3LVXKeJ75hUsax4l4Ym78IaOHh4dvLumbP/fMRZl9vmMag6VWTvXbcVZfhdX3i02oRzQvfDC10REvVMcduR3Ab27HcBQ69aqZxU3SuttvyBlBI/U9uMQzvoYZepoHW0SlgaQOIiQl4cTvvsCS3dpurUDTOh1AmstXJcblZLtZp31FBdLTKxk8Jewskb9sY9jmuadiHNPYPMoxZOjjjtkbBwXS91kseUDLTNWVTJZJK0U/UEOcWbmMt58Pbv2EDZqsxIry76vZthcuXYcbxDfcghyO0WO0X240kcfVtuEbXB80cQYxxi2k24Q3i8XcL16RuH5XZujhm7Lxn9ZkBIpZGSPtlLTva0TND4zwM2LXFzSOQcODbiIJVnZDoNjGVVGYzXI1szsnfRzVHBOIzSy0rA2CWnc0BzHtIDtyTzHm3C+YtDbfVYhkWOX/ACTI8toL5Ttpp3Xqta98TG78Ji6tjGsdud+Lbclrdydglp7hFM6pb/iea6N0tXlE+RR1F8npap1yttDxzE0tRKyUOZC0xPYGhgMXDu0nffcqHP1Yz77GU2sfhFCywx3cxDEO98XVGhbX9yEGfbrev2BfuHcO+zeBXHS6N0gZipuOR3++1WOXJ1zpau51ET5ZHugfDwSFsTQWBsjiNgDvzJPPfTS9GnGZbu6V1zvneB1z78Oxbuxves1fWdbx9XwcfD1n2zq+Pg4ufCkxIrTI9StQrfimp2bQZaGUeHZLUUVPZO9tOYqqljkiLmSyFvHvwyENLC0jYEl26lsmbZVbNfprVk+R1OM2CprYocfohaYpLfd4zCC6M1ZBfHUdZx+IXN5NHCHbqY3LQiwXTC82xiWsuTaDLbhNcq6RksYljkl4OIREs2Dftbdg4OPM8yv286IW7Is2pchumQ5DXU9LcIrrT2KatabfFVRNAjkazg4xwkcQbx8PFz25paRy7iuIXbU/AKnGLHp65txGY1lU3PKh1NFHQtjuj5HSRODuuc8NaY+ENHb27KxKzWjKqHUuzV1pv11yXDLjlTbBKZrHS09rjEkrouGCoDxUSPjeAC/Z0bix3Mcgr/0+wC36bWGW0WyapnppK2pri6rc1z+snmdK8bta0cIc8gct9ttye1QJ/Rfx4yU8ceQZLBbaG6tvVttUVcwUtvqhP1/HEzq93AvL/FkLwA92wB5iZsx3CPYhedTsvp9TbxQZM2onsV7vFssdgNFTMhqTExwgbPKWcewe5m3C5v3PjOdxcovFrxkmPaY1O2QXHI88qbrbbLJabrZYKKss9TUkg7wgxMlaQHGIucGOIG7yN9r6smmFux6x5VbKGvuUDMirqy41FTHOGTwS1P3Zhe1o4OH+ieZB8pUVj6M2L1NpyKlvdxvmTVt8FM2ou11rAayLucl1OYnxsYIzG5xcCBuSTxEq2nwFaXrUHV/DMD1Dr7hHeoqKgx6Wvt18yChtkNTBWscB1QjpZZI5GFp4gXMGxYQd9wr+0+td+t9kbNkWQPv9wqwyd3+6xQQ0xLBxRxBjQSzffYvLnflUafoRSV2E5PjN4y7Kcgpb/SCinqbpXRyTQRgOH2kCIMafGO7iwk7DffYKyaeFtNBHE0ktjaGAnt2A2ViJgYWIfz2yj/oUX+UqmiheIfz2yj/oUX+Uqmi1ZV83+qf/AFhZERFyIoTp2/gnag/B6f51CrssP3jt3waP90Kk+nb+CdqD8Hp/nUKuyw/eO3fBo/3Qgz0REBERAREQEREBERAREQFzt0jvd56O/wAfVvzVdErnbpHe7z0d/j6t+aoOiUREBERBAq1r8RvF1lqKapmt1wqO6o6ilp3z9W4sYxzHtY0uHNvEHbbbEgkbc/Dw7tPmuPyXVezViIu6MopmIz6bz52/Usrx4q78O7T5rj8l1Xs08O7T5rj8l1Xs1YiK6Rhbk8/Y1K78O7T5rj8l1Xs08O7T5rj8l1Xs1YiJpGFuTz9jUrvw7tPmuPyXVezTw7tPmuPyXVezViLxqauCjax1RNHA172xMMjw0Oe47NaN+0k8gPKmkYW5PP2NSA+Hdp81x+S6r2a1dx1lw+z3ShtlfdnUVyrjtSUdRSTsmqD2eIws3d+gLMqcmyTUeoz7FLTbL3gklvjbSW/LqumifHPO4EufBE4+Oxo4Nndh4nDdpaN5LiuCU1htePsuVQ/Jb5ZqM0cWQXSJj614cGiQ8YG44+Fu+3bwjck800jC3J5+xqaLw7tPmuPyXVezTw7tPmuPyXVezViImkYW5PP2NSu/Du0+a4/JdV7NYVLqvjFdc663U1dPUXCg6vuukioah0tPxjiZ1jBHu3iA3G4G47FaKq7Brpi82u2p1Ba8eqrfk8MNrkvF4k36q4gwO6gM8Yj7Wzdp2A7fKmkYW5PP2NTN8O7T5rj8l1Xs08O7T5rj8l1Xs1YiJpGFuTz9jUrvw7tPmuPyXVezTw7tPmuPyXVezViImkYW5PP2NSu/Du0+a4/JdV7NfMmf2eKNz3mvYxoLnOdbKoAAdpP2tWMiaRhbk8/Y1KmsWtGHZPbxX2e7OutEZOp7po6OeWPrOXicTWEcXMcu3mFtfDu0+a4/JdV7NbXUbSXFdVsSqsayS1tq7RUyiokhhlfAetB3bJxRlp4gef8AnusapwbIKfNMbr7Nl81rxa20ncdZjTqKOZlY0NcGPEzvHjcCWbkb7hu3l3TSMLcnn7Gph+Hdp81x+S6r2aeHdp81x+S6r2a8qDUDLbDRZzc82xJtus1lkfNa6iy1Jr57lSgvI+0NbxMkDWs3G+xLztsG7nf4vqfjeWY7Yb1SXKOlpL60m3x3D/dZpyN92tjk2cSNjyA7OfYd00jC3J5+xqaXw7tPmuPyXVezX6M6tbjs1lye7yNZaaok/mAj5qw0TSMLcnn7GpF8NtlSyqul3qoHUj7g6MRU8m3WMijaQ0v27HEuceHnsCN+e4EoRFyYlc4lWdKTrERFrRQnTt/BO1B+D0/zqFXZYfvHbvg0f7oVJ9O38E7UH4PT/OoVdlh+8du+DR/uhBnoiICIiAiIgIiICIiAiIgLnbpHe7z0d/j6t+arolc7dI73eejv8fVvzVB0SiIgIiICIiAiIgItZV5JbqS6C092U8l6fTPq4bW2dgqZo2EAuaxzhy3LRxHYAkbkKsG2LKNfcAtMuRsyDSWpjuvdb7farjGaqopmEmJksjQeDi3a5zO0cJB7eQSas1Xtd2ynJsHxiup63PLTbTWOoqmGXuaF7mjqWzSNGw4i5hLQeLhJOy1Fv0gfntjwmv1ZprZfsyx2pfXxS2zrYqOGoLt2lrC4cfCAwAuHa3fYKzoqWGGaaWOGOOWYgyva0BzyBsC4+XYADmvVAREQEX5um6D9UMsNVm0mp+UQXWjoo8Ijp6U2WpiI6+SUtPdAkHETsHbbeKP0qZbquMetFHS66ZbcGZnJX11VbaRr8WdJu23tbuBOG8R26zz8I7O0oLIRfm6boP1F+br9QEREBERAUdy/TrGM/dbXZHYqG8vtlQ2ropKuEPfTytcHBzHdrebW7gcjsAdwpEiCE0WndZbNSbzl8OV3yqhuNGIBjlZUh1sglaGBssTA3iYdmHfmdy9x820aiz/P9OdMJrzqBjLMnv8ABXCE0Wn1NLUmSmOwEzY5SHbjxiRuPIrbRBFnanYzT5JaMcrbxTW3I7rSCspLPWyCKpljO/Ywnm4cLt2jc+KfIFKVqq3FbLcr5QXqrtFDU3i38Qo7hNTMdUU4c1zXCOQjiaCHOB2PMOPnUKpNHZMPo87qMMyG422+5M91THPdp33CloKkl5MkULzs0F0hJbvsdm8tmgILKRVfcsr1DwSx4XTVeLNz66VlQ2kvlxskrKSKi4nANnEUhLnN58wNtuFxJA2CkNt1ZxW76k3XAaW6dZltrpm1lVbuolBZC4MIeHlvA4fbGDk4kE7FBWfTt/BO1B+D0/zqFXZYfvHbvg0f7oVIdOaeKq6I+fTQyMmhkpaZzJI3BzXA1UOxBHaFd9h+8du+DR/uhBnoiICIiAiIgIiICIiAiIgLnbpHe7z0d/j6t+arolc69MA+Dtdo/mo5MsOZ0cVU89jKWpDopXfuf4oOikREBERARFj3CuitlBU1k/WGGnidM/qo3SP4Wgk8LWgucdhyABJ8gQZCp6fP7xrvp9d5NJ7vJjFdBc+94vV+s0nA6NhAmkp45NuMjdzQXDbiY5pDeTh64bRUGudwwbVgPyexwUNNUmgsNc8U8UnW7xipliaSXEx7lu7tuF7TsPLbYGyCL0emuOU+Z+GUtnoZswfRsopbyINpXRt35N3J4N9z2cyNgSQBtKURAREQEREFAa9Z5dtNtM6++2OOikukdVR00IuMb3wDrqqKElwY5rjsJCeThzCimS5hqlp9gOYZPfK7C7pFarTPVUsFqoaqMmobsW9YXzu3ZsH7gbHcjmOe+16UeOVWWaOXC1Ultnu8k9wtvHR08LpnPjFdA6TxWgktDA4nzAEnktfqtpLZcW0C1Ds+D4tBRVFxtkwFDZ6Tx6mXgIaAxg3c7yAAIMtmvDLfrTbMGu1MynprrZKSupLgxrgwVcr5wad5JIHG2LdnZza4cyRtqKvXS/QUdXK2ktxdDqJBiLQYpNjSPliYXnx/5XaQ8/uezxV4VGmJzbVG+UV5t1bDZqzCrZSMr2xOj6qqjqah4Mcm2wmjJY8bcweEkKD2zTzN7VhkVFfqCoud7bqrQ3KoqqKlcWVFOJadzqsBoPDGQC5x7GkOB22KC0cH1EzfUXMr4+2Mxqjxay3yezVdHVCd9zcITwul4muDGcR8ZrS0+KQd1uNP9T63KLXndRXtoYJrFfbhbKVkQc0Pig24HPBcd3HfmRsPMAq4z6B2R6rWqqw/BcmsebUd6gircjdRGkoKu3skAnM0wdwVEboweEEF+/DttsvnCtAMaySHU+55VgtNWXioye6y0dRcqA9bLASDE6MuG7mEklpHI89kF06G5jW6had4bktxighrrrSU9XNHStc2Jr3bEhocSQPzkq51QnRltdbZNGtPKC40c9BXU1tpY5qWqidHLE8Abtc1wBaR5ir7QEREBERAREQEREBERAXhUUNPVcfWxNc58boS/bZ3A7taHDmAdh2eYL3RByZ0s9Hse0r6F+b2PC4ZMds8VRDcJKOKV8zZS6oiDmEyOcQ0nhOwPLhG3JXZQXLPLbkGFW6jstvvGITW1jbrepazqaumnbG48Qh4dntcWsGzee7z2AbrN1z04bq5pDlmIF/Vy3ShfFA8nYNnGz4ifyCRrCfybrR9F7UqTVTRDG7vVtMV5poTbbpA4bPirID1cocPISWh+3kDwg3GM6147kUmYCZtfj8WKzuhuVRfqR1FCGAvAmZI/ZroyI3ODt+wgkDcKa225Ud4oYa2gqoK6jnbxxVFNIJI5G+drgSCPyheV7sduyW01Vru1DT3O21cZinpKuJssUrD2tc1wII/OoLpvi+Q4Xl1/skVDbbZpnRUtJHjdJRgCSN3C41If5duM8t0FkIiICIiAiIgIiICIiAqW6ZmLOy7ox5/SRA9fS2/vlE5v3TXUz2z7j8u0RH6VdKwb7aIMhslwtdUOKlrqeSmlHnY9pa7/wBiUGq04yluc6e4zkbCC27WymruX/5ImvI/7lI1QPQYvFRXdHCwWutO9yx+pq7LVN/qvgneGt/QwsV/ICItDYc8x7KL1kFotV3pa652CoZTXOlifu+lkfGJGhw8xafuhuN2vbvxMcAG4raynt1HPV1c8dNSwRullmmcGsjY0buc4nkAACSSqwxy5Sa033ENQMSzerjwOmiq2vtMFEYhc5+IxB8j5ACY27P2AaOYBB82cL1f88y/GLnh9+x6u01bHVC7TRHuqasmB6tkMZHita13ES7cnduxG3bP6C30tqoaeioqaGjo6eNsUNPTxhkcTANmta0cgAOQAQZCIiAiIgIiICIiDV+DtH5n/rJ4OUfmf+stoiDV+DlH5n/rKK0sNvl1LrrQMnpZpYrcyoOONjaKmAF+3dDn8W5Y77kDhA38qlt/yK1YpaZ7re7nR2a1wcPXVtwqGQQx8Tg1vE95DRu5zQNzzJA8qou3dIDRMa93eSO+WKnvpsUIlyp95pxRVEPW8qVr+t4TI0+MQBvt5UF4+DlH5n/rJ4OUfmf+svvH8jtOWWiC62O6Ud5tdRxdTW2+oZPBJwuLXcL2EtOzmuB2PIgjyLYoNbFYaWGVkjQ/iYQ4bu8oWyREBERAREQEREBERAREQEREBc26df8A2e6Wma4Y/wC02HPKcZVaWnkxtazxK2MedzthKfMGhdJLnvpmWastWG4/qdZoHS3zT26R3gNj+6monEMq4d/I10ZDnfkYUHQirLDrVZKbXTUKupMrmuV7qaW3Nrsfe4mO2tbG4RvaPJ1g3cfzKT1upuJ2rFbXklzyO12ixXRkT6OvuVZHTQz9ZGZGBrnkAksBcB27AnyKoMU6ROjbtaM7ZBd8ctNxbTUHdGTy3inbBdh1Z4GRvL9nGIeKduzdB0KixrbcqS8W6lr6CqhrqGqibPT1VNIJIpo3AOa9jgSHNIIII5EFZKAiIgIiICIiAi+XvbGxznODWtG5cTsAFCBk+Q32NlZZ2W2itsrQ+ndXMkmlmYRuHlrXMDN+0N3cdtt9iS0bsPCqxLzGqIWITlFBu7sz9+WH1Gb2yd3Zn78sPqM3tlu0ad6PXotuKrOjN/8ASus+v2FHxGU+RRZBCzyFtfCJHcP5AWAfkXRio+3aaZBa9YLtqJT3e2sul0tcVrqaPuOTud7Y38TJdus4uMDxfuttvIpv3dmfvyw+oze2TRp3o9ehbil12ulNY7VW3GskENHRwvqJpD2NYxpc4/oAK/hnate8nx7WO9ahWuZjLjd6uplrqKoBkpq2Cd5dLSzM3HHE4HbbkRs0tLXNaR/X7ULGsr1Fwa+4vWXi2UFHeKOWhnqKKjkbM2ORpa7hLpHDmCRzB5EqnNKehDiulDo54bZYsluTHcTa7IaOSqe0g7jZgkbGNj2Hg37OaaNO9Hr0LcVxdFfNcZzzQzGrliONVGI2VkRp2WeeB7GwPafH6uRwAnYXEkTDfiJPFs8Pa22lDaHJrpaKmlhvcdC6iqJGU0dVQNdGIpHENja5jifFc4hoIPIlo22JImS0YmFVhzrSwiItSCIiAiIgIvxzgxpc4hrQNySdgAqvyHXGmgmdBYKDvuGnY1ssvVUx/KwgOdJ+cANPLZxXVgZNjZTVm4VN/wDu1bLRRUQ/WbL3HdtPZGD+qYZnbfp6wf5L8+zLmP8AZWP1ab2q9P8Aw2VcOZq2rC1r0ypNZNKslwytf1Ud1pDHHLz2imaQ+F527Q2RjHEeXbZfxJxvSTIcj1apdOo6R0ORS3M2uWFw36iRry2Rztv6LA1zifM0lf10+zLmP9lY/VpvaqsLbjItWudw1Yp6G1DKa2k7lkaYZO52nYNdM1nHuJHMaGk8WxG/Lckl/hcq4czVtdS6d4LbNMsGsmK2aMx2200rKWLfbifsPGe7b+k527ifKXFSJUP9mXMf7Kx+rTe1T7MuY/2Vj9Wm9qn+FyrhzNW1fCKiotZ8ta7eSmsso3+5bFMzl+fjP+SleM62UVxqI6W90Zsk7yGsnEvW0ziTsAX7AsJ/vNA8m5K04v8AFZVhU5003jhN/TvLbFlIiLyEEREBERAREQERQ+tyi63OtqorFHRspqWR0D6ytDniSVpIe1jGkcmuHCSSNyHADluduHh1Yk6lsmCKDd3Zn78sPqM3tk7uzP35YfUZvbLfo070evRbcU5WHeLRSX+0V1ruEDamgrYH01RC/wC5kje0tc0/kIJCiXd2Z+/LD6jN7ZO7sz9+WH1Gb2yaNO9Hr0LcXMGF6XP1d6OeomgN7kbLlOA3GSktVTUbB3B40tvnJ/otewuj7OTN/Ov556S6QXjVTVyzYHBDLSV9XW9zVZczxqSNhJne4H+o1rzsfKNvKv65UmmF+t+sddqPSXe3U12uFqZaq2jjo5O5alrH8TJXt6zi6xo2aCHbcPLZanD9ATg+sGUalWt9oZkWQxiOoa+kk6iHctMjomiQEOkc1rnkk7nfbbc7tGnej16FuK9sbx+hxLHbVY7ZF1FttlJFRUsRO/BFGwMY3fy7NaAtkoN3dmfvyw+oze2Tu7M/flh9Rm9smjTvR69C3FOUUG7uzP35YfUZvbJ3dmfvyw+oze2TRp3o9ehbinKKDd3Zn78sPqM3tltLBklZLcRarxDTw1743TQTUjiYp2NLQ7k7mxzS5u7dyCCCCfGDcasnqpi8TE+SWSVERcqNXlJIxi7kcj3HN+4VHsa/m5avgkX7gUhyr+bF4+BzfuFR7Gf5uWr4JF+4F6OD8mfP9MvBFLFrrg+TZicXtV87uvAklhDYaScwOfECZGtn4OqcW8J3AeTyKnq57xuS+9Gi641h0zqTI8CvVxnorNPGDFcaGaQS1DYZW82zRkteBIOEjccQPJQXSdmrupthxrUC33ENqbjWMq6iSfK5nURpxMRNTd7e5OrZswOYNpOMOAcXk7rHO8JYuvkXIeS5nkIzWgzbF6zI2427NKexz1F0yAupKpjqsU08UNuEZaIw4vDZC5rwW77FSfG6m8Rt1qzepv19utRil7uhtFlNxmbRgQ0UcgjdE1wEjS5/Jjt2tLQWgEuJucOjq+thtlDUVlS/q6enjdLK/Ynha0bk7DmeQ8ixccyCgyzH7be7VUd1Wu5U0dZSz8DmdZFI0OY7hcA4bgg7EA+cKlsHwid+k8eZ12bZHkV0uuOSVdU2puTn0Ez5qYvPBTAdXG1pd4vABsBz3UI0hobnp/aejvX0mT3yupsnoIqC4WuvrDLRiM2x08XVRbcMRjdE0AtAJG/EXEkpnDpHOSW2BpBIPd1FzHwqJWGq8zv+b7fh1F86iVhqZR8qjzn9MvAREXAxEREBERBT2suXSVVwGM0shZTMjbNcCw7dZxb8EJ/u7eM4eUFg5guBrpbHK5HzZxkz5P5Tu8tPLnsI2Nb/ANoatcv0rIsGnAyeimnxiJnzlKu+wiLT5leJ8exC+XWliFRU0NBPVRREb8b2Ruc1v6SAF2zMUxMyxbhFQ2m1o1AuEmK5G24GakrRHU3GWpyGSriqoJGbuEdN3O1kTgSC0McNttjxdq1eLVt4o8KwHK35Heau41+QxW6piqq18lPJTyVMkPAYj4u4ABDtuLcdq4Yyq8RObO3+tXVV+ZFkVvxOy1V2utR3Lb6YB0s3A5/CCQByaCTzI7AtiuYc6grc60ozrK7jfroyenuU1HDaaeqMdJTxQ1LY2xviHJ7iBxFzufjDbbZdPLbhY04tU6rRaJj+79AX45rXtLXAOaRsQRuCF+oupFp6MZfNUuqMcrJXSyU0XdFHJI7ic6EENcwny8BczY+Z7R5Faa5107lfFqXjvASDIaiN+w7WdQ937zWf+y6KXwX8vg04OU3p/wCUX/Mfps4iIi8RBERAREQFXuEOLrLUEkk98rhzPwyZWEq8wb7yVHxlcPnky78D5dXnH7XweNLqTjdZbsmro7m0UuNTzU92kkikZ3K+KMSSbhzQXAMcHcTdwQeRK9q/PrBbanHKee4NE2RSGK1MjjfJ3S4RGUkcIOwDGlxc7YDz8wucNbLXVUesl0wWljmFDq5Fb2ySxbjq+5X8NwII8rqMMH51X0OX5JjVoyGoZG+a56K41VWSnqKiPdvdk9SYYagAjxg2igjf2f8AEIP5ZNdkd019bDbKGorKl/V09PG6WV+xPC1o3J2HM8h5Fi45kFBlmP2292qo7qtdypo6yln4HM6yKRocx3C4Bw3BB2IB84XOljwrUyx9fcaq4OkxiezVvfNtdls16NVxU7jDLAx9JEIiH7b8Dg0tcfF5BaLTyiummGD9H6/2nKb5WjI+9dor7Dca01FG+CejLy6GI8ojDwAgs28UHi38tzhfli11wfJsxOL2q+d3XgSSwhsNJOYHPiBMjWz8HVOLeE7gPJ5FT1c52ytv/RblsmLOFJk+C3WsqqaySRgxXGincyapbBK3m2aMlr2iRvC4bjcHkt10ecbrMuxHE9R7tmuQ3a83el7vqaRlxc22AytP2htKPEa2MnYbeNxM5k8wrFU9wvJFylptXPsfRhZqBlGWZld7pcKSSiBpbtIZQZazqYGQMeerbLxdW3rXAuHE7xtuQ0s2QZzguP6149cbjd6B9FhrL3bhVZDJdKuhlcKhhcyrLGPaSY2nh3cGlu7XbFTPHY6LlvMr3e9DLviNxsuQXzIn3qxXWprrRe7hJWtlkpbe6pjmjDzvEesa1jgzZpEoGwOy+tHbBqtdZsGy9t2NRQXEQ1l2nrMslroK6mli4nCKiNIyOBwLmuaI3gN4S08W5KudrsOolpqkkZ3i/PtFV/phVH0VrPX3jBaLL71kt+vdzqai407Ya65SyU0UTa2VjWiIu4XOAj5PcC4BxaCGgAW5Vfz8xf8ANVf6QW3Dm954VfiVhPERF5SNXlX82Lx8Dm/cKj2M/wA3LV8Ei/cCl9XSx11JNTSjeKZjo3geYjYqA00t1xakgtlTZa+4ilY2GOtoGseyZjRs1xBeHNdsOYI2332JHNehk9qsOaInXdlGuLI7ivR+0/wvJhkNox2OC7tdI+KomqZpxA6TfrDE2R7mxF253LANwSPKvu36B4Facu8JaKwNpbr3S6tBiqp204qHAh0opw/qg87ndwZvz7VI/Caq9Gr56s36aeE1V6NXz1Zv01u7Gdkc4LSiVx6OGnV2uVbX1WOB9RV1Xdz+GsqGMZU8YeZ4mNkDYpS4bmSMNcdzuTud5jYMPs+MOvBttGKc3eukuNdvI94mqHta179nE7btY0cI2HLs5lefhNVejV89Wb9NPCaq9Gr56s36avZTw5wWlFrB0edPsXuM1Zasf7ikkjmiEUdZUdRE2UFsgihMnVxcQJHiNb2rfU+mONUlFiVJFbeGnxTh7zM6+U9y8MLoG8+Ld/2tzm+Pxdu/bzWX4TVXo1fPVm/TTwmqvRq+erN+mnYzw5wWl8Z3/N9vw6i+dRKw1ARR1+YS0tPJa6q126KoiqZ5q0Na6Tq3tkbGxrXE83NG5Ow2B2335T5c2UTEU00eMXnnboT3WERFwsRERAREQUXq9jz7NlhurWnuK7Boc/fkyoY0N4T/AMzGtI/5HfpgF5juMtulbaailpa87dXLWwOmiHMb7sa9hPLfbxhz27exdT3i0Ud/tlRb7hTtqaOdvDJG7cb+UEEcwQQCCNiCAQQQqZv+j19tErnWlzL3R7+JG97Yqlg8xJIY/wDPu383lP2X8d/JYVWFGBjTaY1RO2PPwsTF1Md79Q/x9jPyJUfW1k22gzRtdEbnd7BU0G/26GmtM8Uj27dgc6pcB+lpU5fjGSxuLXYxdAR5mRuH+IeQvnwbyP0Yuv7Jn0l7UVYHf2kf+XumbKB49pJieKXhtztNpFHVMLzGG1Epih49+Lq4i4sZvufuWjtWZBp3j1NY7ZZ47fw2621bK+kh66Q9XO2QytfxcW52eSdiSPJttyUw8G8j9GLr+yZ9JPBvI/Ri6/smfSVirJqYtE084M2Vc3zRDCcjuFdW19kEk9c4PqhFVTRRzPG2z3MY8NLuQ8bbf8qzKyhzt9ZO6kveOxUpkcYmTWed72s38UOcKoAnbbcgDfzBTrwbyP0Yuv7Jn0k8G8j9GLr+yZ9JS+TReYqiL7JiPxJmygBt+oXkv2Mj/wDhKj62pTbGVkdBA24TQT1ob9tkponRRud52tc5xA/IXFbqLFclmcGtxm57k/02xtH+JeApTjWjd2uszJb89tqoeRdS00ofUSf3S8eKwectLj27Fp2K115Tk2TxNVWJ639LmbPi9dFcdkrr5U5BI0ijpYnUdK49kkrnDrXD/l4QzfzueP6KudeFFRU9tpIaWkhjpqaFoZHFE0Naxo7AAOxe6+EyzKZyvGnFnVs8mQiIuJBERAREQFXmDfeSo+Mrh88mVhqBOoq/EZquGO2VV0t01TLVQy0XC58Zle6R7Htc4Hk5x2LdxsQDttue7J5iaaqL65t6X6rHdZkV2OW25Xm2XappI5rjbRKKOodvxQ9Y0Nk2/wCYABeEeG2SOW/Sd7Kd7r64OuYkZxtq9omwgPB3BHVsa3bs2/OV5+E1V6NXz1Zv008Jqr0avnqzfprp7KrhzhbSjeJaB4Lg0lW+y2R1KaqkfQPD66omayneQXRRiSRwiYSByZwjkPMvnD+j9gGBXulu9kx5lLX0kToaR8tVPO2lY4bOELJHubFuORLACdzv2lSbwmqvRq+erN+mnhNVejV89Wb9NTsZ2RzgtKM4r0ftP8LyYZDaMdjgu7XSPiqJqmacQOk36wxNke5sRdudywDcEjyr7segmB4zlLchtdhFDcmTyVMYhqpxTxyvDmveyn4+qY4hzgS1g7SpH4TVXo1fPVm/TTwmqvRq+erN+mnYzsjnBaWvi0oxSHT3wGFnjfivVGHvdLJJIOEvL/u3OL9w48QPFuCAQRsFB8v6NWOPwbMKLErdHbskvdjqbOLjXV1TL1okHi9e97nufs4N2c4Oc0bhuwJBsjwmqvRq+erN+mnhNVejV89Wb9NOxnhzgtKM6d6E4fpzUsuVtszGXp9G2jmrJqmapcI9hxRx9a53VsJG/CwNHIcl6YloLgmC5Ay82Kwi3VsZkMLWVU7oIDJvx9VA55ji33O/A0dqkXhNVejV89Wb9NPCaq9Gr56s36adjPDnBaXriWI2nBrDBZbHSdxW2B8skcHWPk4XSSOkeeJ5J5ve49vLfYctgvyq/n5i/wCaq/0gvPwmqvRq+erN+ms+w2yuvF9prxW0Utsp6OOSOmpqgtM0j38PE9wa4hoAGwG5J4jvtsN8rdlEzVsmO+PGJgiLd6YoiLx2IiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg/9k=", "text/plain": [ "" ] @@ -208,7 +469,11 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph(xray=True).draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(chain.get_graph(xray=True).draw_mermaid_png()))" + ] }, { "cell_type": "markdown", @@ -220,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "912b0604-a178-4246-a36f-2dedae606680", "metadata": { "ExecuteTime": { @@ -228,8 +493,28 @@ "start_time": "2024-05-15T08:19:51.937879Z" } }, - "outputs": [], - "source": ["for s in research_chain.stream(\n \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'Search'}}\n", + "---\n", + "{'Search': {'messages': [HumanMessage(content='Taylor Swift\\'s next tour is called \"The Eras Tour,\" which is scheduled to hit U.S. stadiums beginning in March 2023 and running into August, with international dates set to be revealed later. The tour has already started with some shows, including the kickoff on March 18, 2023, in Glendale, AZ. The U.S. leg is set to wrap up in Los Angeles at SoFi Stadium on August 9, 2023.\\n\\nFor specific dates and locations, you may want to check Taylor Swift\\'s official website or trusted ticketing platforms, as the tour dates and details are subject to change.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in research_chain.stream(\n", + " \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] }, { "cell_type": "markdown", @@ -245,7 +530,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "1bcdbf44-9481-430c-8429-fa142ed8a626", "metadata": { "ExecuteTime": { @@ -254,7 +539,76 @@ } }, "outputs": [], - "source": ["import operator\nfrom pathlib import Path\n\n\n# Document writing team graph state\nclass DocWritingState(TypedDict):\n # This tracks the team's conversation internally\n messages: Annotated[List[BaseMessage], operator.add]\n # This provides each worker with context on the others' skill sets\n team_members: str\n # This is how the supervisor tells langgraph who to work next\n next: str\n # This tracks the shared directory state\n current_files: str\n\n\n# This will be run before each worker agent begins work\n# It makes it so they are more aware of the current state\n# of the working directory.\ndef prelude(state):\n written_files = []\n if not WORKING_DIRECTORY.exists():\n WORKING_DIRECTORY.mkdir()\n try:\n written_files = [\n f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n ]\n except Exception:\n pass\n if not written_files:\n return {**state, \"current_files\": \"No files written.\"}\n return {\n **state,\n \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n + \"\\n\".join([f\" - {f}\" for f in written_files]),\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\ndoc_writer_agent = create_agent(\n llm,\n [write_document, edit_document, read_document],\n \"You are an expert writing a research document.\\n\"\n # The {current_files} value is populated automatically by the graph state\n \"Below are files currently in your directory:\\n{current_files}\",\n)\n# Injects current directory working state before each call\ncontext_aware_doc_writer_agent = prelude | doc_writer_agent\ndoc_writing_node = functools.partial(\n agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n)\n\nnote_taking_agent = create_agent(\n llm,\n [create_outline, read_document],\n \"You are an expert senior researcher tasked with writing a paper outline and\"\n \" taking notes to craft a perfect paper.{current_files}\",\n)\ncontext_aware_note_taking_agent = prelude | note_taking_agent\nnote_taking_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n)\n\nchart_generating_agent = create_agent(\n llm,\n [read_document, python_repl],\n \"You are a data viz expert tasked with generating charts for a research project.\"\n \"{current_files}\",\n)\ncontext_aware_chart_generating_agent = prelude | chart_generating_agent\nchart_generating_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n)\n\ndoc_writing_supervisor = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n)"] + "source": [ + "import operator\n", + "from pathlib import Path\n", + "\n", + "\n", + "# Document writing team graph state\n", + "class DocWritingState(TypedDict):\n", + " # This tracks the team's conversation internally\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # This provides each worker with context on the others' skill sets\n", + " team_members: str\n", + " # This is how the supervisor tells langgraph who to work next\n", + " next: str\n", + " # This tracks the shared directory state\n", + " current_files: str\n", + "\n", + "\n", + "# This will be run before each worker agent begins work\n", + "# It makes it so they are more aware of the current state\n", + "# of the working directory.\n", + "def prelude(state):\n", + " written_files = []\n", + " if not WORKING_DIRECTORY.exists():\n", + " WORKING_DIRECTORY.mkdir()\n", + " try:\n", + " written_files = [\n", + " f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n", + " ]\n", + " except Exception:\n", + " pass\n", + " if not written_files:\n", + " return {**state, \"current_files\": \"No files written.\"}\n", + " return {\n", + " **state,\n", + " \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n", + " + \"\\n\".join([f\" - {f}\" for f in written_files]),\n", + " }\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "doc_writer_agent = create_react_agent(llm, tools=[write_document, edit_document, read_document])\n", + "# Injects current directory working state before each call\n", + "context_aware_doc_writer_agent = prelude | doc_writer_agent\n", + "doc_writing_node = functools.partial(\n", + " agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n", + ")\n", + "\n", + "note_taking_agent = create_react_agent(llm,tools=[create_outline, read_document])\n", + "context_aware_note_taking_agent = prelude | note_taking_agent\n", + "note_taking_node = functools.partial(\n", + " agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n", + ")\n", + "\n", + "chart_generating_agent = create_react_agent(llm, tools=[read_document, python_repl])\n", + "context_aware_chart_generating_agent = prelude | chart_generating_agent\n", + "chart_generating_node = functools.partial(\n", + " agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n", + ")\n", + "\n", + "doc_writing_supervisor = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n", + ")" + ] }, { "cell_type": "markdown", @@ -266,7 +620,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "9c5c644f-8966-4d2e-98d2-80d73520e9fe", "metadata": { "ExecuteTime": { @@ -275,11 +629,57 @@ } }, "outputs": [], - "source": ["# Create the graph here:\n# Note that we have unrolled the loop for the sake of this doc\nauthoring_graph = StateGraph(DocWritingState)\nauthoring_graph.add_node(\"DocWriter\", doc_writing_node)\nauthoring_graph.add_node(\"NoteTaker\", note_taking_node)\nauthoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\nauthoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n\n# Add the edges that always occur\nauthoring_graph.add_edge(\"DocWriter\", \"supervisor\")\nauthoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\nauthoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n\n# Add the edges where routing applies\nauthoring_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"DocWriter\": \"DocWriter\",\n \"NoteTaker\": \"NoteTaker\",\n \"ChartGenerator\": \"ChartGenerator\",\n \"FINISH\": END,\n },\n)\n\nauthoring_graph.add_edge(START, \"supervisor\")\nchain = authoring_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str, members: List[str]):\n results = {\n \"messages\": [HumanMessage(content=message)],\n \"team_members\": \", \".join(members),\n }\n return results\n\n\n# We reuse the enter/exit functions to wrap the graph\nauthoring_chain = (\n functools.partial(enter_chain, members=authoring_graph.nodes)\n | authoring_graph.compile()\n)"] + "source": [ + "# Create the graph here:\n", + "# Note that we have unrolled the loop for the sake of this doc\n", + "authoring_graph = StateGraph(DocWritingState)\n", + "authoring_graph.add_node(\"DocWriter\", doc_writing_node)\n", + "authoring_graph.add_node(\"NoteTaker\", note_taking_node)\n", + "authoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\n", + "authoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n", + "\n", + "# Add the edges that always occur\n", + "authoring_graph.add_edge(\"DocWriter\", \"supervisor\")\n", + "authoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\n", + "authoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n", + "\n", + "# Add the edges where routing applies\n", + "authoring_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\n", + " \"DocWriter\": \"DocWriter\",\n", + " \"NoteTaker\": \"NoteTaker\",\n", + " \"ChartGenerator\": \"ChartGenerator\",\n", + " \"FINISH\": END,\n", + " },\n", + ")\n", + "\n", + "authoring_graph.add_edge(START, \"supervisor\")\n", + "chain = authoring_graph.compile()\n", + "\n", + "\n", + "# The following functions interoperate between the top level graph state\n", + "# and the state of the research sub-graph\n", + "# this makes it so that the states of each graph don't get intermixed\n", + "def enter_chain(message: str, members: List[str]):\n", + " results = {\n", + " \"messages\": [HumanMessage(content=message)],\n", + " \"team_members\": \", \".join(members),\n", + " }\n", + " return results\n", + "\n", + "\n", + "# We reuse the enter/exit functions to wrap the graph\n", + "authoring_chain = (\n", + " functools.partial(enter_chain, members=authoring_graph.nodes)\n", + " | authoring_graph.compile()\n", + ")" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 11, "id": "58e7d1e48a9c39a5", "metadata": { "ExecuteTime": { @@ -290,7 +690,7 @@ "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -299,11 +699,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph().draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(chain.get_graph().draw_mermaid_png()))" + ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "9860fd46-c24d-40a5-a6ba-e8fddcd43369", "metadata": { "ExecuteTime": { @@ -311,8 +715,29 @@ "start_time": "2024-05-15T08:19:53.709307Z" } }, - "outputs": [], - "source": ["for s in authoring_chain.stream(\n \"Write an outline for poem and then write the poem to disk.\",\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'NoteTaker'}}\n", + "---\n", + "{'NoteTaker': {'messages': [HumanMessage(content='The poem has been written and saved to \"poem.txt\".', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in authoring_chain.stream(\n", + " \"Write an outline for poem and then write the poem to disk.\",\n", + " {\"recursion_limit\": 100},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] }, { "cell_type": "markdown", @@ -328,7 +753,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "95ae7e52-92ed-41a3-88c4-21b6d7c8b041", "metadata": { "ExecuteTime": { @@ -337,11 +762,26 @@ } }, "outputs": [], - "source": ["from langchain_core.messages import BaseMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_node = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following teams: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"ResearchTeam\", \"PaperWritingTeam\"],\n)"] + "source": [ + "from langchain_core.messages import BaseMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "supervisor_node = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following teams: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"ResearchTeam\", \"PaperWritingTeam\"],\n", + ")" + ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "id": "4880e573-612f-4d24-97c1-2079382a4a2f", "metadata": { "ExecuteTime": { @@ -350,11 +790,50 @@ } }, "outputs": [], - "source": ["# Top-level graph state\nclass State(TypedDict):\n messages: Annotated[List[BaseMessage], operator.add]\n next: str\n\n\ndef get_last_message(state: State) -> str:\n return state[\"messages\"][-1].content\n\n\ndef join_graph(response: dict):\n return {\"messages\": [response[\"messages\"][-1]]}\n\n\n# Define the graph.\nsuper_graph = StateGraph(State)\n# First add the nodes, which will do the work\nsuper_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\nsuper_graph.add_node(\n \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n)\nsuper_graph.add_node(\"supervisor\", supervisor_node)\n\n# Define the graph connections, which controls how the logic\n# propagates through the program\nsuper_graph.add_edge(\"ResearchTeam\", \"supervisor\")\nsuper_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\nsuper_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"PaperWritingTeam\": \"PaperWritingTeam\",\n \"ResearchTeam\": \"ResearchTeam\",\n \"FINISH\": END,\n },\n)\nsuper_graph.add_edge(START, \"supervisor\")\nsuper_graph = super_graph.compile()"] + "source": [ + "# Top-level graph state\n", + "class State(TypedDict):\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " next: str\n", + "\n", + "\n", + "def get_last_message(state: State) -> str:\n", + " return state[\"messages\"][-1].content\n", + "\n", + "\n", + "def join_graph(response: dict):\n", + " return {\"messages\": [response[\"messages\"][-1]]}\n", + "\n", + "\n", + "# Define the graph.\n", + "super_graph = StateGraph(State)\n", + "# First add the nodes, which will do the work\n", + "super_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\n", + "super_graph.add_node(\n", + " \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n", + ")\n", + "super_graph.add_node(\"supervisor\", supervisor_node)\n", + "\n", + "# Define the graph connections, which controls how the logic\n", + "# propagates through the program\n", + "super_graph.add_edge(\"ResearchTeam\", \"supervisor\")\n", + "super_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\n", + "super_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\n", + " \"PaperWritingTeam\": \"PaperWritingTeam\",\n", + " \"ResearchTeam\": \"ResearchTeam\",\n", + " \"FINISH\": END,\n", + " },\n", + ")\n", + "super_graph.add_edge(START, \"supervisor\")\n", + "super_graph = super_graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 15, "id": "270ff3ae26cd42ff", "metadata": { "ExecuteTime": { @@ -365,7 +844,7 @@ "outputs": [ { "data": { - "image/jpeg": "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a3Q3CIi1l7Pw7/XdlH8DRf1ysFV9w7/XdlH8DRf1ysFe9t/wrf8AbT/jDy2VfxqhERZtUREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBTWD/rTtn8F/7laCo4DcN6qeSabA8dlmkcXvkfbIS5zidkk8vUkqy4OENrpImxU9zvEELd8sbKzTWj2Dov7/AAVUP2xe/wBN/sXLvezpuXq7tF3DOmZ2T0y7sZbZwiJhV7+APDSR7nvwHHHPcdlzrZCST7fqqaWy2Ullt1NQUFNFRUVNG2KCngYGRxsA0GtaOgAHqC3n4KqH7Yvf6b/Yn4KqH7Yvf6b/AGLXn2VVVtvR3SmMusxspa1FsvwVUP2xe/03+xVF5OtLW8Svwnd9Xu6P7gzi52Gi7Go5NUsHZdmHdPSd6Z2fWseZ/wDtjullzha3SstRfJ+FuHZrcGV2QYvaL1WsjELaivoo5ntYCSGhzgTrbnHX3lTr8FVD9sXv9N/sT8FVD9sXv9N/sUx7JmmcYux3SicvtTqmJVd/e/8ADPWvoBjevZ3XD/8AqpLi+F2DCKOWkx6y0FkpZZO1khoKdsLHv0BzENA2dADf3KWfgqofti9/pv8AYn4KqH7Yvf6b/Ysp9lV1RhN76sYy2zGuKXk4d/ruyj+Bov65WCtBjGGUWKTVs1NPV1M1XyCWSrm7R2mb5QOnT6xW/XbimKKaaInHCIjuiIci9XFy5NcdIiIikREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBc7+Rv/lx/wBqV8/qF0Qud/I3/wAuP+1K+f1CDohERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBc7+Rv/lx/2pXz+oXRC538jf8Ay4/7Ur5/UIOiEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQERfCdDZ8EH1FB6/iYJ5DHYbebswHRrZZexpT97H6cZB97Wlp9TvZrTmmWuOxSWVn70yTO1/Hof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", + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADuAc0DASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBQgCAwQBCf/EAFYQAAEDBAADAgcKBwwIBQUAAAEAAgMEBQYRBxIhEzEIFBUiQVHUFiMyVVZhk5SV00JSVHGRs9EXMzZTYnJ0dYGSobIkJTQ3OHaDsQlEc4K0JidDo8P/xAAbAQEBAAMBAQEAAAAAAAAAAAAAAQIDBAUGB//EADERAQABAgIJAgQGAwAAAAAAAAABAhEDEhQhMVFSYZGh0QRBBRNxsRUjM8Hh8DJTgf/aAAwDAQACEQMRAD8A/VNERAREQEREBERAREQEREBERAREQERR2tuFbfbhPbbVM6jp4DyVd0a1rnMfr96hDgWl473OcC1vQacSeTOiia5VnKqsp6KPnqJ44GfjSvDR+krw+6my/G9B9ZZ+1eKn4f4/C/tZrZBcKo65qq4N8ZmcR6ed+z/YNBe33LWX4ooPqzP2LbbBj3menmV1HuqsvxxQfWWftT3VWX44oPrLP2p7lbL8T0H1Zn7E9ytl+J6D6sz9ifk8+xqPdVZfjig+ss/anuqsvxxQfWWftT3K2X4noPqzP2J7lbL8T0H1Zn7E/J59jUe6qy/HFB9ZZ+1PdVZfjig+ss/anuVsvxPQfVmfsT3K2X4noPqzP2J+Tz7Gp7KS40lwaXUtTDUgd5hkD/8AsvQsBVYFjtW8SOs1HFODzNqKeIQytPrEjNOH9hXnZUVmITQxV1TNcrNK4Rtrp9GalcTpolIA5oz0AfrmB1zcwJcGSiv9Ode6f2/sJa+xJ0RFzoIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIMTll4dj+MXW5MAdJS00krGnuc8NPKP7TpdmO2dlgstJQtPO6Jm5JPTJI4l0jzv0ucXOPzkrw8QKKW4YTeoYGl8/ir3xsA2XOaOYAD5yNLM0VZFcKKnqoHc8M8bZWO9bXDYP6CuidWDFt836Rb7yvs70RFzoiXEDiti3C6K3vyS5mifcJHRUkENNNUzTua3mfyRQse8ho6k60NjZChV08JjH7bxVxvE201dVUN7svleG50tuq5weeSFsLQ2OF3mubI5zpCQGaaHcpcF5vCVoKR8GO3OO35g3JLc+pktF8w63GtmoJXRtDmTRAODo5egLXNLTy9S3oVEo7vmeO53wuz7McTu1bV1GI1NpvEOPUD6x9HXSS00w54o9lrXdk8bGw09CfSgta68fsCsebNxO4X7xS9mojpOzlo5xAJ5ADHEajs+yD3BzdNL9nmHrXOr47YTR5jXYobpUVGQ0M0dPVUFHbaqofA6SNsjC8xxODWFr2+eTy7JG9gga08dLXmmXR8QKK6WfPrvfKe9wzWOhtEUzbK21wzQytk8wiOeUtZIS13PJz8oa0aBV78IbFWUPF7jPdai2VVHT3S626SkqqimfEKmJlugaSwuA5g1/O067ncwOjtB6+AvHu28c7FU1lLQ1turKeoqY5KeeiqWRiNlRJFG5s0kTGPc5rA5zGklhJa4AhWmqP8GOouGL2e6YJeMevVtuVrul0qvHqihe2gqYZa6SWN0NRrkeXNmaeUHY5XbA0rwQF0V9DT3ShqKOribPS1EboZYn/Bexw05p+YgkLvRWJmJvAj+C109bjsTKqUz1dHNNQzSkkmR0Mjo+c79Lg0O/tUgUY4et7SxT1o3yV9dVVcfMNbjfM7kOvnZyn+1SdbseIjFqiN8rO0REWhBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBRSkmZgcjqOq1Fj73l9JV/gUhcSTDKfwWbPmP+Do8h5SGc8rXF7GyMcx7Q5rhotI2CFsory3idcSsSiWV8I8G4gXCK5ZHiVkyCtbCIY6q4UMU7xGCXBoc4E8u3OOvnPrWGPg28KC0N/c3xblBJA8kwaB9P4PzBSF/D63ROJt1RcLKD/wDjt1W+OIfmiJLG/wBjR/gF8OE1G/4U376aH7pbMmHOyvrHi5aN7uw/h3i3D2GpixjHrZj8VS5rp2W2kZAJSNgFwaBvWz3+tSJRf3E1Hyqv300P3Se4mo+VV++mh+6T5eHx9pLRvShFr7kV6yG1+EviOBw5RdTY7rY6y41DnOiMwlieA3ld2egNHqNK2vcTUfKq/fTQ/dJ8vD4+0lo3vdlmF2DO7Y23ZHZaC+29sgmbS3GnbPGHgEB3K4EbAcRv5yok3wbuFLA4N4cYu0PGnAWmDqNg6Pm+sD9Cz/uJqPlVfvpofuk9xNR8qr99ND90ny8Pj7SWje8WNcE+H+GXiG7WHCrDZrnCHNjrKG3RQysDgWuAc1oI2CQfmK9t1uJyszWa0yl9O7cVwuMTvMgZ1Do43DvmPd0+ANucQeVr/owCknP+sLjdrqze+yqq54jP52M5WuHzOBCkVLSQUNNHT00MdPTxtDWRRNDWsHoAA6AJE0Yeumbz2/nt/wBXVD7T08VJTxQQRtihiaGMjYNNa0DQAHqAXYiLn2sRERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBrvmn/ABy8Of8AlS5frGrYha75p/xy8Of+VLl+satiEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBrvmn/ABy8Of8AlS5frGrYha75p/xy8Of+VLl+satiEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARFDp8vutzkkfYqCjmoWPdG2rrqh8fbOadEsY1jvM2CA4kb1sAtIcduHhVYn+K2umKKEeXcw/ILH9bm+7Ty7mH5BY/rc33a36LXvjrBZN1G+I+BWzihgl8xS8M57ddqV9NKQAXMJHmvbv8Jrg1w+doWM8u5h+QWP63N92nl3MPyCx/W5vu00WvfHWCz8Pcw4bX3C+Itwwitonvv9HXG3+LRNLjNIXcrOQd7g/bS31hw9a/bXweuFn7ivBfFMMdN4xPbKU+MSA7aZ5Hulm5f5PaSP182lVOR+DzLkvhC2Pi3U0FmF4tlP2bqMTyGGomaC2Gd5MW+djTofzYz05etx+Xcw/ILH9bm+7TRa98dYLJuihHl3MPyCx/W5vu08u5h+QWP63N92mi1746wWTdFCPLuYfkFj+tzfdrl7rr9ao3VV2tdE+gjBdM+3VEkksbR3uEboxzgDZIB5tDoHHQTRcT2t1gsmqLhFKyeJksT2yRvaHNew7Dge4g+kLmuNBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAVc8ODzcP8AHHHvNvgJ/OYwrGVc8Nv932Nf1dT/AKtq9D0/6Vf1j7VL7JGi1vsnEbMbflWZUOWZPU2a+wwXSotOOzWiFlHUU8Qc6CekquUmYtZyue1ziQSdsACzdFxLySXE/B7rX3LdTlT6QXh/YRf6UH2yWd/Tl0zcjWu8zl7td3RM0IvVFqnYeJPES1cHce4o1+ZG9U8l2ZSV9gmtlNHFLTyXA0g7N8bGyCVoc1wOy066tXys4scWs3uWV3XDLfe5Ka03aqtltt1Nb7ZJb6o08hjPjMs1QyoBe5rtmMN5A4aDyNmZ4G1qKi8duud59xkzq1HK58ZsVhNqfFb6SipZpuealbLLE6WSN3mb31Hnbd0cANHD4VxTzHNKrDcIN28UzC311fHl1dDTQkiGi97BDHMLG+MPmpngtA80v5daVzDYG2XigvUEk1uraavhjlfA+SllbI1sjHFr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", "text/plain": [ "" ] @@ -374,11 +853,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(super_graph.get_graph().draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(super_graph.get_graph().draw_mermaid_png()))" + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "6b8badbf-d728-44bd-a2a7-5b4e587c92fe", "metadata": { "ExecuteTime": { @@ -386,8 +869,215 @@ "start_time": "2024-05-15T08:19:55.796497Z" } }, - "outputs": [], - "source": ["for s in super_graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n )\n ],\n },\n {\"recursion_limit\": 150},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content=\"Unfortunately, the information obtained from the U.S. Fish & Wildlife Service web pages does not provide additional detailed information on the conservation status, size, or lifespan of the North American sturgeon species beyond what was already included in the initial research report. These pages primarily contain placeholders for the species' profiles without specific information on the topics of interest.\\n\\nBased on the information available, the research report provided earlier remains the most comprehensive summary of the North American sturgeon, including an overview of the species, their conservation status, size, lifespan, conservation efforts, and a chart summarizing key data for each species. Further details would require access to additional sources or in-depth research reports that are not currently available in the provided documents.\", name='WebScraper')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content=\"It appears that the information you were seeking from the U.S. Fish & Wildlife Service web pages was not as detailed as you needed for the North American sturgeon species. If you're looking for more comprehensive data on their conservation status, size, lifespan, and conservation efforts, you might need to consider exploring scientific journals, research papers, or contacting experts in the field.\\n\\nIf you have any specific questions or require assistance with creating an outline or reading a document related to the North American sturgeon, please let me know how I can assist you further.\", name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='I\\'ve found several resources that could provide the comprehensive data you\\'re looking for on the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. A paper titled \"Reconnecting Fragmented Sturgeon Populations in North American Rivers\" by Jager et al., which may contain information on distribution, range contraction, and conservation efforts ([Read the paper](https://web.ornl.gov/~zij/mypubs/sturgeon/Jager at al_2016_Reconnecting Fragmented Sturgeon Populations in North American Rivers_Fisheries.pdf)).\\n\\n2. The North American Sturgeon and Paddlefish Society (NASPS) website, which lists experts and provides details on the society\\'s mission to foster the conservation and restoration of sturgeon species in North America ([Visit NASPS](https://nasps-sturgeon.org/about/)).\\n\\n3. A press release from the U.S. Fish and Wildlife Service indicating that lake sturgeon do not require listing under the Endangered Species Act due to successful ongoing management efforts ([Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n\\n4. Information on the Conservation Genetics of Atlantic Sturgeon by the USGS, discussing genetic studies and management strategies for Atlantic Sturgeon populations ([Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n5. A story on restoring lake sturgeon along the Ontonagon River in Michigan and St. Louis River in Minnesota, detailing efforts by the Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office ([Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon)).\\n\\nThese resources should provide a strong foundation for understanding the current state of North American sturgeon species. If you require more detailed summaries or have any other questions, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The outline for North American sturgeon species, including their conservation status, size, lifespan, and conservation efforts, has been successfully created and saved to a file named \"North_American_Sturgeon_Overview\". If you need to review the document or require additional information, please let me know.', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!', name='DocWriter')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" indeed contains the three main topics outlined earlier:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need detailed information on any of these topics or have another specific request related to the document, please let me know, and I can provide the information or take further action as needed.', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" indeed contains the three main topics outlined earlier:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need detailed information on any of these topics or have another specific request related to the document, please let me know, and I can provide the information or take further action as needed.')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in super_graph.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n", + " )\n", + " ],\n", + " },\n", + " {\"recursion_limit\": 150},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] } ], "metadata": { @@ -406,7 +1096,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/multi_agent/multi-agent-collaboration.ipynb b/examples/multi_agent/multi-agent-collaboration.ipynb index 5c2c89284..ff06aec19 100644 --- a/examples/multi_agent/multi-agent-collaboration.ipynb +++ b/examples/multi_agent/multi-agent-collaboration.ipynb @@ -26,7 +26,10 @@ "id": "0d7b6dcc-c985-46e2-8457-7e6b0298b950", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core" + ] }, { "cell_type": "code", @@ -34,7 +37,24 @@ "id": "743c19df-6da9-4d1e-b2d2-ea40080b9fdc", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] }, { "cell_type": "markdown", @@ -54,7 +74,38 @@ "id": "4325a10e-38dc-4a98-9004-e1525eaba377", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import (\n BaseMessage,\n HumanMessage,\n ToolMessage,\n)\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(llm, tools, system_message: str):\n \"\"\"Create an agent.\"\"\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful AI assistant, collaborating with other assistants.\"\n \" Use the provided tools to progress towards answering the question.\"\n \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n \" will help where you left off. Execute what you can to make progress.\"\n \" If you or any of the other assistants have the final answer or deliverable,\"\n \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n )\n prompt = prompt.partial(system_message=system_message)\n prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n return prompt | llm.bind_tools(tools)"] + "source": [ + "from langchain_core.messages import (\n", + " BaseMessage,\n", + " HumanMessage,\n", + " ToolMessage,\n", + ")\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "\n", + "def create_agent(llm, tools, system_message: str):\n", + " \"\"\"Create an agent.\"\"\"\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful AI assistant, collaborating with other assistants.\"\n", + " \" Use the provided tools to progress towards answering the question.\"\n", + " \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n", + " \" will help where you left off. Execute what you can to make progress.\"\n", + " \" If you or any of the other assistants have the final answer or deliverable,\"\n", + " \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n", + " \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + " )\n", + " prompt = prompt.partial(system_message=system_message)\n", + " prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n", + " return prompt | llm.bind_tools(tools)" + ] }, { "cell_type": "markdown", @@ -72,7 +123,35 @@ "id": "ca076f3b-a729-4ca9-8f91-05c2ba58d610", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\nfrom langchain_experimental.utilities import PythonREPL\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n return (\n result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n )"] + "source": [ + "from typing import Annotated\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", + "from langchain_experimental.utilities import PythonREPL\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", + "\n", + "repl = PythonREPL()\n", + "\n", + "\n", + "@tool\n", + "def python_repl(\n", + " code: Annotated[str, \"The python code to execute to generate your chart.\"],\n", + "):\n", + " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", + " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", + " try:\n", + " result = repl.run(code)\n", + " except BaseException as e:\n", + " return f\"Failed to execute. Error: {repr(e)}\"\n", + " result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n", + " return (\n", + " result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n", + " )" + ] }, { "cell_type": "markdown", @@ -100,7 +179,19 @@ "id": "290c91d4-f6f4-443c-8181-233d39102974", "metadata": {}, "outputs": [], - "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_openai import ChatOpenAI\n\n\n# This defines the object that is passed between each node\n# in the graph. We will create different nodes for each agent and tool\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n sender: str"] + "source": [ + "import operator\n", + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "# This defines the object that is passed between each node\n", + "# in the graph. We will create different nodes for each agent and tool\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + " sender: str" + ] }, { "cell_type": "markdown", @@ -118,7 +209,46 @@ "id": "71b790ca-9cef-4b22-b469-4b1d5d8424d6", "metadata": {}, "outputs": [], - "source": ["import functools\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to create a node for a given agent\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n # We convert the agent output into a format that is suitable to append to the global state\n if isinstance(result, ToolMessage):\n pass\n else:\n result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n return {\n \"messages\": [result],\n # Since we have a strict workflow, we can\n # track the sender so we know who to pass to next.\n \"sender\": name,\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\n# Research agent and node\nresearch_agent = create_agent(\n llm,\n [tavily_tool],\n system_message=\"You should provide accurate data for the chart_generator to use.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# chart_generator\nchart_agent = create_agent(\n llm,\n [python_repl],\n system_message=\"Any charts you display will be visible by the user.\",\n)\nchart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"] + "source": [ + "import functools\n", + "\n", + "from langchain_core.messages import AIMessage\n", + "\n", + "\n", + "# Helper function to create a node for a given agent\n", + "def agent_node(state, agent, name):\n", + " result = agent.invoke(state)\n", + " # We convert the agent output into a format that is suitable to append to the global state\n", + " if isinstance(result, ToolMessage):\n", + " pass\n", + " else:\n", + " result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n", + " return {\n", + " \"messages\": [result],\n", + " # Since we have a strict workflow, we can\n", + " # track the sender so we know who to pass to next.\n", + " \"sender\": name,\n", + " }\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "# Research agent and node\n", + "research_agent = create_agent(\n", + " llm,\n", + " [tavily_tool],\n", + " system_message=\"You should provide accurate data for the chart_generator to use.\",\n", + ")\n", + "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n", + "\n", + "# chart_generator\n", + "chart_agent = create_agent(\n", + " llm,\n", + " [python_repl],\n", + " system_message=\"Any charts you display will be visible by the user.\",\n", + ")\n", + "chart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")" + ] }, { "cell_type": "markdown", @@ -136,7 +266,12 @@ "id": "d9a79c76-5c7c-42f6-91cf-635bc8305804", "metadata": {}, "outputs": [], - "source": ["from langgraph.prebuilt import ToolNode\n\ntools = [tavily_tool, python_repl]\ntool_node = ToolNode(tools)"] + "source": [ + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tools = [tavily_tool, python_repl]\n", + "tool_node = ToolNode(tools)" + ] }, { "cell_type": "markdown", @@ -154,7 +289,23 @@ "id": "4f4b4d37-e8a3-4abb-8d42-eaea26016f35", "metadata": {}, "outputs": [], - "source": ["# Either agent can decide to end\nfrom typing import Literal\n\n\ndef router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n # This is the router\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message.tool_calls:\n # The previous agent is invoking a tool\n return \"call_tool\"\n if \"FINAL ANSWER\" in last_message.content:\n # Any agent decided the work is done\n return \"__end__\"\n return \"continue\""] + "source": [ + "# Either agent can decide to end\n", + "from typing import Literal\n", + "\n", + "\n", + "def router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n", + " # This is the router\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " if last_message.tool_calls:\n", + " # The previous agent is invoking a tool\n", + " return \"call_tool\"\n", + " if \"FINAL ANSWER\" in last_message.content:\n", + " # Any agent decided the work is done\n", + " return \"__end__\"\n", + " return \"continue\"" + ] }, { "cell_type": "markdown", @@ -172,7 +323,39 @@ "id": "4dce3901-6ad5-4df5-8528-6e865cf96cb0", "metadata": {}, "outputs": [], - "source": ["workflow = StateGraph(AgentState)\n\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"chart_generator\", chart_node)\nworkflow.add_node(\"call_tool\", tool_node)\n\nworkflow.add_conditional_edges(\n \"Researcher\",\n router,\n {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\nworkflow.add_conditional_edges(\n \"chart_generator\",\n router,\n {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\n\nworkflow.add_conditional_edges(\n \"call_tool\",\n # Each agent node updates the 'sender' field\n # the tool calling node does not, meaning\n # this edge will route back to the original agent\n # who invoked the tool\n lambda x: x[\"sender\"],\n {\n \"Researcher\": \"Researcher\",\n \"chart_generator\": \"chart_generator\",\n },\n)\nworkflow.add_edge(START, \"Researcher\")\ngraph = workflow.compile()"] + "source": [ + "workflow = StateGraph(AgentState)\n", + "\n", + "workflow.add_node(\"Researcher\", research_node)\n", + "workflow.add_node(\"chart_generator\", chart_node)\n", + "workflow.add_node(\"call_tool\", tool_node)\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"Researcher\",\n", + " router,\n", + " {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n", + ")\n", + "workflow.add_conditional_edges(\n", + " \"chart_generator\",\n", + " router,\n", + " {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n", + ")\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"call_tool\",\n", + " # Each agent node updates the 'sender' field\n", + " # the tool calling node does not, meaning\n", + " # this edge will route back to the original agent\n", + " # who invoked the tool\n", + " lambda x: x[\"sender\"],\n", + " {\n", + " \"Researcher\": \"Researcher\",\n", + " \"chart_generator\": \"chart_generator\",\n", + " },\n", + ")\n", + "workflow.add_edge(START, \"Researcher\")\n", + "graph = workflow.compile()" + ] }, { "cell_type": "code", @@ -191,7 +374,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -213,45 +404,45 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_3zDlnDMUkWEJxnHASo59doCL', 'function': {'arguments': '{\"query\":\"UK GDP 2018 to 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 221, 'total_tokens': 247}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-ac6640c6-2bb4-478f-b3c4-eabf98cf4900-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2018 to 2023'}, 'id': 'call_3zDlnDMUkWEJxnHASo59doCL'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_3zDlnDMUkWEJxnHASo59doCL', 'function': {'arguments': '{\"query\":\"UK GDP 2018 to 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 221, 'total_tokens': 247}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-ac6640c6-2bb4-478f-b3c4-eabf98cf4900-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2018 to 2023'}, 'id': 'call_3zDlnDMUkWEJxnHASo59doCL'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/timeseries/ihyp/pn2\", \"content\": \"Preliminary estimate of GDP time series (PGDP), released on 27 April 2018\\\\nPublications that use this data\\\\nContact details for this data\\\\nFooter links\\\\nHelp\\\\nAbout ONS\\\\nConnect with us\\\\nAll content is available under the Open Government Licence v3.0, except where otherwise stated Year on Year growth: CVM SA %\\\\nDownload full time series as:\\\\nDownload filtered time series as:\\\\nTable\\\\nNotes\\\\nFollowing a quality review it has been identified that the methodology used to estimate elements of purchased software within gross fixed capital formation (GFCF) has led to some double counting from 1997 onwards. GDP quarterly national accounts time series (QNA), released on 22 December 2023\\\\nIHYP: UK Economic Accounts time series (UKEA), released on 22 December 2023\\\\nIHYP: GDP first quarterly estimate time series\\\\n(PN2), released on 10 November 2023\\\\nIHYP: Year on Year growth: CVM SA %\\\\nSource dataset: GDP first quarterly estimate time series (PN2)\\\\nContact: Niamh McAuley\\\\nRelease date: 10 November 2023\\\\nView previous versions\\\\n %\\\\nFilters\\\\nCustom time period\\\\nChart\\\\nDownload this time seriesGross Domestic Product:\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}, {\"url\": \"https://www.macrotrends.net/global-metrics/countries/GBR/united-kingdom/gdp-gross-domestic-product\", \"content\": \"U.K. gdp for 2021 was $3,141.51B, a 16.45% increase from 2020. U.K. gdp for 2020 was $2,697.81B, a 5.39% decline from 2019. U.K. gdp for 2019 was $2,851.41B, a 0.69% decline from 2018. GDP at purchaser\\'s prices is the sum of gross value added by all resident producers in the economy plus any product taxes and minus any subsidies not included in ...\"}, {\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}]', name='tavily_search_results_json', tool_call_id='call_3zDlnDMUkWEJxnHASo59doCL')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results provide some information about the UK's GDP over the past years, but most of the relevant data is either not in a structured format that can be easily extracted or it is behind a source that requires further access for detailed statistics. To proceed with generating a line graph, we need specific GDP values for each year from 2018 to 2023.\\n\\nHowever, one of the search results from macrotrends.net does provide specific GDP values for the years 2018 to 2021:\\n\\n- U.K. GDP for 2021 was $3,141.51 billion, a 16.45% increase from 2020.\\n- U.K. GDP for 2020 was $2,697.81 billion, a 5.39% decline from 2019.\\n- U.K. GDP for 2019 was $2,851.41 billion, a 0.69% decline from 2018.\\n\\nWe still need the GDP values for 2022 and 2023 to complete the dataset for the past five years. I will now conduct a further search to find the missing GDP data for 2022 and 2023.\", additional_kwargs={'tool_calls': [{'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 263, 'prompt_tokens': 3199, 'total_tokens': 3462}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-25901401-0d62-485f-b7d5-37e3c159effe-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023'}, 'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results provide some information about the UK's GDP over the past years, but most of the relevant data is either not in a structured format that can be easily extracted or it is behind a source that requires further access for detailed statistics. To proceed with generating a line graph, we need specific GDP values for each year from 2018 to 2023.\\n\\nHowever, one of the search results from macrotrends.net does provide specific GDP values for the years 2018 to 2021:\\n\\n- U.K. GDP for 2021 was $3,141.51 billion, a 16.45% increase from 2020.\\n- U.K. GDP for 2020 was $2,697.81 billion, a 5.39% decline from 2019.\\n- U.K. GDP for 2019 was $2,851.41 billion, a 0.69% decline from 2018.\\n\\nWe still need the GDP values for 2022 and 2023 to complete the dataset for the past five years. I will now conduct a further search to find the missing GDP data for 2022 and 2023.\", additional_kwargs={'tool_calls': [{'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 263, 'prompt_tokens': 3199, 'total_tokens': 3462}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-25901401-0d62-485f-b7d5-37e3c159effe-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023'}, 'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/gdpfirstquarterlyestimateuk/octobertodecember2023\", \"content\": \"This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and have set out our plans on how we will improve the way we communicate uncertainty.\\\\n Source: GDP first quarterly estimate from the Office for National Statistics\\\\nNotes\\\\nOffice for Statistics Regulation Revisions of estimates of UK GDP review\\\\nThe Office for Statistics Regulation (OSR) have completed a review of the practices around the preparation and release of information about revisions to estimates of GDP in our Impact of Blue Book 2023 article released on 1 September 2023, as announced on 6 September 2023 on the OSR website. Across 2023, the services sector sees revisions for the following reasons, with only Quarter 1 2023 seeing growth revised from our previous publication, including:\\\\nupdated input data for the deflator used for telecommunications\\\\nupdated seasonal adjustment which now uses a complete year of data for 2023\\\\nProduction\\\\nThe production sector is estimated to have decreased by 1.0% in the latest quarter after growth of 0.1% in Quarter 3 2023 (unrevised from our previous publication). Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are often based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\u201cerrors\\\\u201d in the popular sense of the word. Construction output in Great Britain: December 2023, new orders and Construction Output Price Indices, October to December 2023\\\\nBulletin | Released 15 February 2024\\\\nShort-term measures of output by the construction industry, contracts awarded for new construction work in Great Britain and a summary of the Construction Output Price Indices (OPIs) in the UK for Quarter 4 (October to December) 2023.\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/quarterlynationalaccounts/latest\", \"content\": \"Looking at the quarters open to revision, real GDP growth is unrevised in five of the seven quarters compared with the first quarterly estimate; however, it is important to note that the typical absolute average revision between the initial quarterly GDP estimate and the estimate three years later is 0.2 percentage points, as there is potential for revision to GDP when the annual supply and use balance occurs as more comprehensive annual data sources are available at a detailed industry and product level; all the GDP growth vintages for these quarters are shown in Table 4.\\\\n Overall the revisions to production reflect:\\\\nrevised volume data from the\\\\u00a0Department for Energy Security and Net Zero (DESNZ) for electricity, gas, steam and air conditioning supply\\\\nnew Value Added Tax (VAT) turnover data for Quarter 2 2023\\\\nnew and revised Monthly Business Survey data\\\\nseasonal adjustment models\\\\nFigure 7: Revisions to production output across 2022 and 2023 are mainly driven by manufacturing; and the electricity, gas and steam subsectors\\\\nConstruction\\\\nConstruction output rose by 0.4% in Quarter 3 2023, revised up from a first estimate increase of 0.1%. Professional, scientific and technical activities: the upward revision in Quarter 4 (Oct to Dec) 2022 and Quarter 1 2023 are driven by new and revised survey data within the advertising and market research industry; in Quarter 3 2023, six of the eight industries in this section are revised down, with the largest contribution coming from architecture and engineering activities; technical testing and analysis, because of revised survey data since our last publication and the new VAT data for Quarter 2 2023.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\\"errors\\\\\" in the popular sense of the word.\"}]', name='tavily_search_results_json', tool_call_id='call_nvB1wQyQuNeTrOXQZnEtgNDZ')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results did not provide exact figures for the UK's GDP in 2022 and 2023. While there are several references to GDP data, growth rates, and quarterly figures, we do not have the specific annual GDP values in a consistent currency format (such as USD or GBP) that would allow us to compile a complete dataset for the past five years.\\n\\nTo proceed, we will need to find another source or use a different method to obtain the missing GDP data for 2022 and 2023. If this data is not available, we may not be able to draw an accurate line graph of the UK's GDP over the past five years.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 6996, 'total_tokens': 7130}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-aa7d307d-cfdd-4c83-ad09-b6b0efbffe6e-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results did not provide exact figures for the UK's GDP in 2022 and 2023. While there are several references to GDP data, growth rates, and quarterly figures, we do not have the specific annual GDP values in a consistent currency format (such as USD or GBP) that would allow us to compile a complete dataset for the past five years.\\n\\nTo proceed, we will need to find another source or use a different method to obtain the missing GDP data for 2022 and 2023. If this data is not available, we may not be able to draw an accurate line graph of the UK's GDP over the past five years.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 6996, 'total_tokens': 7130}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-aa7d307d-cfdd-4c83-ad09-b6b0efbffe6e-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"It seems we have hit a roadblock in finding the exact GDP figures for the UK for the years 2022 and 2023. The information provided by the search results does not include the specific data we need. Therefore, we currently do not have the complete dataset to generate a line graph of the UK's GDP over the past five years.\\n\\nTo proceed, we might need to look for an official statistical release or a comprehensive economic report that includes the GDP figures for 2022 and 2023. If such data can be obtained, we can then use it to create the desired line graph. Without this data, we cannot fulfill the request as specified.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 7150, 'total_tokens': 7284}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-a667e647-45b2-414e-b301-81f846fa59ad-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"It seems we have hit a roadblock in finding the exact GDP figures for the UK for the years 2022 and 2023. The information provided by the search results does not include the specific data we need. Therefore, we currently do not have the complete dataset to generate a line graph of the UK's GDP over the past five years.\\n\\nTo proceed, we might need to look for an official statistical release or a comprehensive economic report that includes the GDP figures for 2022 and 2023. If such data can be obtained, we can then use it to create the desired line graph. Without this data, we cannot fulfill the request as specified.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 7150, 'total_tokens': 7284}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-a667e647-45b2-414e-b301-81f846fa59ad-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I am unable to proceed with generating the line graph as we lack the specific GDP data for the UK for the years 2022 and 2023. To complete this task, we would need to acquire the missing data points. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, we must conclude this attempt.\", response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 7276, 'total_tokens': 7363}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-1731ab9f-7ee9-4ff9-a920-7b998a41fe4d-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I am unable to proceed with generating the line graph as we lack the specific GDP data for the UK for the years 2022 and 2023. To complete this task, we would need to acquire the missing data points. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, we must conclude this attempt.\", response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 7276, 'total_tokens': 7363}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-1731ab9f-7ee9-4ff9-a920-7b998a41fe4d-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"As of my last attempt, I have not been able to find the exact GDP figures for the UK for the years 2022 and 2023. Without these figures, it's not possible to generate a complete and accurate line graph of the UK's GDP over the past five years. Should the data become available or if an alternative source can provide the figures, we can proceed with creating the graph. For the time being, this task cannot be completed.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 7383, 'total_tokens': 7476}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-6e12dccd-b1b2-4c7d-befd-4a338d027756-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"As of my last attempt, I have not been able to find the exact GDP figures for the UK for the years 2022 and 2023. Without these figures, it's not possible to generate a complete and accurate line graph of the UK's GDP over the past five years. Should the data become available or if an alternative source can provide the figures, we can proceed with creating the graph. For the time being, this task cannot be completed.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 7383, 'total_tokens': 7476}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-6e12dccd-b1b2-4c7d-befd-4a338d027756-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I have attempted to gather the UK's GDP data for the past five years to draw a line graph, but I was only able to obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 111, 'prompt_tokens': 7468, 'total_tokens': 7579}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-9d93e637-cedd-4b17-945a-cc97e490432c-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I have attempted to gather the UK's GDP data for the past five years to draw a line graph, but I was only able to obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 111, 'prompt_tokens': 7468, 'total_tokens': 7579}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-9d93e637-cedd-4b17-945a-cc97e490432c-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"I've exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7599, 'total_tokens': 7735}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-82063d76-6655-473e-9f6a-a5c005d92bd3-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"I've exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7599, 'total_tokens': 7735}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-82063d76-6655-473e-9f6a-a5c005d92bd3-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I have exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7727, 'total_tokens': 7863}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-954e7bb2-ae3e-4610-9927-4b16d733414e-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I have exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7727, 'total_tokens': 7863}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-954e7bb2-ae3e-4610-9927-4b16d733414e-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"Having attempted to find the UK's GDP figures for the years 2022 and 2023 through available data sources and coming up short, it appears we are currently unable to produce a complete line graph of the UK's GDP over the past five years as initially requested. To create a graph, we would need precise GDP data for each year from 2018 to 2023.\\n\\nIf the missing GDP data for 2022 and 2023 becomes available or if an alternative source can provide these figures, we can then proceed to generate the line graph. As of now, this task must be paused until the necessary data can be obtained.\", response_metadata={'token_usage': {'completion_tokens': 130, 'prompt_tokens': 7883, 'total_tokens': 8013}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8d1382e2-a77c-4d2f-b06a-2597be59542b-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"Having attempted to find the UK's GDP figures for the years 2022 and 2023 through available data sources and coming up short, it appears we are currently unable to produce a complete line graph of the UK's GDP over the past five years as initially requested. To create a graph, we would need precise GDP data for each year from 2018 to 2023.\\n\\nIf the missing GDP data for 2022 and 2023 becomes available or if an alternative source can provide these figures, we can then proceed to generate the line graph. As of now, this task must be paused until the necessary data can be obtained.\", response_metadata={'token_usage': {'completion_tokens': 130, 'prompt_tokens': 7883, 'total_tokens': 8013}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8d1382e2-a77c-4d2f-b06a-2597be59542b-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results do not provide the exact GDP figures for the UK for 2022 and 2023. Without this information, it is not possible to generate a line graph of the UK's GDP over the past five years. We would require the GDP values for those two years to complete the dataset and create the graph. As of now, I must conclude this task until the necessary data becomes available.\", response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 8005, 'total_tokens': 8087}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-246b9b29-ffc7-4da9-a09a-0dcfbbb3bd7a-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results do not provide the exact GDP figures for the UK for 2022 and 2023. Without this information, it is not possible to generate a line graph of the UK's GDP over the past five years. We would require the GDP values for those two years to complete the dataset and create the graph. As of now, I must conclude this task until the necessary data becomes available.\", response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 8005, 'total_tokens': 8087}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-246b9b29-ffc7-4da9-a09a-0dcfbbb3bd7a-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"I have attempted to find the UK's GDP for the past five years to create a line graph, but I could only obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 108, 'prompt_tokens': 8107, 'total_tokens': 8215}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-f2847a80-610d-49c5-924a-ccffccb7cd5a-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"I have attempted to find the UK's GDP for the past five years to create a line graph, but I could only obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 108, 'prompt_tokens': 8107, 'total_tokens': 8215}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-f2847a80-610d-49c5-924a-ccffccb7cd5a-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"As of now, I was unable to obtain the complete data for the UK's GDP over the past five years due to lack of specific information for the years 2022 and 2023. Therefore, it's not possible to draw a line graph of the UK's GDP for this period without the complete dataset. Further action to acquire the missing data would be required to proceed.\", response_metadata={'token_usage': {'completion_tokens': 77, 'prompt_tokens': 8207, 'total_tokens': 8284}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-28e09000-8787-4ac0-a7d8-0aba888c2520-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"As of now, I was unable to obtain the complete data for the UK's GDP over the past five years due to lack of specific information for the years 2022 and 2023. Therefore, it's not possible to draw a line graph of the UK's GDP for this period without the complete dataset. Further action to acquire the missing data would be required to proceed.\", response_metadata={'token_usage': {'completion_tokens': 77, 'prompt_tokens': 8207, 'total_tokens': 8284}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-28e09000-8787-4ac0-a7d8-0aba888c2520-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"It appears we have encountered a limitation in obtaining the complete GDP data for the UK for 2022 and 2023. Without these figures, we cannot create the line graph of the UK's GDP over the past five years as requested. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, this task will have to be concluded without completion.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 8304, 'total_tokens': 8397}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8bf8f247-cb86-4ef0-a81b-14da2d27b6f1-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"It appears we have encountered a limitation in obtaining the complete GDP data for the UK for 2022 and 2023. Without these figures, we cannot create the line graph of the UK's GDP over the past five years as requested. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, this task will have to be concluded without completion.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 8304, 'total_tokens': 8397}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8bf8f247-cb86-4ef0-a81b-14da2d27b6f1-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_df3UdS3vJkJFB30O0WYq38k8', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023 statistics\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 8389, 'total_tokens': 8415}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-e1577cc7-5673-4821-9683-34947c7a2bc5-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023 statistics'}, 'id': 'call_df3UdS3vJkJFB30O0WYq38k8'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_df3UdS3vJkJFB30O0WYq38k8', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023 statistics\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 8389, 'total_tokens': 8415}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-e1577cc7-5673-4821-9683-34947c7a2bc5-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023 statistics'}, 'id': 'call_df3UdS3vJkJFB30O0WYq38k8'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/quarterlynationalaccounts/latest\", \"content\": \"Looking at the quarters open to revision, real GDP growth is unrevised in five of the seven quarters compared with the first quarterly estimate; however, it is important to note that the typical absolute average revision between the initial quarterly GDP estimate and the estimate three years later is 0.2 percentage points, as there is potential for revision to GDP when the annual supply and use balance occurs as more comprehensive annual data sources are available at a detailed industry and product level; all the GDP growth vintages for these quarters are shown in Table 4.\\\\n Overall the revisions to production reflect:\\\\nrevised volume data from the\\\\u00a0Department for Energy Security and Net Zero (DESNZ) for electricity, gas, steam and air conditioning supply\\\\nnew Value Added Tax (VAT) turnover data for Quarter 2 2023\\\\nnew and revised Monthly Business Survey data\\\\nseasonal adjustment models\\\\nFigure 7: Revisions to production output across 2022 and 2023 are mainly driven by manufacturing; and the electricity, gas and steam subsectors\\\\nConstruction\\\\nConstruction output rose by 0.4% in Quarter 3 2023, revised up from a first estimate increase of 0.1%. Professional, scientific and technical activities: the upward revision in Quarter 4 (Oct to Dec) 2022 and Quarter 1 2023 are driven by new and revised survey data within the advertising and market research industry; in Quarter 3 2023, six of the eight industries in this section are revised down, with the largest contribution coming from architecture and engineering activities; technical testing and analysis, because of revised survey data since our last publication and the new VAT data for Quarter 2 2023.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\\"errors\\\\\" in the popular sense of the word.\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/gdpmonthlyestimateuk/latest\", \"content\": \"The following list contains the full SIC names of industries included in consumer-facing services and their corresponding shortened industry name where this has been used in Figure 5:\\\\nwholesale and retail trade and repair of motor vehicles and motorcycles - sales and repairs of motor vehicles\\\\nretail trade, except of motor vehicles and motorcycles - retail except motor vehicles\\\\nrail transport\\\\naccommodation\\\\nfood and beverage service activities - food and beverage\\\\nbuying and selling, renting and operating of own or leased real estate, excluding imputed rent - real estate activities\\\\nveterinary activities\\\\ntravel agency, tour operator and other reservation service and related activities - travel and tourism activities\\\\ngambling and betting services\\\\nsports activities and amusement and recreation activities - sports, amusement and recreation\\\\nactivities of membership organisations\\\\nother personal service activities\\\\nactivities of households as employers of domestic personnel - households as employers of domestic personnel\\\\nAdditional bank holiday in May 2023 for the Coronation of King Charles III\\\\nThere was an additional bank holiday for the coronation of King Charles III on Monday 8 May 2023. Source: Monthly GDP estimate from Office for National Statistics\\\\nThe main reasons for revisions in October 2023 are:\\\\nin the services sector, the upwards revision is mainly from updated and late monthly business survey responses primarily in the information and communication subsection\\\\nin the production sector, the downward revision is from source data replacing forecasts in mining and quarrying and electricity, gas, steam and air conditioning supply, as well as revised and late monthly business survey responses predominantly in the manufacture of pharmaceutical products and pharmaceutical preparations, and sewerage industries\\\\nin the construction sector, the upwards revisions is because of updated and late monthly business survey responses for new public housing and other public new work\\\\nDetails on the revisions to monthly GDP prior to October 2023 are provided in our GDP quarterly national accounts, UK: July to September 2023 bulletin.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n11. The main data source for these statistics is the Monthly Business Survey (MBS) and response rates for each can be found in our:\\\\nOutput in the construction industry dataset\\\\nMonthly Business Survey (production) response rates dataset\\\\nCurrent and historical Monthly Business Survey (services) response rates dataset\\\\nOur monthly gross domestic product (GDP) data sources catalogue provides a full breakdown of the data used in this publication.\\\\n On the negative side, the lack of demand for construction products was prevalent across manufacturing, with manufacture of wood, rubber and plastic, glass, cement and plaster all seeing declines on the month in November 2023 in line with the two consecutive monthly falls in construction output in October and November 2023.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}]', name='tavily_search_results_json', tool_call_id='call_df3UdS3vJkJFB30O0WYq38k8')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results unfortunately do not provide specific figures for the UK's GDP in 2022 and 2023. While there are references to GDP data and related economic indicators, the exact annual GDP values needed to create the line graph are not present.\\n\\nOne possible approach to obtain these figures would be to access detailed statistical databases or reports from official sources such as the Office for National Statistics (ONS) or economic research institutions that publish historical GDP data. These sources might have the most recent and accurate GDP figures available for the UK, which are necessary to complete the line graph.\\n\\nSince I cannot directly access or retrieve the data from these sources using the tools available to me, I recommend consulting such databases or reports to find the UK's GDP for 2022 and 2023. Once the data is obtained, it can be used to create the line graph.\", response_metadata={'token_usage': {'completion_tokens': 172, 'prompt_tokens': 12099, 'total_tokens': 12271}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-7b4bcbc3-3ed0-4fa0-8e5d-a366c5a80d5a-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results unfortunately do not provide specific figures for the UK's GDP in 2022 and 2023. While there are references to GDP data and related economic indicators, the exact annual GDP values needed to create the line graph are not present.\\n\\nOne possible approach to obtain these figures would be to access detailed statistical databases or reports from official sources such as the Office for National Statistics (ONS) or economic research institutions that publish historical GDP data. These sources might have the most recent and accurate GDP figures available for the UK, which are necessary to complete the line graph.\\n\\nSince I cannot directly access or retrieve the data from these sources using the tools available to me, I recommend consulting such databases or reports to find the UK's GDP for 2022 and 2023. Once the data is obtained, it can be used to create the line graph.\", response_metadata={'token_usage': {'completion_tokens': 172, 'prompt_tokens': 12099, 'total_tokens': 12271}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-7b4bcbc3-3ed0-4fa0-8e5d-a366c5a80d5a-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021, 2022, 2023],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2697.81, 3141.51, None, None]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2023\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 240, 'prompt_tokens': 12291, 'total_tokens': 12531}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-6cff57bc-ba87-4690-9528-4d15bba7986c-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl'}])], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021, 2022, 2023],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2697.81, 3141.51, None, None]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2023\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 240, 'prompt_tokens': 12291, 'total_tokens': 12531}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-6cff57bc-ba87-4690-9528-4d15bba7986c-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl'}])], 'sender': 'chart_generator'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content=\"Successfully executed:\\n```python\\nimport matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\\n```\\nStdout: ValueError('x and y must have same first dimension, but have shapes (6,) and (5,)')\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\", name='python_repl', tool_call_id='call_JPVxDAzEFi21crVT7Rt6SRJl')]}}\n", "----\n" @@ -281,24 +472,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'chart_generator': {'messages': [AIMessage(content=\"Unfortunately, I made an error in the provided code. There was a mismatch in the dimensions of the 'Year' and 'GDP (Billion USD)' data arrays, which caused a ValueError. Additionally, I mistakenly included placeholders for the years 2022 and 2023 without having the actual GDP data for those years.\\n\\nLet's correct this and generate a line graph with the data we have up to 2021. I will revise the code to only include the years for which we have data, and then we can create the graph accordingly.\", additional_kwargs={'tool_calls': [{'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# No data for 2022 and 2023 are available\\\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2851.41, 2697.81, 3141.51]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2021\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 359, 'prompt_tokens': 12796, 'total_tokens': 13155}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-0d4a67d2-696a-4955-990b-9a9d775b7635-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX'}])], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"Unfortunately, I made an error in the provided code. There was a mismatch in the dimensions of the 'Year' and 'GDP (Billion USD)' data arrays, which caused a ValueError. Additionally, I mistakenly included placeholders for the years 2022 and 2023 without having the actual GDP data for those years.\\n\\nLet's correct this and generate a line graph with the data we have up to 2021. I will revise the code to only include the years for which we have data, and then we can create the graph accordingly.\", additional_kwargs={'tool_calls': [{'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# No data for 2022 and 2023 are available\\\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2851.41, 2697.81, 3141.51]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2021\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 359, 'prompt_tokens': 12796, 'total_tokens': 13155}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-0d4a67d2-696a-4955-990b-9a9d775b7635-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX'}])], 'sender': 'chart_generator'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content=\"Successfully executed:\\n```python\\nimport matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\\n```\\nStdout: \\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\", name='python_repl', tool_call_id='call_1zGQMGouC0oFQJRUkNPvs9zX')]}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"FINAL ANSWER\\n\\nI have generated a line graph for the UK's GDP from 2018 to 2021 using the available data. Unfortunately, due to the lack of data for 2022 and 2023, the graph only includes figures up to 2021. Here is the graph:\\n\\n[Graph Image]\\n\\nPlease note that the data for 2022 and 2023 should be added to this graph once it becomes available to complete the analysis for the past five years.\", response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 13412, 'total_tokens': 13511}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-3474a61c-0773-4e44-bd6e-2e88cf56bb90-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"FINAL ANSWER\\n\\nI have generated a line graph for the UK's GDP from 2018 to 2021 using the available data. Unfortunately, due to the lack of data for 2022 and 2023, the graph only includes figures up to 2021. Here is the graph:\\n\\n[Graph Image]\\n\\nPlease note that the data for 2022 and 2023 should be added to this graph once it becomes available to complete the analysis for the past five years.\", response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 13412, 'total_tokens': 13511}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-3474a61c-0773-4e44-bd6e-2e88cf56bb90-0')], 'sender': 'chart_generator'}}\n", "----\n" ] } ], - "source": ["events = graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Fetch the UK's GDP over the past 5 years,\"\n \" then draw a line graph of it.\"\n \" Once you code it up, finish.\"\n )\n ],\n },\n # Maximum number of steps to take in the graph\n {\"recursion_limit\": 150},\n)\nfor s in events:\n print(s)\n print(\"----\")"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "010fc36e-4116-4758-bcac-b02c7dcd405d", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "events = graph.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Fetch the UK's GDP over the past 5 years,\"\n", + " \" then draw a line graph of it.\"\n", + " \" Once you code it up, finish.\"\n", + " )\n", + " ],\n", + " },\n", + " # Maximum number of steps to take in the graph\n", + " {\"recursion_limit\": 150},\n", + ")\n", + "for s in events:\n", + " print(s)\n", + " print(\"----\")" + ] } ], "metadata": { diff --git a/examples/option1.png b/examples/option1.png new file mode 100644 index 000000000..485a48b65 Binary files /dev/null and b/examples/option1.png differ diff --git a/examples/option2.png b/examples/option2.png new file mode 100644 index 000000000..b24107505 Binary files /dev/null and b/examples/option2.png differ diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index 3c74bd06d..ab6680230 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -329,6 +329,7 @@ "\n", "tools = [get_context, cite_context_sources]\n", "\n", + "\n", "# Define the function that calls the model\n", "def call_model(state, config):\n", " messages = state[\"messages\"]\n", diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb index 2e60d805e..b790e312e 100644 --- a/examples/pass_private_state.ipynb +++ b/examples/pass_private_state.ipynb @@ -72,12 +72,12 @@ "# Node to retrieve documents\n", "def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n", " # Replace this with real logic\n", - " return {\"docs\": [state['query']] * 2}\n", + " return {\"docs\": [state[\"query\"]] * 2}\n", "\n", "\n", "# Node to generate answer\n", "def generate(state: GenerateInputState) -> OverallState:\n", - " return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n", + " return {\"answer\": \"\\n\\n\".join(state[\"docs\"] + [state[\"question\"]])}\n", "\n", "\n", "graph = StateGraph(OverallState)\n", @@ -92,14 +92,6 @@ "\n", "graph.invoke({\"question\": \"foo\"})" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ffc2d8c-717f-42c9-b0aa-15b178a5cc8b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/persistence.ipynb b/examples/persistence.ipynb index 869043cf1..376706da5 100644 --- a/examples/persistence.ipynb +++ b/examples/persistence.ipynb @@ -561,14 +561,6 @@ "):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eb20430f", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -587,7 +579,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/examples/persistence_mongodb.ipynb b/examples/persistence_mongodb.ipynb index 99ce73057..54dfdc638 100644 --- a/examples/persistence_mongodb.ipynb +++ b/examples/persistence_mongodb.ipynb @@ -630,7 +630,7 @@ " upsert=True,\n", " )\n", " )\n", - " await self.db[\"checkpoint_writes\"].bulk_write(operations)\n" + " await self.db[\"checkpoint_writes\"].bulk_write(operations)" ] }, { @@ -685,7 +685,9 @@ "metadata": {}, "outputs": [], "source": [ - "with MongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "with MongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"1\"}}\n", " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", @@ -796,10 +798,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncMongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "async with AsyncMongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index ef8be17cb..b4fa1e2fe 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -122,7 +122,7 @@ "metadata": {}, "outputs": [], "source": [ - "DB_URI = \"postgresql://postgres:postgres@localhost:5441/postgres?sslmode=disable\"" + "DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"" ] }, { @@ -132,12 +132,9 @@ "metadata": {}, "outputs": [], "source": [ - "from psycopg.rows import dict_row\n", - "\n", - "connection_kwargs ={\n", + "connection_kwargs = {\n", " \"autocommit\": True,\n", " \"prepare_threshold\": 0,\n", - " \"row_factory\": dict_row,\n", "}" ] }, @@ -162,15 +159,13 @@ "source": [ "from psycopg_pool import ConnectionPool\n", "\n", - "pool = ConnectionPool(\n", + "with ConnectionPool(\n", " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", - ")\n", - "\n", - "with pool.connection() as conn:\n", - " checkpointer = PostgresSaver(conn)\n", + " kwargs=connection_kwargs,\n", + ") as pool:\n", + " checkpointer = PostgresSaver(pool)\n", "\n", " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", " checkpointer.setup()\n", @@ -394,9 +389,9 @@ " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", - ") as pool, pool.connection() as conn:\n", - " checkpointer = AsyncPostgresSaver(conn)\n", + " kwargs=connection_kwargs,\n", + ") as pool:\n", + " checkpointer = AsyncPostgresSaver(pool)\n", "\n", " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", " # await checkpointer.setup()\n", @@ -551,9 +546,9 @@ ], "metadata": { "kernelspec": { - "display_name": "langgraph-postgres", + "display_name": "langgraph", "language": "python", - "name": "langgraph-postgres" + "name": "langgraph" }, "language_info": { "codemirror_mode": { diff --git a/examples/persistence_redis.ipynb b/examples/persistence_redis.ipynb index 3b2ad1170..34d78a53f 100644 --- a/examples/persistence_redis.ipynb +++ b/examples/persistence_redis.ipynb @@ -530,7 +530,9 @@ "\n", " @classmethod\n", " @asynccontextmanager\n", - " async def from_conn_info(cls, *, host: str, port: int, db: int) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", + " async def from_conn_info(\n", + " cls, *, host: str, port: int, db: int\n", + " ) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", " conn = None\n", " try:\n", " conn = AsyncRedis(host=host, port=port, db=db)\n", @@ -887,10 +889,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncRedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\n", + "async with AsyncRedisSaver.from_conn_info(\n", + " host=\"localhost\", port=6379, db=0\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/plan-and-execute/plan-and-execute.ipynb b/examples/plan-and-execute/plan-and-execute.ipynb index 95eb7d384..1f960d035 100644 --- a/examples/plan-and-execute/plan-and-execute.ipynb +++ b/examples/plan-and-execute/plan-and-execute.ipynb @@ -45,7 +45,10 @@ "id": "b451b58a-89bd-424f-8c06-0d9fe325e01b", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python" + ] }, { "cell_type": "markdown", @@ -61,7 +64,19 @@ "id": "ce438281-08d5-4804-afe7-e4089f7b016b", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")\n", + "_set_env(\"TAVILY_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -77,7 +92,11 @@ "id": "01f460d1-f26f-47d1-ae76-de74d5d851de", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\""] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\"" + ] }, { "cell_type": "markdown", @@ -95,7 +114,11 @@ "id": "25b9ec62-0675-4715-811c-9b32c635b22f", "metadata": {}, "outputs": [], - "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=3)]"] + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=3)]" + ] }, { "cell_type": "markdown", @@ -128,7 +151,20 @@ ] } ], - "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import create_react_agent\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"wfh/react-agent-executor\")\nprompt.pretty_print()\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\nagent_executor = create_react_agent(llm, tools, messages_modifier=prompt)"] + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"wfh/react-agent-executor\")\n", + "prompt.pretty_print()\n", + "\n", + "# Choose the LLM that will drive the agent\n", + "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", + "agent_executor = create_react_agent(llm, tools, messages_modifier=prompt)" + ] }, { "cell_type": "code", @@ -150,7 +186,9 @@ "output_type": "execute_result" } ], - "source": ["agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})"] + "source": [ + "agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})" + ] }, { "cell_type": "markdown", @@ -174,7 +212,17 @@ "id": "8eeeaeea-8f10-4fbe-8e24-4e1a2381a009", "metadata": {}, "outputs": [], - "source": ["import operator\nfrom typing import Annotated, List, Tuple, TypedDict\n\n\nclass PlanExecute(TypedDict):\n input: str\n plan: List[str]\n past_steps: Annotated[List[Tuple], operator.add]\n response: str"] + "source": [ + "import operator\n", + "from typing import Annotated, List, Tuple, TypedDict\n", + "\n", + "\n", + "class PlanExecute(TypedDict):\n", + " input: str\n", + " plan: List[str]\n", + " past_steps: Annotated[List[Tuple], operator.add]\n", + " response: str" + ] }, { "cell_type": "markdown", @@ -192,7 +240,17 @@ "id": "4a88626d-6dfd-4488-87f0-a9a0dd6da44c", "metadata": {}, "outputs": [], - "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Plan(BaseModel):\n \"\"\"Plan to follow in future\"\"\"\n\n steps: List[str] = Field(\n description=\"different steps to follow, should be in sorted order\"\n )"] + "source": [ + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "\n", + "class Plan(BaseModel):\n", + " \"\"\"Plan to follow in future\"\"\"\n", + "\n", + " steps: List[str] = Field(\n", + " description=\"different steps to follow, should be in sorted order\"\n", + " )" + ] }, { "cell_type": "code", @@ -200,7 +258,24 @@ "id": "ec7b1867-1ea3-4df3-9a98-992a1c32ec49", "metadata": {}, "outputs": [], - "source": ["from langchain_core.prompts import ChatPromptTemplate\n\nplanner_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\nplanner = planner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Plan)"] + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "planner_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"For the given objective, come up with a simple step by step plan. \\\n", + "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", + "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n", + " ),\n", + " (\"placeholder\", \"{messages}\"),\n", + " ]\n", + ")\n", + "planner = planner_prompt | ChatOpenAI(\n", + " model=\"gpt-4o\", temperature=0\n", + ").with_structured_output(Plan)" + ] }, { "cell_type": "code", @@ -219,7 +294,15 @@ "output_type": "execute_result" } ], - "source": ["planner.invoke(\n {\n \"messages\": [\n (\"user\", \"what is the hometown of the current Australia open winner?\")\n ]\n }\n)"] + "source": [ + "planner.invoke(\n", + " {\n", + " \"messages\": [\n", + " (\"user\", \"what is the hometown of the current Australia open winner?\")\n", + " ]\n", + " }\n", + ")" + ] }, { "cell_type": "markdown", @@ -237,7 +320,47 @@ "id": "ec2d12cc-016a-44d1-aa08-4c5ce1e8fe2a", "metadata": {}, "outputs": [], - "source": ["from typing import Union\n\n\nclass Response(BaseModel):\n \"\"\"Response to user.\"\"\"\n\n response: str\n\n\nclass Act(BaseModel):\n \"\"\"Action to perform.\"\"\"\n\n action: Union[Response, Plan] = Field(\n description=\"Action to perform. If you want to respond to user, use Response. \"\n \"If you need to further use tools to get the answer, use Plan.\"\n )\n\n\nreplanner_prompt = ChatPromptTemplate.from_template(\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n\nYour objective was this:\n{input}\n\nYour original plan was this:\n{plan}\n\nYou have currently done the follow steps:\n{past_steps}\n\nUpdate your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n)\n\n\nreplanner = replanner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Act)"] + "source": [ + "from typing import Union\n", + "\n", + "\n", + "class Response(BaseModel):\n", + " \"\"\"Response to user.\"\"\"\n", + "\n", + " response: str\n", + "\n", + "\n", + "class Act(BaseModel):\n", + " \"\"\"Action to perform.\"\"\"\n", + "\n", + " action: Union[Response, Plan] = Field(\n", + " description=\"Action to perform. If you want to respond to user, use Response. \"\n", + " \"If you need to further use tools to get the answer, use Plan.\"\n", + " )\n", + "\n", + "\n", + "replanner_prompt = ChatPromptTemplate.from_template(\n", + " \"\"\"For the given objective, come up with a simple step by step plan. \\\n", + "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", + "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", + "\n", + "Your objective was this:\n", + "{input}\n", + "\n", + "Your original plan was this:\n", + "{plan}\n", + "\n", + "You have currently done the follow steps:\n", + "{past_steps}\n", + "\n", + "Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n", + ")\n", + "\n", + "\n", + "replanner = replanner_prompt | ChatOpenAI(\n", + " model=\"gpt-4o\", temperature=0\n", + ").with_structured_output(Act)" + ] }, { "cell_type": "markdown", @@ -255,7 +378,43 @@ "id": "6c8e0dad-bcea-4c9a-8922-0d820892e2d0", "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\n\nasync def execute_step(state: PlanExecute):\n plan = state[\"plan\"]\n plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n task = plan[0]\n task_formatted = f\"\"\"For the following plan:\n{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n agent_response = await agent_executor.ainvoke(\n {\"messages\": [(\"user\", task_formatted)]}\n )\n return {\n \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n }\n\n\nasync def plan_step(state: PlanExecute):\n plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n return {\"plan\": plan.steps}\n\n\nasync def replan_step(state: PlanExecute):\n output = await replanner.ainvoke(state)\n if isinstance(output.action, Response):\n return {\"response\": output.action.response}\n else:\n return {\"plan\": output.action.steps}\n\n\ndef should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n if \"response\" in state and state[\"response\"]:\n return \"__end__\"\n else:\n return \"agent\""] + "source": [ + "from typing import Literal\n", + "\n", + "\n", + "async def execute_step(state: PlanExecute):\n", + " plan = state[\"plan\"]\n", + " plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n", + " task = plan[0]\n", + " task_formatted = f\"\"\"For the following plan:\n", + "{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n", + " agent_response = await agent_executor.ainvoke(\n", + " {\"messages\": [(\"user\", task_formatted)]}\n", + " )\n", + " return {\n", + " \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n", + " }\n", + "\n", + "\n", + "async def plan_step(state: PlanExecute):\n", + " plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n", + " return {\"plan\": plan.steps}\n", + "\n", + "\n", + "async def replan_step(state: PlanExecute):\n", + " output = await replanner.ainvoke(state)\n", + " if isinstance(output.action, Response):\n", + " return {\"response\": output.action.response}\n", + " else:\n", + " return {\"plan\": output.action.steps}\n", + "\n", + "\n", + "def should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n", + " if \"response\" in state and state[\"response\"]:\n", + " return \"__end__\"\n", + " else:\n", + " return \"agent\"" + ] }, { "cell_type": "code", @@ -263,7 +422,39 @@ "id": "e954cea0-5ccc-46c2-a27b-f5b7185b597d", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import StateGraph, START\n\nworkflow = StateGraph(PlanExecute)\n\n# Add the plan node\nworkflow.add_node(\"planner\", plan_step)\n\n# Add the execution step\nworkflow.add_node(\"agent\", execute_step)\n\n# Add a replan node\nworkflow.add_node(\"replan\", replan_step)\n\nworkflow.add_edge(START, \"planner\")\n\n# From plan we go to agent\nworkflow.add_edge(\"planner\", \"agent\")\n\n# From agent, we replan\nworkflow.add_edge(\"agent\", \"replan\")\n\nworkflow.add_conditional_edges(\n \"replan\",\n # Next, we pass in the function that will determine which node is called next.\n should_end,\n)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import StateGraph, START\n", + "\n", + "workflow = StateGraph(PlanExecute)\n", + "\n", + "# Add the plan node\n", + "workflow.add_node(\"planner\", plan_step)\n", + "\n", + "# Add the execution step\n", + "workflow.add_node(\"agent\", execute_step)\n", + "\n", + "# Add a replan node\n", + "workflow.add_node(\"replan\", replan_step)\n", + "\n", + "workflow.add_edge(START, \"planner\")\n", + "\n", + "# From plan we go to agent\n", + "workflow.add_edge(\"planner\", \"agent\")\n", + "\n", + "# From agent, we replan\n", + "workflow.add_edge(\"agent\", \"replan\")\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"replan\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_end,\n", + ")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", @@ -282,7 +473,11 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" + ] }, { "cell_type": "code", @@ -310,7 +505,14 @@ ] } ], - "source": ["config = {\"recursion_limit\": 50}\ninputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\nasync for event in app.astream(inputs, config=config):\n for k, v in event.items():\n if k != \"__end__\":\n print(v)"] + "source": [ + "config = {\"recursion_limit\": 50}\n", + "inputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\n", + "async for event in app.astream(inputs, config=config):\n", + " for k, v in event.items():\n", + " if k != \"__end__\":\n", + " print(v)" + ] }, { "cell_type": "markdown", @@ -321,14 +523,6 @@ "\n", "Congrats on making a plan-and-execute agent! One known limitations of the above design is that each task is still executed in sequence, meaning embarrassingly parallel operations all add to the total execution time. You could improve on this by having each task represented as a DAG (similar to LLMCompiler), rather than a regular list." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ad8f7955-2cc9-4ebb-8c41-13abb3351a24", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_adaptive_rag.ipynb b/examples/rag/langgraph_adaptive_rag.ipynb index 8caa57bfa..f612183c5 100644 --- a/examples/rag/langgraph_adaptive_rag.ipynb +++ b/examples/rag/langgraph_adaptive_rag.ipynb @@ -47,7 +47,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python" + "%%capture --no-stderr\n", + "! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python" ] }, { @@ -57,7 +58,12 @@ "metadata": {}, "outputs": [], "source": [ - "### LLMs\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\"\nos.environ[\"COHERE_API_KEY\"] = \"\"\nos.environ[\"TAVILY_API_KEY\"] = \"\"" + "### LLMs\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"\"\n", + "os.environ[\"COHERE_API_KEY\"] = \"\"\n", + "os.environ[\"TAVILY_API_KEY\"] = \"\"" ] }, { @@ -77,7 +83,10 @@ "metadata": {}, "outputs": [], "source": [ - "### Tracing (optional)\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\"" + "### Tracing (optional)\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" ] }, { @@ -95,7 +104,42 @@ "metadata": {}, "outputs": [], "source": [ - "### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\n### from langchain_cohere import CohereEmbeddings\n\n# Set embeddings\nembd = OpenAIEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=500, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=embd,\n)\nretriever = vectorstore.as_retriever()" + "### Build Index\n", + "\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "### from langchain_cohere import CohereEmbeddings\n", + "\n", + "# Set embeddings\n", + "embd = OpenAIEmbeddings()\n", + "\n", + "# Docs to index\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "# Load\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "# Split\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=500, chunk_overlap=0\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorstore\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=embd,\n", + ")\n", + "retriever = vectorstore.as_retriever()" ] }, { @@ -122,7 +166,47 @@ } ], "source": [ - "### Router\n\nfrom typing import Literal\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass RouteQuery(BaseModel):\n \"\"\"Route a user query to the most relevant datasource.\"\"\"\n\n datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n ...,\n description=\"Given a user question choose to route it to web search or a vectorstore.\",\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_router = llm.with_structured_output(RouteQuery)\n\n# Prompt\nsystem = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nprint(\n question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n )\n)\nprint(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))" + "### Router\n", + "\n", + "from typing import Literal\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "# Data model\n", + "class RouteQuery(BaseModel):\n", + " \"\"\"Route a user query to the most relevant datasource.\"\"\"\n", + "\n", + " datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n", + " ...,\n", + " description=\"Given a user question choose to route it to web search or a vectorstore.\",\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_router = llm.with_structured_output(RouteQuery)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n", + "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n", + "Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n", + "route_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"{question}\"),\n", + " ]\n", + ")\n", + "\n", + "question_router = route_prompt | structured_llm_router\n", + "print(\n", + " question_router.invoke(\n", + " {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n", + " )\n", + ")\n", + "print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))" ] }, { @@ -140,7 +224,39 @@ } ], "source": [ - "### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" + "### Retrieval Grader\n", + "\n", + "\n", + "# Data model\n", + "class GradeDocuments(BaseModel):\n", + " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", + " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", + "grade_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", + " ]\n", + ")\n", + "\n", + "retrieval_grader = grade_prompt | structured_llm_grader\n", + "question = \"agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" ] }, { @@ -158,7 +274,29 @@ } ], "source": [ - "### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)" + "### Generate\n", + "\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "\n", + "\n", + "# Post-processing\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + "print(generation)" ] }, { @@ -179,7 +317,34 @@ } ], "source": [ - "### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" + "### Hallucination Grader\n", + "\n", + "\n", + "# Data model\n", + "class GradeHallucinations(BaseModel):\n", + " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n", + " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n", + "hallucination_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n", + " ]\n", + ")\n", + "\n", + "hallucination_grader = hallucination_prompt | structured_llm_grader\n", + "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" ] }, { @@ -200,7 +365,34 @@ } ], "source": [ - "### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})" + "### Answer Grader\n", + "\n", + "\n", + "# Data model\n", + "class GradeAnswer(BaseModel):\n", + " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Answer addresses the question, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n", + " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n", + "answer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n", + " ]\n", + ")\n", + "\n", + "answer_grader = answer_prompt | structured_llm_grader\n", + "answer_grader.invoke({\"question\": question, \"generation\": generation})" ] }, { @@ -221,7 +413,26 @@ } ], "source": [ - "### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})" + "### Question Re-writer\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", + " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", + "re_write_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\n", + " \"human\",\n", + " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", + "question_rewriter.invoke({\"question\": question})" ] }, { @@ -239,7 +450,11 @@ "metadata": {}, "outputs": [], "source": [ - "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)" + "### Search\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "web_search_tool = TavilySearchResults(k=3)" ] }, { @@ -261,7 +476,24 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]" + "from typing import List\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " documents: List[str]" ] }, { @@ -279,7 +511,209 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n if source.datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source.datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\"" + "from langchain.schema import Document\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " print(\"---RETRIEVE---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Retrieval\n", + " documents = retriever.invoke(question)\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # RAG generation\n", + " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", + " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Score each doc\n", + " filtered_docs = []\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"document\": d.page_content}\n", + " )\n", + " grade = score.binary_score\n", + " if grade == \"yes\":\n", + " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", + " filtered_docs.append(d)\n", + " else:\n", + " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", + " continue\n", + " return {\"documents\": filtered_docs, \"question\": question}\n", + "\n", + "\n", + "def transform_query(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates question key with a re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Re-write question\n", + " better_question = question_rewriter.invoke({\"question\": question})\n", + " return {\"documents\": documents, \"question\": better_question}\n", + "\n", + "\n", + "def web_search(state):\n", + " \"\"\"\n", + " Web search based on the re-phrased question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with appended web results\n", + " \"\"\"\n", + "\n", + " print(\"---WEB SEARCH---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Web search\n", + " docs = web_search_tool.invoke({\"query\": question})\n", + " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", + " web_results = Document(page_content=web_results)\n", + "\n", + " return {\"documents\": web_results, \"question\": question}\n", + "\n", + "\n", + "### Edges ###\n", + "\n", + "\n", + "def route_question(state):\n", + " \"\"\"\n", + " Route question to web search or RAG.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ROUTE QUESTION---\")\n", + " question = state[\"question\"]\n", + " source = question_router.invoke({\"question\": question})\n", + " if source.datasource == \"web_search\":\n", + " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", + " return \"web_search\"\n", + " elif source.datasource == \"vectorstore\":\n", + " print(\"---ROUTE QUESTION TO RAG---\")\n", + " return \"vectorstore\"\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ASSESS GRADED DOCUMENTS---\")\n", + " state[\"question\"]\n", + " filtered_documents = state[\"documents\"]\n", + "\n", + " if not filtered_documents:\n", + " # All documents have been filtered check_relevance\n", + " # We will re-generate a new query\n", + " print(\n", + " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", + " )\n", + " return \"transform_query\"\n", + " else:\n", + " # We have relevant documents, so generate answer\n", + " print(\"---DECISION: GENERATE---\")\n", + " return \"generate\"\n", + "\n", + "\n", + "def grade_generation_v_documents_and_question(state):\n", + " \"\"\"\n", + " Determines whether the generation is grounded in the document and answers question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK HALLUCINATIONS---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " generation = state[\"generation\"]\n", + "\n", + " score = hallucination_grader.invoke(\n", + " {\"documents\": documents, \"generation\": generation}\n", + " )\n", + " grade = score.binary_score\n", + "\n", + " # Check hallucination\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", + " # Check question-answering\n", + " print(\"---GRADE GENERATION vs QUESTION---\")\n", + " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", + " grade = score.binary_score\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", + " return \"useful\"\n", + " else:\n", + " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", + " return \"not useful\"\n", + " else:\n", + " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", + " return \"not supported\"" ] }, { @@ -373,7 +807,22 @@ } ], "source": [ - "from pprint import pprint\n\n# Run\ninputs = {\n \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])" + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\n", + " \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n", + "}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" ] }, { @@ -426,7 +875,18 @@ } ], "source": [ - "# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])" + "# Run\n", + "inputs = {\"question\": \"What are the types of agent memory?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" ] }, { @@ -438,16 +898,6 @@ "\n", "https://smith.langchain.com/public/fdf0a180-6d15-4d09-bb92-f84f2105ca51/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19ac1f6f-2d84-488f-8a0e-7ee2a46b0f71", - "metadata": {}, - "outputs": [], - "source": [ - "" - ] } ], "metadata": { diff --git a/examples/rag/langgraph_adaptive_rag_local.ipynb b/examples/rag/langgraph_adaptive_rag_local.ipynb index 500efc649..f666536db 100644 --- a/examples/rag/langgraph_adaptive_rag_local.ipynb +++ b/examples/rag/langgraph_adaptive_rag_local.ipynb @@ -45,7 +45,8 @@ "metadata": {}, "outputs": [], "source": [ - "%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]" + "%capture --no-stderr\n", + "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]" ] }, { @@ -79,7 +80,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Ollama model name\nlocal_llm = \"mistral\"" + "# Ollama model name\n", + "local_llm = \"mistral\"" ] }, { @@ -99,7 +101,11 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\"" + "import os\n", + "\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" ] }, { @@ -117,7 +123,32 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()" + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_nomic.embeddings import NomicEmbeddings\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=250, chunk_overlap=0\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n", + ")\n", + "retriever = vectorstore.as_retriever()" ] }, { @@ -145,7 +176,30 @@ } ], "source": [ - "### Router\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n You do not need to be stringent with the keywords in the question related to these topics. \\n\n Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n Question to route: {question}\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))" + "### Router\n", + "\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import JsonOutputParser\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n", + " Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n", + " You do not need to be stringent with the keywords in the question related to these topics. \\n\n", + " Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n", + " Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n", + " Question to route: {question}\"\"\",\n", + " input_variables=[\"question\"],\n", + ")\n", + "\n", + "question_router = prompt | llm | JsonOutputParser()\n", + "question = \"llm agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(question_router.invoke({\"question\": question}))" ] }, { @@ -163,7 +217,31 @@ } ], "source": [ - "### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" + "### Retrieval Grader\n", + "\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import JsonOutputParser\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " Here is the retrieved document: \\n\\n {document} \\n\\n\n", + " Here is the user question: {question} \\n\n", + " If the document contains keywords related to the user question, grade it as relevant. \\n\n", + " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n", + " input_variables=[\"question\", \"document\"],\n", + ")\n", + "\n", + "retrieval_grader = prompt | llm | JsonOutputParser()\n", + "question = \"agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" ] }, { @@ -181,7 +259,31 @@ } ], "source": [ - "### Generate\n\nfrom langchain import hub\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)" + "### Generate\n", + "\n", + "from langchain import hub\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, temperature=0)\n", + "\n", + "\n", + "# Post-processing\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "question = \"agent memory\"\n", + "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + "print(generation)" ] }, { @@ -202,7 +304,26 @@ } ], "source": [ - "### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" + "### Hallucination Grader\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n", + " Here are the facts:\n", + " \\n ------- \\n\n", + " {documents} \n", + " \\n ------- \\n\n", + " Here is the answer: {generation}\n", + " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", + " input_variables=[\"generation\", \"documents\"],\n", + ")\n", + "\n", + "hallucination_grader = prompt | llm | JsonOutputParser()\n", + "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" ] }, { @@ -223,7 +344,26 @@ } ], "source": [ - "### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})" + "### Answer Grader\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n", + " Here is the answer:\n", + " \\n ------- \\n\n", + " {generation} \n", + " \\n ------- \\n\n", + " Here is the question: {question}\n", + " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", + " input_variables=[\"generation\", \"question\"],\n", + ")\n", + "\n", + "answer_grader = prompt | llm | JsonOutputParser()\n", + "answer_grader.invoke({\"question\": question, \"generation\": generation})" ] }, { @@ -244,7 +384,21 @@ } ], "source": [ - "### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})" + "### Question Re-writer\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, temperature=0)\n", + "\n", + "# Prompt\n", + "re_write_prompt = PromptTemplate(\n", + " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", + " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n", + " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n", + " input_variables=[\"generation\", \"question\"],\n", + ")\n", + "\n", + "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", + "question_rewriter.invoke({\"question\": question})" ] }, { @@ -262,7 +416,11 @@ "metadata": {}, "outputs": [], "source": [ - "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)" + "### Search\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "web_search_tool = TavilySearchResults(k=3)" ] }, { @@ -284,7 +442,24 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]" + "from typing import List\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " documents: List[str]" ] }, { @@ -294,7 +469,214 @@ "metadata": {}, "outputs": [], "source": [ - "### Nodes\n\nfrom langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\"" + "### Nodes\n", + "\n", + "from langchain.schema import Document\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " print(\"---RETRIEVE---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Retrieval\n", + " documents = retriever.get_relevant_documents(question)\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # RAG generation\n", + " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", + " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Score each doc\n", + " filtered_docs = []\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"document\": d.page_content}\n", + " )\n", + " grade = score[\"score\"]\n", + " if grade == \"yes\":\n", + " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", + " filtered_docs.append(d)\n", + " else:\n", + " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", + " continue\n", + " return {\"documents\": filtered_docs, \"question\": question}\n", + "\n", + "\n", + "def transform_query(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates question key with a re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Re-write question\n", + " better_question = question_rewriter.invoke({\"question\": question})\n", + " return {\"documents\": documents, \"question\": better_question}\n", + "\n", + "\n", + "def web_search(state):\n", + " \"\"\"\n", + " Web search based on the re-phrased question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with appended web results\n", + " \"\"\"\n", + "\n", + " print(\"---WEB SEARCH---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Web search\n", + " docs = web_search_tool.invoke({\"query\": question})\n", + " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", + " web_results = Document(page_content=web_results)\n", + "\n", + " return {\"documents\": web_results, \"question\": question}\n", + "\n", + "\n", + "### Edges ###\n", + "\n", + "\n", + "def route_question(state):\n", + " \"\"\"\n", + " Route question to web search or RAG.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ROUTE QUESTION---\")\n", + " question = state[\"question\"]\n", + " print(question)\n", + " source = question_router.invoke({\"question\": question})\n", + " print(source)\n", + " print(source[\"datasource\"])\n", + " if source[\"datasource\"] == \"web_search\":\n", + " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", + " return \"web_search\"\n", + " elif source[\"datasource\"] == \"vectorstore\":\n", + " print(\"---ROUTE QUESTION TO RAG---\")\n", + " return \"vectorstore\"\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ASSESS GRADED DOCUMENTS---\")\n", + " state[\"question\"]\n", + " filtered_documents = state[\"documents\"]\n", + "\n", + " if not filtered_documents:\n", + " # All documents have been filtered check_relevance\n", + " # We will re-generate a new query\n", + " print(\n", + " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", + " )\n", + " return \"transform_query\"\n", + " else:\n", + " # We have relevant documents, so generate answer\n", + " print(\"---DECISION: GENERATE---\")\n", + " return \"generate\"\n", + "\n", + "\n", + "def grade_generation_v_documents_and_question(state):\n", + " \"\"\"\n", + " Determines whether the generation is grounded in the document and answers question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK HALLUCINATIONS---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " generation = state[\"generation\"]\n", + "\n", + " score = hallucination_grader.invoke(\n", + " {\"documents\": documents, \"generation\": generation}\n", + " )\n", + " grade = score[\"score\"]\n", + "\n", + " # Check hallucination\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", + " # Check question-answering\n", + " print(\"---GRADE GENERATION vs QUESTION---\")\n", + " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", + " grade = score[\"score\"]\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", + " return \"useful\"\n", + " else:\n", + " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", + " return \"not useful\"\n", + " else:\n", + " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", + " return \"not supported\"" ] }, { @@ -394,7 +776,20 @@ } ], "source": [ - "from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What is the AlphaCodium paper about?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])" + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\"question\": \"What is the AlphaCodium paper about?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" ] }, { @@ -406,16 +801,6 @@ "\n", "https://smith.langchain.com/public/81813813-be53-403c-9877-afcd5786ca2e/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4620ede9-b014-499f-8acf-80f80ce0d944", - "metadata": {}, - "outputs": [], - "source": [ - "" - ] } ], "metadata": { diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index 486c6caed..ca9f1f029 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -20,7 +20,10 @@ "id": "969fb438", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters" + ] }, { "cell_type": "code", @@ -28,7 +31,22 @@ "id": "e4958a8c", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(key: str):\n", + " if key not in os.environ:\n", + " os.environ[key] = getpass.getpass(f\"{key}:\")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")\n", + "\n", + "# (Optional) For tracing\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -46,7 +64,34 @@ "id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6", "metadata": {}, "outputs": [], - "source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] + "source": [ + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_openai import OpenAIEmbeddings\n", + "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=100, chunk_overlap=50\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=OpenAIEmbeddings(),\n", + ")\n", + "retriever = vectorstore.as_retriever()" + ] }, { "cell_type": "markdown", @@ -62,7 +107,17 @@ "id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048", "metadata": {}, "outputs": [], - "source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"] + "source": [ + "from langchain.tools.retriever import create_retriever_tool\n", + "\n", + "retriever_tool = create_retriever_tool(\n", + " retriever,\n", + " \"retrieve_blog_posts\",\n", + " \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n", + ")\n", + "\n", + "tools = [retriever_tool]" + ] }, { "cell_type": "markdown", @@ -71,7 +126,7 @@ "source": [ "## Agent state\n", " \n", - "We will defined a graph.\n", + "We will define a graph.\n", "\n", "A `state` object that it passes around to each node.\n", "\n", @@ -86,7 +141,19 @@ "id": "0e378706-47d5-425a-8ba0-57b9acffbd0c", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"] + "source": [ + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The add_messages function defines how an update should be processed\n", + " # Default is to replace. add_messages says \"append\"\n", + " messages: Annotated[Sequence[BaseMessage], add_messages]" + ] }, { "attachments": { @@ -129,7 +196,173 @@ ] } ], - "source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"] + "source": [ + "from typing import Annotated, Literal, Sequence, TypedDict\n", + "\n", + "from langchain import hub\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.prebuilt import tools_condition\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " str: A decision for whether the documents are relevant or not\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK RELEVANCE---\")\n", + "\n", + " # Data model\n", + " class grade(BaseModel):\n", + " \"\"\"Binary score for relevance check.\"\"\"\n", + "\n", + " binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n", + "\n", + " # LLM\n", + " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", + "\n", + " # LLM with tool and validation\n", + " llm_with_tool = model.with_structured_output(grade)\n", + "\n", + " # Prompt\n", + " prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " Here is the retrieved document: \\n\\n {context} \\n\\n\n", + " Here is the user question: {question} \\n\n", + " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n", + " input_variables=[\"context\", \"question\"],\n", + " )\n", + "\n", + " # Chain\n", + " chain = prompt | llm_with_tool\n", + "\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + "\n", + " question = messages[0].content\n", + " docs = last_message.content\n", + "\n", + " scored_result = chain.invoke({\"question\": question, \"context\": docs})\n", + "\n", + " score = scored_result.binary_score\n", + "\n", + " if score == \"yes\":\n", + " print(\"---DECISION: DOCS RELEVANT---\")\n", + " return \"generate\"\n", + "\n", + " else:\n", + " print(\"---DECISION: DOCS NOT RELEVANT---\")\n", + " print(score)\n", + " return \"rewrite\"\n", + "\n", + "\n", + "### Nodes\n", + "\n", + "\n", + "def agent(state):\n", + " \"\"\"\n", + " Invokes the agent model to generate a response based on the current state. Given\n", + " the question, it will decide to retrieve using the retriever tool, or simply end.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with the agent response appended to messages\n", + " \"\"\"\n", + " print(\"---CALL AGENT---\")\n", + " messages = state[\"messages\"]\n", + " model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n", + " model = model.bind_tools(tools)\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def rewrite(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " messages = state[\"messages\"]\n", + " question = messages[0].content\n", + "\n", + " msg = [\n", + " HumanMessage(\n", + " content=f\"\"\" \\n \n", + " Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n", + " Here is the initial question:\n", + " \\n ------- \\n\n", + " {question} \n", + " \\n ------- \\n\n", + " Formulate an improved question: \"\"\",\n", + " )\n", + " ]\n", + "\n", + " # Grader\n", + " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", + " response = model.invoke(msg)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with re-phrased question\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " messages = state[\"messages\"]\n", + " question = messages[0].content\n", + " last_message = messages[-1]\n", + "\n", + " docs = last_message.content\n", + "\n", + " # Prompt\n", + " prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + " # LLM\n", + " llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n", + "\n", + " # Post-processing\n", + " def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + " # Chain\n", + " rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + " # Run\n", + " response = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n", + "prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like" + ] }, { "cell_type": "markdown", @@ -150,7 +383,48 @@ "id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the nodes we will cycle between\n", + "workflow.add_node(\"agent\", agent) # agent\n", + "retrieve = ToolNode([retriever_tool])\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieval\n", + "workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n", + "workflow.add_node(\n", + " \"generate\", generate\n", + ") # Generating a response after we know the documents are relevant\n", + "# Call agent node to decide to retrieve or not\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# Decide whether to retrieve\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " # Assess agent decision\n", + " tools_condition,\n", + " {\n", + " # Translate the condition outputs to nodes in our graph\n", + " \"tools\": \"retrieve\",\n", + " END: END,\n", + " },\n", + ")\n", + "\n", + "# Edges taken after the `action` node is called.\n", + "workflow.add_conditional_edges(\n", + " \"retrieve\",\n", + " # Assess agent decision\n", + " grade_documents,\n", + ")\n", + "workflow.add_edge(\"generate\", END)\n", + "workflow.add_edge(\"rewrite\", \"agent\")\n", + "\n", + "# Compile\n", + "graph = workflow.compile()" + ] }, { "cell_type": "code", @@ -169,7 +443,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", @@ -203,15 +485,21 @@ ] } ], - "source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n pprint.pprint(value, indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "189333cc-5d34-4869-9f9b-741210e1096f", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "import pprint\n", + "\n", + "inputs = {\n", + " \"messages\": [\n", + " (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n", + " ]\n", + "}\n", + "for output in graph.stream(inputs):\n", + " for key, value in output.items():\n", + " pprint.pprint(f\"Output from node '{key}':\")\n", + " pprint.pprint(\"---\")\n", + " pprint.pprint(value, indent=2, width=80, depth=None)\n", + " pprint.pprint(\"\\n---\\n\")" + ] } ], "metadata": { diff --git a/examples/rag/langgraph_crag.ipynb b/examples/rag/langgraph_crag.ipynb index f131e398e..2e48143de 100644 --- a/examples/rag/langgraph_crag.ipynb +++ b/examples/rag/langgraph_crag.ipynb @@ -47,7 +47,9 @@ "id": "568c84d6-9df6-4b7b-b50d-476c0a64a04b", "metadata": {}, "outputs": [], - "source": ["! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python"] + "source": [ + "! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python" + ] }, { "cell_type": "markdown", @@ -63,7 +65,11 @@ "id": "74710419-158d-4270-931c-de83db7b580d", "metadata": {}, "outputs": [], - "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\""] + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -81,7 +87,9 @@ "id": "c3ac6e65-2d4e-48dd-9fff-40047373332d", "metadata": {}, "outputs": [], - "source": ["os.environ[\"TAVILY_API_KEY\"] = \"\""] + "source": [ + "os.environ[\"TAVILY_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -99,7 +107,11 @@ "id": "e205f57e-5218-478b-ad8e-1723bdb0d45e", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -117,7 +129,34 @@ "id": "3a566a30-cf0e-4330-ad4d-9bf994bdfa86", "metadata": {}, "outputs": [], - "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] + "source": [ + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=250, chunk_overlap=0\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=OpenAIEmbeddings(),\n", + ")\n", + "retriever = vectorstore.as_retriever()" + ] }, { "cell_type": "markdown", @@ -141,7 +180,44 @@ ] } ], - "source": ["### Retrieval Grader\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] + "source": [ + "### Retrieval Grader\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "# Data model\n", + "class GradeDocuments(BaseModel):\n", + " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", + "grade_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", + " ]\n", + ")\n", + "\n", + "retrieval_grader = grade_prompt | structured_llm_grader\n", + "question = \"agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" + ] }, { "cell_type": "code", @@ -157,7 +233,31 @@ ] } ], - "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] + "source": [ + "### Generate\n", + "\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "\n", + "\n", + "# Post-processing\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + "print(generation)" + ] }, { "cell_type": "code", @@ -176,7 +276,28 @@ "output_type": "execute_result" } ], - "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] + "source": [ + "### Question Re-writer\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", + " for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", + "re_write_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\n", + " \"human\",\n", + " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", + "question_rewriter.invoke({\"question\": question})" + ] }, { "cell_type": "markdown", @@ -192,7 +313,13 @@ "id": "46d51b53-54a9-4e0a-9f14-e39998f5b340", "metadata": {}, "outputs": [], - "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] + "source": [ + "### Search\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "web_search_tool = TavilySearchResults(k=3)" + ] }, { "cell_type": "markdown", @@ -212,7 +339,28 @@ "id": "94b3945f-ef0f-458d-a443-f763903550b0", "metadata": {}, "outputs": [], - "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]"] + "source": [ + "from typing import List\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " web_search: whether to add search\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " web_search: str\n", + " documents: List[str]" + ] }, { "cell_type": "code", @@ -220,7 +368,155 @@ "id": "efd639c5-82e2-45e6-a94a-6a4039646ef5", "metadata": {}, "outputs": [], - "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n documents.append(web_results)\n\n return {\"documents\": documents, \"question\": question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\""] + "source": [ + "from langchain.schema import Document\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " print(\"---RETRIEVE---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Retrieval\n", + " documents = retriever.get_relevant_documents(question)\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # RAG generation\n", + " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", + " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Score each doc\n", + " filtered_docs = []\n", + " web_search = \"No\"\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"document\": d.page_content}\n", + " )\n", + " grade = score.binary_score\n", + " if grade == \"yes\":\n", + " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", + " filtered_docs.append(d)\n", + " else:\n", + " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", + " web_search = \"Yes\"\n", + " continue\n", + " return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n", + "\n", + "\n", + "def transform_query(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates question key with a re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Re-write question\n", + " better_question = question_rewriter.invoke({\"question\": question})\n", + " return {\"documents\": documents, \"question\": better_question}\n", + "\n", + "\n", + "def web_search(state):\n", + " \"\"\"\n", + " Web search based on the re-phrased question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with appended web results\n", + " \"\"\"\n", + "\n", + " print(\"---WEB SEARCH---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Web search\n", + " docs = web_search_tool.invoke({\"query\": question})\n", + " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", + " web_results = Document(page_content=web_results)\n", + " documents.append(web_results)\n", + "\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ASSESS GRADED DOCUMENTS---\")\n", + " state[\"question\"]\n", + " web_search = state[\"web_search\"]\n", + " state[\"documents\"]\n", + "\n", + " if web_search == \"Yes\":\n", + " # All documents have been filtered check_relevance\n", + " # We will re-generate a new query\n", + " print(\n", + " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", + " )\n", + " return \"transform_query\"\n", + " else:\n", + " # We have relevant documents, so generate answer\n", + " print(\"---DECISION: GENERATE---\")\n", + " return \"generate\"" + ] }, { "cell_type": "markdown", @@ -238,7 +534,36 @@ "id": "dedae17a-98c6-474d-90a7-9234b7c8cea0", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\nworkflow.add_node(\"web_search_node\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"web_search_node\")\nworkflow.add_edge(\"web_search_node\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\n# Compile\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "workflow = StateGraph(GraphState)\n", + "\n", + "# Define the nodes\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", + "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", + "workflow.add_node(\"generate\", generate) # generatae\n", + "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", + "workflow.add_node(\"web_search_node\", web_search) # web search\n", + "\n", + "# Build graph\n", + "workflow.add_edge(START, \"retrieve\")\n", + "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", + "workflow.add_conditional_edges(\n", + " \"grade_documents\",\n", + " decide_to_generate,\n", + " {\n", + " \"transform_query\": \"transform_query\",\n", + " \"generate\": \"generate\",\n", + " },\n", + ")\n", + "workflow.add_edge(\"transform_query\", \"web_search_node\")\n", + "workflow.add_edge(\"web_search_node\", \"generate\")\n", + "workflow.add_edge(\"generate\", END)\n", + "\n", + "# Compile\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", @@ -281,7 +606,22 @@ ] } ], - "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] + "source": [ + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\"question\": \"What are the types of agent memory?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" + ] }, { "cell_type": "code", @@ -326,7 +666,22 @@ ] } ], - "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"How does the AlphaCodium paper work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] + "source": [ + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\"question\": \"How does the AlphaCodium paper work?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" + ] }, { "cell_type": "markdown", @@ -339,14 +694,6 @@ "\n", "* https://smith.langchain.com/public/497c8ed9-d9e2-429e-8ada-e64de3ec26c9/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ce65be5-fd12-4ffc-984c-34c132693e69", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_crag_local.ipynb b/examples/rag/langgraph_crag_local.ipynb index dbac6f697..b43a89929 100644 --- a/examples/rag/langgraph_crag_local.ipynb +++ b/examples/rag/langgraph_crag_local.ipynb @@ -57,7 +57,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai" + "%%capture --no-stderr\n", + "%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai" ] }, { @@ -80,7 +81,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Embedding (optional)\nos.environ[\"OPENAI_API_KEY\"] = \"xxx\"" + "# Embedding (optional)\n", + "os.environ[\"OPENAI_API_KEY\"] = \"xxx\"" ] }, { @@ -90,7 +92,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Tracing and testing (optional)\nos.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\"" + "# Tracing and testing (optional)\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\"" ] }, { @@ -110,7 +116,9 @@ "metadata": {}, "outputs": [], "source": [ - "local_llm = \"llama3\"\nmodel_tested = \"llama3-8b\"\nmetadata = f\"CRAG, {model_tested}\"" + "local_llm = \"llama3\"\n", + "model_tested = \"llama3-8b\"\n", + "metadata = f\"CRAG, {model_tested}\"" ] }, { @@ -204,7 +212,45 @@ } ], "source": [ - "### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_mistralai.chat_models import ChatMistralAI\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a teacher grading a quiz. You will be given: \n 1/ a QUESTION\n 2/ A FACT provided by the student\n \n You are grading RELEVANCE RECALL:\n A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n 1 is the highest (best) score. 0 is the lowest score you can give. \n \n Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n \n Avoid simply stating the correct answer at the outset.\n \n Question: {question} \\n\n Fact: \\n\\n {documents} \\n\\n\n \n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))" + "### Retrieval Grader\n", + "\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import JsonOutputParser\n", + "from langchain_mistralai.chat_models import ChatMistralAI\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a teacher grading a quiz. You will be given: \n", + " 1/ a QUESTION\n", + " 2/ A FACT provided by the student\n", + " \n", + " You are grading RELEVANCE RECALL:\n", + " A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n", + " A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n", + " 1 is the highest (best) score. 0 is the lowest score you can give. \n", + " \n", + " Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n", + " \n", + " Avoid simply stating the correct answer at the outset.\n", + " \n", + " Question: {question} \\n\n", + " Fact: \\n\\n {documents} \\n\\n\n", + " \n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n", + " \"\"\",\n", + " input_variables=[\"question\", \"documents\"],\n", + ")\n", + "\n", + "retrieval_grader = prompt | llm | JsonOutputParser()\n", + "question = \"agent memory\"\n", + "docs = retriever.invoke(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))" ] }, { @@ -222,7 +268,35 @@ } ], "source": [ - "### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are an assistant for question-answering tasks. \n \n Use the following documents to answer the question. \n \n If you don't know the answer, just say that you don't know. \n \n Use three sentences maximum and keep the answer concise:\n Question: {question} \n Documents: {documents} \n Answer: \n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)" + "### Generate\n", + "\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are an assistant for question-answering tasks. \n", + " \n", + " Use the following documents to answer the question. \n", + " \n", + " If you don't know the answer, just say that you don't know. \n", + " \n", + " Use three sentences maximum and keep the answer concise:\n", + " Question: {question} \n", + " Documents: {documents} \n", + " Answer: \n", + " \"\"\",\n", + " input_variables=[\"question\", \"documents\"],\n", + ")\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, temperature=0)\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n", + "print(generation)" ] }, { @@ -232,7 +306,11 @@ "metadata": {}, "outputs": [], "source": [ - "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)" + "### Search\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "web_search_tool = TavilySearchResults(k=3)" ] }, { @@ -263,7 +341,175 @@ } ], "source": [ - "from typing import List\nfrom typing_extensions import TypedDict\nfrom IPython.display import Image, display\nfrom langchain.schema import Document\nfrom langgraph.graph import START, END, StateGraph\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n search: str\n documents: List[str]\n steps: List[str]\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n question = state[\"question\"]\n documents = retriever.invoke(question)\n steps = state[\"steps\"]\n steps.append(\"retrieve_documents\")\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n steps = state[\"steps\"]\n steps.append(\"generate_answer\")\n return {\n \"documents\": documents,\n \"question\": question,\n \"generation\": generation,\n \"steps\": steps,\n }\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n steps = state[\"steps\"]\n steps.append(\"grade_document_retrieval\")\n filtered_docs = []\n search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"documents\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n filtered_docs.append(d)\n else:\n search = \"Yes\"\n continue\n return {\n \"documents\": filtered_docs,\n \"question\": question,\n \"search\": search,\n \"steps\": steps,\n }\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n question = state[\"question\"]\n documents = state.get(\"documents\", [])\n steps = state[\"steps\"]\n steps.append(\"web_search\")\n web_results = web_search_tool.invoke({\"query\": question})\n documents.extend(\n [\n Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n for d in web_results\n ]\n )\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n search = state[\"search\"]\n if search == \"Yes\":\n return \"search\"\n else:\n return \"generate\"\n\n\n# Graph\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"web_search\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\ncustom_graph = workflow.compile()\n\ndisplay(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))" + "from typing import List\n", + "from typing_extensions import TypedDict\n", + "from IPython.display import Image, display\n", + "from langchain.schema import Document\n", + "from langgraph.graph import START, END, StateGraph\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " search: whether to add search\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " search: str\n", + " documents: List[str]\n", + " steps: List[str]\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " question = state[\"question\"]\n", + " documents = retriever.invoke(question)\n", + " steps = state[\"steps\"]\n", + " steps.append(\"retrieve_documents\")\n", + " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + "\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n", + " steps = state[\"steps\"]\n", + " steps.append(\"generate_answer\")\n", + " return {\n", + " \"documents\": documents,\n", + " \"question\": question,\n", + " \"generation\": generation,\n", + " \"steps\": steps,\n", + " }\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " steps = state[\"steps\"]\n", + " steps.append(\"grade_document_retrieval\")\n", + " filtered_docs = []\n", + " search = \"No\"\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"documents\": d.page_content}\n", + " )\n", + " grade = score[\"score\"]\n", + " if grade == \"yes\":\n", + " filtered_docs.append(d)\n", + " else:\n", + " search = \"Yes\"\n", + " continue\n", + " return {\n", + " \"documents\": filtered_docs,\n", + " \"question\": question,\n", + " \"search\": search,\n", + " \"steps\": steps,\n", + " }\n", + "\n", + "\n", + "def web_search(state):\n", + " \"\"\"\n", + " Web search based on the re-phrased question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with appended web results\n", + " \"\"\"\n", + "\n", + " question = state[\"question\"]\n", + " documents = state.get(\"documents\", [])\n", + " steps = state[\"steps\"]\n", + " steps.append(\"web_search\")\n", + " web_results = web_search_tool.invoke({\"query\": question})\n", + " documents.extend(\n", + " [\n", + " Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n", + " for d in web_results\n", + " ]\n", + " )\n", + " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + " search = state[\"search\"]\n", + " if search == \"Yes\":\n", + " return \"search\"\n", + " else:\n", + " return \"generate\"\n", + "\n", + "\n", + "# Graph\n", + "workflow = StateGraph(GraphState)\n", + "\n", + "# Define the nodes\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", + "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", + "workflow.add_node(\"generate\", generate) # generatae\n", + "workflow.add_node(\"web_search\", web_search) # web search\n", + "\n", + "# Build graph\n", + "workflow.add_edge(START, \"retrieve\")\n", + "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", + "workflow.add_conditional_edges(\n", + " \"grade_documents\",\n", + " decide_to_generate,\n", + " {\n", + " \"search\": \"web_search\",\n", + " \"generate\": \"generate\",\n", + " },\n", + ")\n", + "workflow.add_edge(\"web_search\", \"generate\")\n", + "workflow.add_edge(\"generate\", END)\n", + "\n", + "custom_graph = workflow.compile()\n", + "\n", + "display(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))" ] }, { @@ -339,7 +585,39 @@ "metadata": {}, "outputs": [], "source": [ - "from langsmith import Client\n\nclient = Client()\n\n# Create a dataset\nexamples = [\n (\n \"How does the ReAct agent use self-reflection? \",\n \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n ),\n (\n \"What are the types of biases that can arise with few-shot prompting?\",\n \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n ),\n (\n \"What are five types of adversarial attacks?\",\n \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n ),\n (\n \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n ),\n (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n]\n\n# Save it\ndataset_name = \"Corrective RAG Agent Testing\"\nif not client.has_dataset(dataset_name=dataset_name):\n dataset = client.create_dataset(dataset_name=dataset_name)\n inputs, outputs = zip(\n *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n )\n client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)" + "from langsmith import Client\n", + "\n", + "client = Client()\n", + "\n", + "# Create a dataset\n", + "examples = [\n", + " (\n", + " \"How does the ReAct agent use self-reflection? \",\n", + " \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n", + " ),\n", + " (\n", + " \"What are the types of biases that can arise with few-shot prompting?\",\n", + " \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n", + " ),\n", + " (\n", + " \"What are five types of adversarial attacks?\",\n", + " \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n", + " ),\n", + " (\n", + " \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n", + " \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n", + " ),\n", + " (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n", + "]\n", + "\n", + "# Save it\n", + "dataset_name = \"Corrective RAG Agent Testing\"\n", + "if not client.has_dataset(dataset_name=dataset_name):\n", + " dataset = client.create_dataset(dataset_name=dataset_name)\n", + " inputs, outputs = zip(\n", + " *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n", + " )\n", + " client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)" ] }, { @@ -429,6 +707,14 @@ "]\n", "\n", "\n", + "def find_tool_calls_react(messages):\n", + " \"\"\"\n", + " Find all tool calls in the messages returned\n", + " \"\"\"\n", + " tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n", + " return tool_calls\n", + "\n", + "\n", "def check_trajectory_react(root_run: Run, example: Example) -> dict:\n", " \"\"\"\n", " Check if all expected tools are called in exact order and without any additional tool calls.\n", @@ -543,16 +829,6 @@ "\n", "However, the answer accuracy performance lags the larger models with `custom agent` implementations." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "79295798-0181-417e-abad-11dddb6ff05e", - "metadata": {}, - "outputs": [], - "source": [ - "" - ] } ], "metadata": { diff --git a/examples/rag/langgraph_self_rag.ipynb b/examples/rag/langgraph_self_rag.ipynb index cb2d5d934..496237443 100644 --- a/examples/rag/langgraph_self_rag.ipynb +++ b/examples/rag/langgraph_self_rag.ipynb @@ -59,7 +59,9 @@ "id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9", "metadata": {}, "outputs": [], - "source": ["! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph"] + "source": [ + "! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph" + ] }, { "cell_type": "markdown", @@ -75,7 +77,11 @@ "id": "f18b63c7-d0d3-41c1-ae6b-5a0f1b8ccf0f", "metadata": {}, "outputs": [], - "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\""] + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -93,7 +99,11 @@ "id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -111,7 +121,34 @@ "id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d", "metadata": {}, "outputs": [], - "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] + "source": [ + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=250, chunk_overlap=0\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=OpenAIEmbeddings(),\n", + ")\n", + "retriever = vectorstore.as_retriever()" + ] }, { "cell_type": "markdown", @@ -143,7 +180,46 @@ ] } ], - "source": ["### Retrieval Grader\n\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] + "source": [ + "### Retrieval Grader\n", + "\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "# Data model\n", + "class GradeDocuments(BaseModel):\n", + " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", + " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", + "grade_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", + " ]\n", + ")\n", + "\n", + "retrieval_grader = grade_prompt | structured_llm_grader\n", + "question = \"agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" + ] }, { "cell_type": "code", @@ -159,7 +235,31 @@ ] } ], - "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] + "source": [ + "### Generate\n", + "\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "\n", + "\n", + "# Post-processing\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + "print(generation)" + ] }, { "cell_type": "code", @@ -178,7 +278,36 @@ "output_type": "execute_result" } ], - "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] + "source": [ + "### Hallucination Grader\n", + "\n", + "\n", + "# Data model\n", + "class GradeHallucinations(BaseModel):\n", + " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n", + " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n", + "hallucination_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n", + " ]\n", + ")\n", + "\n", + "hallucination_grader = hallucination_prompt | structured_llm_grader\n", + "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" + ] }, { "cell_type": "code", @@ -197,7 +326,36 @@ "output_type": "execute_result" } ], - "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] + "source": [ + "### Answer Grader\n", + "\n", + "\n", + "# Data model\n", + "class GradeAnswer(BaseModel):\n", + " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", + "\n", + " binary_score: str = Field(\n", + " description=\"Answer addresses the question, 'yes' or 'no'\"\n", + " )\n", + "\n", + "\n", + "# LLM with function call\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n", + " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n", + "answer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n", + " ]\n", + ")\n", + "\n", + "answer_grader = answer_prompt | structured_llm_grader\n", + "answer_grader.invoke({\"question\": question, \"generation\": generation})" + ] }, { "cell_type": "code", @@ -216,7 +374,28 @@ "output_type": "execute_result" } ], - "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] + "source": [ + "### Question Re-writer\n", + "\n", + "# LLM\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", + "\n", + "# Prompt\n", + "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", + " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", + "re_write_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system),\n", + " (\n", + " \"human\",\n", + " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", + "question_rewriter.invoke({\"question\": question})" + ] }, { "cell_type": "markdown", @@ -236,7 +415,26 @@ "id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085", "metadata": {}, "outputs": [], - "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] + "source": [ + "from typing import List\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " documents: List[str]" + ] }, { "cell_type": "code", @@ -244,7 +442,167 @@ "id": "add509d8-6682-4127-8d95-13dd37d79702", "metadata": {}, "outputs": [], - "source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] + "source": [ + "### Nodes\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " print(\"---RETRIEVE---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Retrieval\n", + " documents = retriever.get_relevant_documents(question)\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # RAG generation\n", + " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", + " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Score each doc\n", + " filtered_docs = []\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"document\": d.page_content}\n", + " )\n", + " grade = score.binary_score\n", + " if grade == \"yes\":\n", + " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", + " filtered_docs.append(d)\n", + " else:\n", + " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", + " continue\n", + " return {\"documents\": filtered_docs, \"question\": question}\n", + "\n", + "\n", + "def transform_query(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates question key with a re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Re-write question\n", + " better_question = question_rewriter.invoke({\"question\": question})\n", + " return {\"documents\": documents, \"question\": better_question}\n", + "\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ASSESS GRADED DOCUMENTS---\")\n", + " state[\"question\"]\n", + " filtered_documents = state[\"documents\"]\n", + "\n", + " if not filtered_documents:\n", + " # All documents have been filtered check_relevance\n", + " # We will re-generate a new query\n", + " print(\n", + " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", + " )\n", + " return \"transform_query\"\n", + " else:\n", + " # We have relevant documents, so generate answer\n", + " print(\"---DECISION: GENERATE---\")\n", + " return \"generate\"\n", + "\n", + "\n", + "def grade_generation_v_documents_and_question(state):\n", + " \"\"\"\n", + " Determines whether the generation is grounded in the document and answers question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK HALLUCINATIONS---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " generation = state[\"generation\"]\n", + "\n", + " score = hallucination_grader.invoke(\n", + " {\"documents\": documents, \"generation\": generation}\n", + " )\n", + " grade = score.binary_score\n", + "\n", + " # Check hallucination\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", + " # Check question-answering\n", + " print(\"---GRADE GENERATION vs QUESTION---\")\n", + " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", + " grade = score.binary_score\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", + " return \"useful\"\n", + " else:\n", + " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", + " return \"not useful\"\n", + " else:\n", + " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", + " return \"not supported\"" + ] }, { "cell_type": "markdown", @@ -262,7 +620,42 @@ "id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "workflow = StateGraph(GraphState)\n", + "\n", + "# Define the nodes\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", + "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", + "workflow.add_node(\"generate\", generate) # generatae\n", + "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", + "\n", + "# Build graph\n", + "workflow.add_edge(START, \"retrieve\")\n", + "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", + "workflow.add_conditional_edges(\n", + " \"grade_documents\",\n", + " decide_to_generate,\n", + " {\n", + " \"transform_query\": \"transform_query\",\n", + " \"generate\": \"generate\",\n", + " },\n", + ")\n", + "workflow.add_edge(\"transform_query\", \"retrieve\")\n", + "workflow.add_conditional_edges(\n", + " \"generate\",\n", + " grade_generation_v_documents_and_question,\n", + " {\n", + " \"not supported\": \"generate\",\n", + " \"useful\": END,\n", + " \"not useful\": \"transform_query\",\n", + " },\n", + ")\n", + "\n", + "# Compile\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", @@ -301,7 +694,22 @@ ] } ], - "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] + "source": [ + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" + ] }, { "cell_type": "code", @@ -341,7 +749,19 @@ ] } ], - "source": ["inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] + "source": [ + "inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" + ] }, { "cell_type": "markdown", @@ -354,14 +774,6 @@ "\n", "* https://smith.langchain.com/public/1c6bf654-61b2-4fc5-9889-054b020c78aa/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42369ab8-322d-434a-b5dd-2266e4cb2903", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_self_rag_local.ipynb b/examples/rag/langgraph_self_rag_local.ipynb index bc107d0b4..64b576ad4 100644 --- a/examples/rag/langgraph_self_rag_local.ipynb +++ b/examples/rag/langgraph_self_rag_local.ipynb @@ -59,7 +59,10 @@ "id": "d7f9cc6d-a70c-433a-b0ad-ea47c5a0717e", "metadata": {}, "outputs": [], - "source": ["%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]"] + "source": [ + "%capture --no-stderr\n", + "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]" + ] }, { "cell_type": "markdown", @@ -91,7 +94,10 @@ "id": "bedffc73-6b10-42c8-8768-2085c8ed3398", "metadata": {}, "outputs": [], - "source": ["# Ollama model name\nlocal_llm = \"mistral\""] + "source": [ + "# Ollama model name\n", + "local_llm = \"mistral\"" + ] }, { "cell_type": "markdown", @@ -109,7 +115,13 @@ "id": "2208f342-8163-4af3-8dc0-aa70f5e06143", "metadata": {}, "outputs": [], - "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] + "source": [ + "import os\n", + "\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" + ] }, { "cell_type": "markdown", @@ -127,7 +139,34 @@ "id": "c3bb9060-ad74-4470-9991-2ba167b6b8d8", "metadata": {}, "outputs": [], - "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"] + "source": [ + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_nomic.embeddings import NomicEmbeddings\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=250, chunk_overlap=0\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n", + ")\n", + "retriever = vectorstore.as_retriever()" + ] }, { "cell_type": "markdown", @@ -151,7 +190,33 @@ ] } ], - "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] + "source": [ + "### Retrieval Grader\n", + "\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import JsonOutputParser\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " Here is the retrieved document: \\n\\n {document} \\n\\n\n", + " Here is the user question: {question} \\n\n", + " If the document contains keywords related to the user question, grade it as relevant. \\n\n", + " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n", + " input_variables=[\"question\", \"document\"],\n", + ")\n", + "\n", + "retrieval_grader = prompt | llm | JsonOutputParser()\n", + "question = \"agent memory\"\n", + "docs = retriever.get_relevant_documents(question)\n", + "doc_txt = docs[1].page_content\n", + "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" + ] }, { "cell_type": "code", @@ -167,7 +232,31 @@ ] } ], - "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] + "source": [ + "### Generate\n", + "\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "# Prompt\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, temperature=0)\n", + "\n", + "\n", + "# Post-processing\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "# Chain\n", + "rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + "# Run\n", + "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + "print(generation)" + ] }, { "cell_type": "code", @@ -186,7 +275,28 @@ "output_type": "execute_result" } ], - "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] + "source": [ + "### Hallucination Grader\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n", + " Here are the facts:\n", + " \\n ------- \\n\n", + " {documents} \n", + " \\n ------- \\n\n", + " Here is the answer: {generation}\n", + " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", + " input_variables=[\"generation\", \"documents\"],\n", + ")\n", + "\n", + "hallucination_grader = prompt | llm | JsonOutputParser()\n", + "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" + ] }, { "cell_type": "code", @@ -205,7 +315,28 @@ "output_type": "execute_result" } ], - "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] + "source": [ + "### Answer Grader\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", + "\n", + "# Prompt\n", + "prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n", + " Here is the answer:\n", + " \\n ------- \\n\n", + " {generation} \n", + " \\n ------- \\n\n", + " Here is the question: {question}\n", + " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n", + " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", + " input_variables=[\"generation\", \"question\"],\n", + ")\n", + "\n", + "answer_grader = prompt | llm | JsonOutputParser()\n", + "answer_grader.invoke({\"question\": question, \"generation\": generation})" + ] }, { "cell_type": "code", @@ -224,7 +355,23 @@ "output_type": "execute_result" } ], - "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] + "source": [ + "### Question Re-writer\n", + "\n", + "# LLM\n", + "llm = ChatOllama(model=local_llm, temperature=0)\n", + "\n", + "# Prompt\n", + "re_write_prompt = PromptTemplate(\n", + " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", + " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n", + " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n", + " input_variables=[\"generation\", \"question\"],\n", + ")\n", + "\n", + "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", + "question_rewriter.invoke({\"question\": question})" + ] }, { "cell_type": "markdown", @@ -244,7 +391,26 @@ "id": "90fb1dc6-c482-483a-8441-39965c401beb", "metadata": {}, "outputs": [], - "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] + "source": [ + "from typing import List\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class GraphState(TypedDict):\n", + " \"\"\"\n", + " Represents the state of our graph.\n", + "\n", + " Attributes:\n", + " question: question\n", + " generation: LLM generation\n", + " documents: list of documents\n", + " \"\"\"\n", + "\n", + " question: str\n", + " generation: str\n", + " documents: List[str]" + ] }, { "cell_type": "code", @@ -252,7 +418,167 @@ "id": "5324ea49-5745-47b5-a0a5-bf58c8babe46", "metadata": {}, "outputs": [], - "source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] + "source": [ + "### Nodes\n", + "\n", + "\n", + "def retrieve(state):\n", + " \"\"\"\n", + " Retrieve documents\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, documents, that contains retrieved documents\n", + " \"\"\"\n", + " print(\"---RETRIEVE---\")\n", + " question = state[\"question\"]\n", + "\n", + " # Retrieval\n", + " documents = retriever.get_relevant_documents(question)\n", + " return {\"documents\": documents, \"question\": question}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): New key added to state, generation, that contains LLM generation\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # RAG generation\n", + " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", + " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", + "\n", + "\n", + "def grade_documents(state):\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates documents key with only filtered relevant documents\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Score each doc\n", + " filtered_docs = []\n", + " for d in documents:\n", + " score = retrieval_grader.invoke(\n", + " {\"question\": question, \"document\": d.page_content}\n", + " )\n", + " grade = score[\"score\"]\n", + " if grade == \"yes\":\n", + " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", + " filtered_docs.append(d)\n", + " else:\n", + " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", + " continue\n", + " return {\"documents\": filtered_docs, \"question\": question}\n", + "\n", + "\n", + "def transform_query(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " state (dict): Updates question key with a re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + "\n", + " # Re-write question\n", + " better_question = question_rewriter.invoke({\"question\": question})\n", + " return {\"documents\": documents, \"question\": better_question}\n", + "\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def decide_to_generate(state):\n", + " \"\"\"\n", + " Determines whether to generate an answer, or re-generate a question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Binary decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---ASSESS GRADED DOCUMENTS---\")\n", + " state[\"question\"]\n", + " filtered_documents = state[\"documents\"]\n", + "\n", + " if not filtered_documents:\n", + " # All documents have been filtered check_relevance\n", + " # We will re-generate a new query\n", + " print(\n", + " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", + " )\n", + " return \"transform_query\"\n", + " else:\n", + " # We have relevant documents, so generate answer\n", + " print(\"---DECISION: GENERATE---\")\n", + " return \"generate\"\n", + "\n", + "\n", + "def grade_generation_v_documents_and_question(state):\n", + " \"\"\"\n", + " Determines whether the generation is grounded in the document and answers question.\n", + "\n", + " Args:\n", + " state (dict): The current graph state\n", + "\n", + " Returns:\n", + " str: Decision for next node to call\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK HALLUCINATIONS---\")\n", + " question = state[\"question\"]\n", + " documents = state[\"documents\"]\n", + " generation = state[\"generation\"]\n", + "\n", + " score = hallucination_grader.invoke(\n", + " {\"documents\": documents, \"generation\": generation}\n", + " )\n", + " grade = score[\"score\"]\n", + "\n", + " # Check hallucination\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", + " # Check question-answering\n", + " print(\"---GRADE GENERATION vs QUESTION---\")\n", + " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", + " grade = score[\"score\"]\n", + " if grade == \"yes\":\n", + " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", + " return \"useful\"\n", + " else:\n", + " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", + " return \"not useful\"\n", + " else:\n", + " print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", + " return \"not supported\"" + ] }, { "cell_type": "markdown", @@ -270,7 +596,42 @@ "id": "5605dee4-b2df-46ae-a640-cc2ed90c21a6", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "workflow = StateGraph(GraphState)\n", + "\n", + "# Define the nodes\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", + "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", + "workflow.add_node(\"generate\", generate) # generatae\n", + "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", + "\n", + "# Build graph\n", + "workflow.add_edge(START, \"retrieve\")\n", + "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", + "workflow.add_conditional_edges(\n", + " \"grade_documents\",\n", + " decide_to_generate,\n", + " {\n", + " \"transform_query\": \"transform_query\",\n", + " \"generate\": \"generate\",\n", + " },\n", + ")\n", + "workflow.add_edge(\"transform_query\", \"retrieve\")\n", + "workflow.add_conditional_edges(\n", + " \"generate\",\n", + " grade_generation_v_documents_and_question,\n", + " {\n", + " \"not supported\": \"generate\",\n", + " \"useful\": END,\n", + " \"not useful\": \"transform_query\",\n", + " },\n", + ")\n", + "\n", + "# Compile\n", + "app = workflow.compile()" + ] }, { "cell_type": "markdown", @@ -324,7 +685,22 @@ ] } ], - "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] + "source": [ + "from pprint import pprint\n", + "\n", + "# Run\n", + "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n", + "for output in app.stream(inputs):\n", + " for key, value in output.items():\n", + " # Node\n", + " pprint(f\"Node '{key}':\")\n", + " # Optional: print full state at each node\n", + " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", + " pprint(\"\\n---\\n\")\n", + "\n", + "# Final generation\n", + "pprint(value[\"generation\"])" + ] }, { "cell_type": "markdown", @@ -335,14 +711,6 @@ "\n", "https://smith.langchain.com/public/4163a342-5260-4852-8602-bda3f95177e7/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "953143c2-2f2a-4361-a36b-87db7cf21d63", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/react-agent-structured-output.ipynb b/examples/react-agent-structured-output.ipynb new file mode 100644 index 000000000..31b7847f6 --- /dev/null +++ b/examples/react-agent-structured-output.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "attachments": { + "59e8ed35-f2b4-421e-8d21-880e7ab31e5f.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAg0AAAF+CAYAAAABRilmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAUGVYSWZNTQAqAAAACAACARIAAwAAAAEAAQAAh2kABAAAAAEAAAAmAAAAAAADoAEAAwAAAAEAAQAAoAIABAAAAAEAAAINoAMABAAAAAEAAAF+AAAAAMvtFIkAAAIyaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zODI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTI1PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpiGWaJAABAAElEQVR4Aey9B5wmRbX+fybPbIJNsOScYckgOYMgScmCyNULGK9ZuIb74Sr+7l9RrwrXBAaMiAQlKyJBQKJIRsICS4Zl2Tx55v98T3e90/PuO2l3lt2ZPbXbUx2qKzz9dp+nzjlVVdWtYBECgUAgEAgEAoFAIBAYAIHqAa7H5UAgEAgEAoFAIBAIBByBIA3xQwgEAoFAIBAIBAKBQSEQpGFQMEWiQCAQCAQCgUAgEAjSEL+BQCAQCAQCgUAgEBgUAkEaBgVTJAoEAoFAIBAIBAKBIA3xGwgEAoFAIBAIBAKBQSEQpGFQMEWiQCAQCAQCgUAgEAjSEL+BQCAQCAQCgUAgEBgUAkEaBgVTJAoEAoFAYOVFoK2tzTo6OnoBwLyA7e3t1tnZ2et8HAwOgZGKXe3gmhepAoFAIBAIBObNm2cvvPCCLViwoARGVVWV1dTUWENDg02ZMsUmT55sdXV1peuD3UGIPPvss573hhtuaKuuuupgby2lmzt3rtXX11tjY6NRr+EIra2t9o9//MPWXntt3yAPr7zyir3xxhv25ptvenmrrLKKrbnmmt722tpMrEAmwIm6gM2yDC0tLU5qKCuVvyzLK8/7tddeszlz5tgaa6xhEyZMKL9cOgbLV1991XEDO+o7adIkx42YZzeUsKza3d+zC9IwlCcUaQOBQGClRuDBBx+0s88+25577jmbOnWqVVdnylo+9gjGPffc00466STbbLPNhkwcICNf+MIX7NZbb7Uf/ehHdsQRRwwpj/nz59s111xjW265pU2fPt2JzHA8rOeff94uuugiO+GEE5wYPPzww/bTn/7U7rrrLkPbgICBJL3rXe+yE0880TbeeGMX3DNnzrR77rnHdtxxRz83HHWplAd1uPfee52g7LTTTv5cKqVblueuvvpqu/766+3Tn/607bHHHhWLgkDdd999dskll9hDDz1kXV1d/vuBaG677bb2nve8x7baaqtBE4dl2e7+nl2QhoqPN04GAoFAINA3AmeccYYddthhThT4eNPzvuOOO+y3v/2tC4KPfOQj3uvsO4fFr/z973934Ttx4kTv2e+yyy7es188ZeUz//rXv1y4f+hDH3LSUDnV0M8+9thjrkFZbbXVbPbs2fad73zH6FmfeeaZhpBubm62O++80y6++GIXhB/+8Ie97X/605/spptusnXXXXeZkwZIDLghdFfEAEGADH772992zcJnPvMZJwporiA8v/nNb2zWrFnG72aLLbYYlLaE392yand/zy5Iw4r4C4s6BQKBwAqNAIJwm222cfUyFaWnSA8TbcEjjzziqntU1XzYEQao8lFNo43gPOr8pKXgfgTv3Xff7QKWvJ944gk3Vay11lq9zAzkQX4Ib8wP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" + }, + "e9ef3df1-dbc0-4ff0-8040-0280372d67ac.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAFFCAYAAACE4KdOAABAAElEQVR4AexdB5wV1fU+7C69d6R3RAEBCxZUEFGwRUURNXaMNUaNf7sxRo0aiSWx914iib0idkSwUETp0ntnFxa2/u93d89jdpjXy8577zv83puZO7d+d9j53jn3nFuj3IhQiAARIAJEgAgQASJABIiABwI5HmlMIgJEgAgQASJABIgAESACFgGSRT4IRIAIEAEiQASIABEgAkERIFkMCg1vEAEiQASIABEgAkSACJAs8hkgAkSACBABIkAEiAARCIoAyWJQaHiDCBABIkAEiAARIAJEgGSRzwARIAJEgAgQASJABIhAUARIFoNCwxtEgAgQASJABIgAESACJIt8BogAESACRIAIEAEiQASCIkCyGBQa3iACRIAIEAEiQASIABEgWeQzQASIABEgAkSACBABIhAUAZLFoNDwBhEgAkSACBABIkAEiADJIp8BIkAEiAARIAJEgAgQgaAIkCwGhYY3iAARIAJEgAgQASJABEgW+QwQASJABIgAESACRIAIBEWAZDEoNLxBBIgAESACRIAIEAEiQLLIZ4AIEAEiQASIABEgAkQgKAIki0Gh4Q0iQASIABEgAkSACBABkkU+A0SACBABIkAEiAARIAJBESBZDAoNbxABIkAEiAARIAJEgAiQLPIZIAJEgAgQASJABIgAEQiKAMliUGh4gwgQASJABIgAESACRIBkkc8AESACRIAIEAEiQASIQFAESBaDQsMbRIAIEAEiQASIABEgAiSLfAaIABEgAkSACBABIkAEgiJAshgUGt4gAkSACBABIkAEiAARIFnkM0AEiAARIAJEgAgQASIQFAGSxaDQ8AYRIAJEgAgQASJABIgAySKfASJABIhABiEwc+2vctknN1bbiNztu6+rrWNsmAgQgZgRIFmMGToWJAJEgAj4D4ElW1fIpBXfy46SndXSOXf77utq6RQbJQJEIC4ESBbjgo+FiQARIAL+QqC0vNR2qLBkR7V0zN2++7paOsVGiQARiAsBksW44GNhIkAEiIC/ECgqLbIdysvJjblj24sLYy7rbt99HXPFLEgEiEC1IZBXbS2zYSJABIhAFiGwccdmmbZmlhSVFgvOt+7cKtsMKSstK5VezbvJiT1GJAQN1A+pm1cn4vp2GoL52LQX5NMlX0tx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" + }, + "f717c664-605d-48d7-b534-deec99087214.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to return structured output with a ReAct style agent\n", + "\n", + "You might want your agent to return its output in a structured format. For example, if the output of the agent is used by some other downstream software, you may want the output to be in the same structured format every time the agent is invoked to ensure consistency.\n", + "\n", + "This notebook will walk through two different options for forcing a function calling agent to structure its output. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user. Both of the options will use the same graph structure as shown in the diagram below, but will have different mechanisms under the hood.\n", + "\n", + "![react_diagrams.png](attachment:59e8ed35-f2b4-421e-8d21-880e7ab31e5f.png)\n", + "\n", + "**Option 1**\n", + "\n", + "![option1.png](attachment:f717c664-605d-48d7-b534-deec99087214.png)\n", + "\n", + "The first way you can force your tool calling agent to have structured output is to bind the output you would like as an additional tool for the `agent` node to use. In contrast to the basic ReAct agent, the `agent` node in this case is not selecting between `tools` and `END` but rather selecting between the specific tools it calls. The expected flow in this case is that the LLM in the `agent` node will first select the action tool, and after receiving the action tool output it will call the response tool, which will then route to the `respond` node which simply structures the arguments from the `agent` node tool call.\n", + "\n", + "**Pros and Cons**\n", + "\n", + "The benefit to this format is that you only need one LLM, and can save money and latency because of this. The downside to this option is that it isn't guaranteed that the single LLM will call the correct tool when you want it to. We can help the LLM by setting `tool_choice` to `any` when we use `bind_tools` which forces the LLM to select at least one tool at every turn, but this is far from a fool proof strategy. In addition, another downside is that the agent might call *multiple* tools, so we need to check for this explicitly in our routing function (or if we are using OpenAI we an set `parallell_tool_calling=False` to ensure only one tool is called at a time).\n", + "\n", + "**Option 2**\n", + "\n", + "![option2.png](attachment:e9ef3df1-dbc0-4ff0-8040-0280372d67ac.png)\n", + "\n", + "The second way you can force your tool calling agent to have structured output is to use a second LLM (in this case `model_with_structured_output`) to respond to the user. \n", + "\n", + "In this case, you will define a basic ReAct agent normally, but instead of having the `agent` node choose between the `tools` node and ending the conversation, the `agent` node will choose between the `tools` node and the `respond` node. The `respond` node will contain a second LLM that uses structured output, and once called will return directly to the user. You can think of this method as basic ReAct with one extra step before responding to the user. \n", + "\n", + "**Pros and Cons**\n", + "\n", + "The benefit of this method is that it guarantees structured output (as long as `.with_structured_output` works as expected with the LLM). The downside to using this approach is that it requires making an additional LLM call before responding to the user, which can increase costs as well as latency. In addition, by not providing the `agent` node LLM with information about the desired output schema there is a risk that the `agent` LLM will fail to call the correct tools required to answer in the correct output schema.\n", + "\n", + "Note that both of these options will follow the exact same graph structure (see the diagram above), in that they are both exact replicas of the basic ReAct architecture but with a `respond` node before the end.\n", + "\n", + "## Setup\n", + "\n", + "For our setup we need to define how we want to structure our output, define our graph state, and also our tools and the models we are going to use.\n", + "\n", + "To use structured output, we will use the `with_structured_output` method from LangChain, which you can read more about [here](https://python.langchain.com/v0.2/docs/how_to/structured_output/).\n", + "\n", + "We are going to use a single tool in this example for finding the weather, and will return a structured weather response to the user." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import BaseModel, Field\n", + "from typing import Literal\n", + "from langchain_core.tools import tool\n", + "from langchain_anthropic import ChatAnthropic\n", + "from langgraph.graph import MessagesState\n", + "\n", + "class WeatherResponse(BaseModel):\n", + " \"\"\"Respond to the user with this\"\"\"\n", + " temperature: float = Field(description=\"The temperature in fahrenheit\")\n", + " wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n", + " wind_speed: float = Field(description=\"The speed of the wind in km/h\")\n", + "\n", + "# Inherit 'messages' key from MessagesState, which is a list of chat messages \n", + "class AgentState(MessagesState):\n", + " # Final structured response from the agent\n", + " final_response: WeatherResponse\n", + "\n", + "@tool\n", + "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", + " \"\"\"Use this to get weather information.\"\"\"\n", + " if city == \"nyc\":\n", + " return \"It is cloudy in NYC, with 5 mph winds in the North-East direction and a temperature of 70 degrees\"\n", + " elif city == \"sf\":\n", + " return \"It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction\"\n", + " else:\n", + " raise AssertionError(\"Unknown city\")\n", + " \n", + "tools = [get_weather]\n", + " \n", + "model = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", + " \n", + "model_with_tools = model.bind_tools(tools)\n", + "model_with_structured_output = model.with_structured_output(WeatherResponse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 1: Bind output as tool\n", + "\n", + "Let's now examine how we would use the single LLM option.\n", + "\n", + "### Define Graph\n", + "\n", + "The graph definition is very similar to the one above, the only difference is we no longer call an LLM in the `response` node, and instead bind the `WeatherResponse` tool to our LLM that already contains the `get_weather` tool." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tools = [get_weather, WeatherResponse]\n", + "\n", + "# Force the model to use tools by passing tool_choice=\"any\" \n", + "model_with_response_tool = model.bind_tools(tools,tool_choice=\"any\")\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: AgentState):\n", + " response = model_with_response_tool.invoke(state['messages'])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function that responds to the user\n", + "def respond(state: AgentState):\n", + " # Construct the final answer from the arguments of the last tool call\n", + " response = WeatherResponse(**state['messages'][-1].tool_calls[0]['args'])\n", + " # We return the final answer\n", + " return {\"final_response\": response}\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: AgentState):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is only one tool call and it is the response tool call we respond to the user\n", + " if len(last_message.tool_calls) == 1 and last_message.tool_calls[0]['name'] == \"WeatherResponse\":\n", + " return \"respond\"\n", + " # Otherwise we will use the tool node again\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"respond\", respond)\n", + "workflow.add_node(\"tools\", ToolNode(tools))\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + " {\n", + " \"continue\": \"tools\",\n", + " \"respond\": \"respond\",\n", + " },\n", + ")\n", + "\n", + "workflow.add_edge(\"tools\", \"agent\")\n", + "workflow.add_edge(\"respond\", END)\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Usage\n", + "\n", + "Now we can run our graph to check that it worked as intended:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=3.0)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, the agent returned a `WeatherResponse` object as we expected." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 2: 2 LLMs\n", + "\n", + "Let's now dive into how we would use a second LLM to force structured output.\n", + "\n", + "### Define Graph\n", + "\n", + "We can now define our graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: AgentState):\n", + " response = model_with_tools.invoke(state['messages'])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function that responds to the user\n", + "def respond(state: AgentState):\n", + " # We call the model with structured output in order to return the same format to the user every time\n", + " # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n", + " # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n", + " response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n", + " # We return the final answer\n", + " return {\"final_response\": response}\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: AgentState):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we respond to the user\n", + " if not last_message.tool_calls:\n", + " return \"respond\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"respond\", respond)\n", + "workflow.add_node(\"tools\", ToolNode(tools))\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + " {\n", + " \"continue\": \"tools\",\n", + " \"respond\": \"respond\",\n", + " },\n", + ")\n", + "\n", + "workflow.add_edge(\"tools\", \"agent\")\n", + "workflow.add_edge(\"respond\", END)\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Usage\n", + "\n", + "We can now invoke our graph to verify that the output is being structured as desired:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=4.83)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the agent returned a `WeatherResponse` object as we expected. If would now be easy to use this agent in a more complex software stack without having to worry about the output of the agent not matching the format expected from the next step in the stack." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/react_diagrams.png b/examples/react_diagrams.png new file mode 100644 index 000000000..9d755884c Binary files /dev/null and b/examples/react_diagrams.png differ diff --git a/examples/recursion-limit.ipynb b/examples/recursion-limit.ipynb new file mode 100644 index 000000000..0021baa8b --- /dev/null +++ b/examples/recursion-limit.ipynb @@ -0,0 +1,172 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to control graph recursion limit\n", + "\n", + "You can set the graph recursion limit when invoking or streaming the graph. The recursion limit sets the number of supersteps that the graph is allowed to execute before it raises an error. Read more about the concept of recursion limits [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#recursion-limit). Let's see an example of this in a simple graph with parallel branches to better understand exactly how the recursion limit works.\n", + "\n", + "## Define our graph" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, Any\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "class State(TypedDict):\n", + " # The operator.add reducer fn makes this append-only\n", + " aggregate: Annotated[list, operator.add]\n", + "\n", + "\n", + "class ReturnNodeValue:\n", + " def __init__(self, node_secret: str):\n", + " self._value = node_secret\n", + "\n", + " def __call__(self, state: State) -> Any:\n", + " print(f\"Adding {self._value} to {state['aggregate']}\")\n", + " return {\"aggregate\": [self._value]}\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", + "builder.add_edge(START, \"a\")\n", + "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n", + "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n", + "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n", + "builder.add_edge(\"a\", \"b\")\n", + "builder.add_edge(\"a\", \"c\")\n", + "builder.add_edge(\"b\", \"d\")\n", + "builder.add_edge(\"c\", \"d\")\n", + "builder.add_edge(\"d\", END)\n", + "graph = builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(graph.get_graph().draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our graph will execute nodes `b` and `c` in parallel (i.e. in a single super-step), which means that if we run this graph it should take exactly 3 steps. We can set the recursion limit to 3 first to check that it raises an error (the recursion limit is inclusive, so if the limit is 3 the graph will raise an error when it reaches step 3) as expected: " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding I'm A to []\n", + "Adding I'm B to [\"I'm A\"]\n", + "Adding I'm C to [\"I'm A\"]\n", + "Adding I'm D to [\"I'm A\", \"I'm B\", \"I'm C\"]\n", + "Recursion Error\n" + ] + } + ], + "source": [ + "from langgraph.errors import GraphRecursionError\n", + "\n", + "try:\n", + " graph.invoke({\"aggregate\": []},{\"recursion_limit\":3})\n", + "except GraphRecursionError:\n", + " print(\"Recursion Error\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Success! The graph raised an error as expected - now let's test setting the recursion limit to 4 and ensure that the graph succeeds in this case:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding I'm A to []\n", + "Adding I'm B to [\"I'm A\"]\n", + "Adding I'm C to [\"I'm A\"]\n", + "Adding I'm D to [\"I'm A\", \"I'm B\", \"I'm C\"]\n" + ] + } + ], + "source": [ + "try:\n", + " graph.invoke({\"aggregate\": []},{\"recursion_limit\":4})\n", + "except GraphRecursionError:\n", + " print(\"Recursion Error\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect, just as we expected the graph runs successfully in this case. \n", + "\n", + "Setting the correct graph recursion limit is important for avoiding graph runs stuck in long-running loops and thus helps minimize unnecessary costs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 810990e38..494bb4aca 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -42,7 +42,16 @@ "execution_count": 2, "id": "3368f330-cad6-4d35-a291-68fbf4389d98", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LANGCHAIN_API_KEY ········\n", + "FIREWORKS_API_KEY ········\n" + ] + } + ], "source": [ "import getpass\n", "import os\n", @@ -96,7 +105,7 @@ ")\n", "llm = ChatFireworks(\n", " model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n", - " model_kwargs={\"max_tokens\": 32768},\n", + " max_tokens=32768\n", ")\n", "generate = prompt | llm" ] @@ -111,19 +120,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "Title: The Relevance of The Little Prince in Modern Childhood\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "The Little Prince, a novella by Antoine de Saint-Exupéry, has been a childhood favorite for generations. Despite being published over seven decades ago, its timeless themes continue to resonate with modern children, making it highly relevant in contemporary childhood.\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's The Little Prince is a timeless novella that has captured the hearts and minds of children and adults alike for over seven decades. Its enduring charm and profound wisdom have transcended generations, making it a classic staple in childhood literature. This essay explores the reasons why The Little Prince remains relevant in modern childhood.\n", "\n", - "Firstly, the story explores the complex nature of human relationships, which is particularly relevant for modern children growing up in an increasingly connected yet impersonal world. Through the little prince's encounters with various grown-ups on different planets, the book highlights the importance of genuine connections and understanding. In an age where digital communication often replaces face-to-face interaction, this message is more pertinent than ever. The Little Prince encourages children to look beyond superficial relationships and seek deeper connections, fostering empathy and emotional intelligence.\n", + "First Paragraph:\n", + "One of the primary reasons for The Little Prince's relevance is its exploration of themes that resonate with children today. The story addresses universal aspects of childhood, such as the struggle to understand the world, the desire for friendship and love, and the pain of loss and loneliness. The Little Prince's encounters with various grown-ups, each representing different facets of adult absurdity, mirror the confusion and disillusionment children experience as they grow and navigate their way through a complex world.\n", "\n", - "Secondly, the book deals with the concept of responsibility and self-discovery, elements that are integral to a child's growth. The little prince's journey is essentially a quest for self-discovery, leading him to realize his responsibility towards his beloved rose. This narrative encourages modern children to embrace their individuality while understanding the significance of their actions. In a society that often overlooks the emotional well-being of children, The Little Prince offers a refreshing perspective on personal growth and responsibility.\n", + "Second Paragraph:\n", + "Moreover, The Little Prince promotes values that are essential for modern childhood. It emphasizes the importance of imagination, creativity, and curiosity, encouraging children to question, explore, and seek their own truths. The Little Prince's friendship with the fox teaches children about the value of emotional connections, empathy, and responsibility, lessons that are increasingly vital in our technology-driven, fast-paced society.\n", "\n", - "Thirdly, the book addresses the challenging theme of loss and bereavement. The little prince's departure from his asteroid and his subsequent encounters with the fox and the snake are profound reflections on the inevitability of loss and the importance of cherishing relationships. In a time when children are exposed to various forms of loss, from the death of loved ones to environmental degradation, The Little Prince provides a gentle yet powerful way to understand and cope with these experiences.\n", + "Third Paragraph:\n", + "The Little Prince also serves as a reminder of the significance of nature and the environment in our lives. The story's depiction of the desert, the baobabs, and the mysterious asteroid B-612 fosters an appreciation for the beauty and fragility of the natural world. In an era of climate change and environmental degradation, The Little Prince's message about the importance of nurturing and preserving our planet is more relevant than ever.\n", "\n", - "However, some critics argue that the book's pace and abstract concepts might be challenging for modern children with short attention spans. To address this, a revised version could incorporate more visual elements and interactive activities to engage young readers better. Additionally, supplementary materials explaining the book's themes in simpler terms could be provided for parents and educators to use in discussions with children.\n", + "Fourth Paragraph:\n", + "Furthermore, The Little Prince offers a unique perspective on mental health and emotional well-being. The story delicately tackles issues such as depression, isolation, and the search for meaning, providing a nuanced understanding of these complex topics. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives.\n", "\n", - "In conclusion, The Little Prince remains relevant in modern childhood due to its exploration of human relationships, self-discovery, and loss. These themes, wrapped in a captivating narrative, offer valuable lessons for modern children. While some adaptations may be necessary to cater to the preferences of today's children, the essence of the story remains a powerful tool for teaching emotional intelligence, personal growth, and resilience." + "Conclusion:\n", + "In conclusion, The Little Prince remains a relevant and essential read for modern childhood due to its exploration of timeless themes, promotion of essential values, emphasis on nature and environmental stewardship, and sensitive treatment of mental health and emotional well-being. By engaging with this classic tale, children can gain invaluable insights and skills that will serve them well throughout their lives. The Little Prince's enduring legacy is a testament to its ability to captivate, inspire, and educate generations of children, making it an indispensable part of childhood literature." ] } ], @@ -175,21 +190,35 @@ "name": "stdout", "output_type": "stream", "text": [ - "Essay Grade: B+\n", + "Essay Critique and Recommendations:\n", "\n", - "The essay you submitted provides a clear and well-structured argument about the relevance of The Little Prince in modern childhood. You have demonstrated a strong understanding of the text and its themes, and have effectively applied them to the context of contemporary childhood. However, there are some areas where improvement could be made to enhance the depth, style, and overall flow of your essay.\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "1. Length: While your essay is well-written and informative, it is relatively brief. Expanding on each point with more detailed analysis and examples would strengthen your argument and demonstrate a more comprehensive understanding of the text. Aim for a minimum of 500 words to allow for a more in-depth exploration of your ideas.\n", + "Introduction:\n", + "The introduction provides a clear and concise overview of the topic, setting the stage for the rest of the essay. The author has done an excellent job of establishing the significance of The Little Prince and its enduring appeal.\n", "\n", - "2. Depth: Although you have touched upon the relevance of the novel's themes, further analysis is needed to truly establish its significance in modern childhood. For example, when discussing the complex nature of human relationships, delve into how the digital age affects children's communication skills, and how The Little Prince addresses this issue. Providing concrete examples from the text and connecting them to real-world scenarios will make your argument more compelling.\n", + "First Paragraph:\n", + "The first paragraph effectively highlights the universal themes present in The Little Prince that resonate with children today. The author could improve the paragraph by providing specific examples from the book to illustrate each theme, making the essay more engaging and demonstrating a deeper understanding of the text.\n", "\n", - "3. Style: To engage your readers more effectively, consider varying your sentence structure and length. Using a mix of simple, compound, and complex sentences will improve the flow of your essay and make it more engaging to read. Additionally, watch your tense consistency. Ensure that you maintain the same tense throughout your essay to avoid confusion.\n", + "Second Paragraph:\n", + "The second paragraph emphasizes the values promoted by The Little Prince and their relevance to modern childhood. The author could expand on this by discussing how these values can be applied in everyday life, providing practical examples for children to follow. Additionally, the author may consider delving into the role of the fox in the story and its impact on the Prince's character development.\n", "\n", - "4. Recommendations: While your suggestions for adaptation are a good start, they could be expanded upon to provide more comprehensive recommendations. For example, you may want to discuss different methods of incorporating visual elements and interactive activities, such as illustrations, quizzes, or discussion questions. This will demonstrate that you have thoughtfully considered the needs of modern children and have developed strategies to address these challenges.\n", + "Third Paragraph:\n", + "The third paragraph discusses the importance of nature and environmental stewardship in The Little Prince. The author could strengthen this paragraph by connecting the story's themes to current environmental issues, helping children understand the relevance and urgency of protecting the planet. Furthermore, the author may include specific strategies children can adopt to contribute to environmental conservation.\n", "\n", - "5. Conclusion: Your conclusion could benefit from a stronger summarization of your key points and an assertive final statement about the relevance of The Little Prince in modern childhood. Tying all your arguments together in a concise and powerful manner will leave a lasting impression on your readers and solidify your position.\n", + "Fourth Paragraph:\n", + "The fourth paragraph addresses the sensitive topic of mental health and emotional well-being in The Little Prince. The author could improve this paragraph by providing more context on the representation of these issues in the story and offering resources or advice for children who may be experiencing similar emotions. This approach would ensure the essay is not only informative but also supportive and empathetic.\n", "\n", - "Overall, your essay is well-researched and provides a solid foundation for a compelling argument about the relevance of The Little Prince in modern childhood. With some expansion, deeper analysis, and stylistic improvements, your essay can achieve an even higher level of excellence." + "Conclusion:\n", + "The conclusion effectively summarizes the main points of the essay while emphasizing the importance of The Little Prince in modern childhood. The author could consider adding a call-to-action, encouraging children to read or revisit the novella and reflect on its lessons. Additionally, the author may include a brief statement on the lasting impact of The Little Prince and its potential influence on future generations.\n", + "\n", + "Recommendations:\n", + "\n", + "1. Incorporate more direct quotes from the text to support arguments and engage the reader.\n", + "2. Expand on specific themes, values, and concepts to provide greater depth and insight.\n", + "3. Offer practical applications and strategies for children to apply the lessons from The Little Prince in their daily lives.\n", + "4. Consider the age range and reading level of the intended audience and adjust the language and content accordingly.\n", + "5. Ensure a balanced mix of summary, analysis, and interpretation to maintain the reader's interest and demonstrate a thorough understanding of the text." ] } ], @@ -220,19 +249,45 @@ "name": "stdout", "output_type": "stream", "text": [ - "Title: The Relevance of The Little Prince in Modern Childhood: A Contemporary Analysis\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "In the digital age, where human connections are often overshadowed by virtual communication, Antoine de Saint-Exupéry's The Little Prince remains a timeless classic that offers invaluable insights for modern children. This essay aims to delve deeper into the relevance of this novella in contemporary childhood, focusing on the complex nature of human relationships, self-discovery, and the inevitability of loss.\n", + "Introduction:\n", + "The introduction provides a clear and concise overview of the topic, setting the stage for the rest of the essay. The author has done an excellent job of establishing the significance of The Little Prince and its enduring appeal.\n", "\n", - "Firstly, The Little Prince offers a powerful critique of the superficiality that permeates the digital world. Through the little prince's encounters with various grown-ups, the book emphasizes the importance of genuine connections and understanding. Despite being published in 1943, Saint-Exupéry's work uncannily predicts the isolating effects of technology on human interaction. It encourages children to seek deeper connections, fostering empathy and emotional intelligence. For instance, the little prince's relationship with the fox teaches him that \"the eyes are blind, and you have to look with the heart\" (Saint-Exupéry, 1943, p. 48). In the context of modern childhood, where children are increasingly dependent on digital devices, this message is more pertinent than ever.\n", + "First Paragraph:\n", + "The first paragraph effectively highlights the universal themes present in The Little Prince that resonate with children today. To improve the paragraph, specific examples from the book will be added to illustrate each theme, making the essay more engaging and demonstrating a deeper understanding of the text.\n", "\n", - "Secondly, The Little Prince addresses the challenges of self-discovery and responsibility faced by modern children. The little prince's journey to Earth can be seen as an exploration of his individuality and understanding of his role in the world. His relationship with the rose illustrates the significance of taking responsibility for one's actions. In the current world, where children are often left to navigate their personal growth without proper guidance, the book offers a refreshing perspective on self-discovery, responsibility, and the importance of inner beauty.\n", + "Second Paragraph:\n", + "The second paragraph emphasizes the values promoted by The Little Prince and their relevance to modern childhood. The author will expand on this by discussing how these values can be applied in everyday life, providing practical examples for children to follow. Additionally, the author will delve into the role of the fox in the story and its impact on the Prince's character development.\n", "\n", - "Thirdly, The Little Prince offers a nuanced understanding of loss and bereavement, which is increasingly relevant to modern children. Through the little prince's departure from his asteroid and his subsequent encounters with the fox and the snake, Saint-Exupéry delivers a profound reflection on the inevitability of loss and the importance of cherishing relationships. As children grapple with issues like environmental degradation, bullying, or the death of loved ones, The Little Prince provides a gentle yet powerful way to understand and cope with these experiences.\n", + "Third Paragraph:\n", + "The third paragraph discusses the importance of nature and environmental stewardship in The Little Prince. To strengthen this paragraph, the author will connect the story's themes to current environmental issues, helping children understand the relevance and urgency of protecting the planet. Furthermore, the author will include specific strategies children can adopt to contribute to environmental conservation.\n", "\n", - "However, as noted by critics, the book's abstract language and lengthy monologues may present challenges for some modern children. To address this, adaptations can be made to better align the book with their preferences and needs. For instance, incorporating more visual elements such as illustrations can help maintain engagement, while interactive activities like quizzes or discussion questions can deepen understanding. Furthermore, supplementary materials explaining the book's themes in simpler terms can aid parents and educators in guiding children through complex discussions.\n", + "Fourth Paragraph:\n", + "The fourth paragraph addresses the sensitive topic of mental health and emotional well-being in The Little Prince. The author will improve this paragraph by providing more context on the representation of these issues in the story and offering resources or advice for children who may be experiencing similar emotions. This approach will ensure the essay is not only informative but also supportive and empathetic.\n", "\n", - "In conclusion, The Little Prince remains a powerful and enduring narrative for modern children as it delves into the complex nature of human relationships, self-discovery, and loss. With thoughtful adaptations and insightful guidance, this timeless classic can continue to guide young readers through their personal growth and emotional development. The Little Prince truly is a testament to the power of literature as a vehicle for conveying universal truths and emotions, making it an indispensable part of childhood reading experiences." + "Conclusion:\n", + "The conclusion effectively summarizes the main points of the essay while emphasizing the importance of The Little Prince in modern childhood. The author will add a call-to-action, encouraging children to read or revisit the novella and reflect on its lessons. Additionally, the author will include a brief statement on the lasting impact of The Little Prince and its potential influence on future generations.\n", + "\n", + "Revised Essay:\n", + "\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's The Little Prince is a timeless novella that has captured the hearts and minds of children and adults alike for over seven decades. Its enduring charm and profound wisdom have transcended generations, making it a classic staple in childhood literature. This essay explores the reasons why The Little Prince remains relevant in modern childhood, focusing on its exploration of universal themes, promotion of essential values, emphasis on nature and environmental stewardship, and sensitive treatment of mental health and emotional well-being.\n", + "\n", + "First Paragraph:\n", + "The Little Prince explores themes that resonate with children today, such as the struggle to understand the world, the desire for friendship and love, and the pain of loss and loneliness. For example, the Prince's encounter with the conceited man (Chapter IV) mirrors the frustration children experience when interacting with adults who prioritize their own egos over genuine connections. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives.\n", + "\n", + "Second Paragraph:\n", + "The Little Prince promotes values that are essential for modern childhood. It emphasizes the importance of imagination, creativity, and curiosity, encouraging children to question, explore, and seek their own truths. For instance, the Prince's friendship with the fox teaches children about the value of emotional connections, empathy, and responsibility. In our technology-driven, fast-paced society, these values are increasingly vital for building meaningful relationships and fostering emotional well-being.\n", + "\n", + "Third Paragraph:\n", + "The Little Prince also serves as a reminder of the significance of nature and the environment in our lives. The story's depiction of the desert, the baobabs, and the mysterious asteroid B-612 fosters an appreciation for the beauty and fragility of the natural world. In an era of climate change and environmental degradation, The Little Prince's message about the importance of nurturing and preserving our planet is more relevant than ever. To contribute to environmental conservation, children can adopt simple strategies, such as reducing waste, planting trees, and raising awareness about environmental issues in their communities.\n", + "\n", + "Fourth Paragraph:\n", + "Furthermore, The Little Prince offers a unique perspective on mental health and emotional well-being. The story delicately tackles issues such as depression, isolation, and the search for meaning, providing a nuanced understanding of these complex topics. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives. For children struggling with mental health issues, it is essential to seek help from trusted adults, such as parents, teachers, or mental health professionals.\n", + "\n", + "Conclusion:\n", + "In conclusion, The Little Prince's enduring legacy is a testament to its ability to captivate, inspire, and educate generations of children, making it an indispensable part of childhood literature. By engaging with this classic tale, children can gain invaluable insights and skills that will serve them well throughout their lives. The author encourages children to read or revisit The Little Prince and reflect on its lessons, ultimately applying its timeless wisdom to their daily lives." ] } ], @@ -263,27 +318,28 @@ "from typing import Annotated, List, Sequence\n", "from langgraph.graph import END, StateGraph, START\n", "from langgraph.graph.message import add_messages\n", + "from langgraph.checkpoint.memory import MemorySaver\n", "from typing_extensions import TypedDict\n", "\n", "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", - " \n", - "async def generation_node(state: Sequence[BaseMessage]):\n", - " return await generate.ainvoke({\"messages\": state})\n", + "\n", + "async def generation_node(state: State) -> State:\n", + " return {\"messages\": [await generate.ainvoke(state['messages'])]}\n", "\n", "\n", - "async def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n", + "async def reflection_node(state: State) -> State:\n", " # Other messages we need to adjust\n", " cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n", " # First message is the original user request. We hold it the same for all nodes\n", - " translated = [messages[0]] + [\n", - " cls_map[msg.type](content=msg.content) for msg in messages[1:]\n", + " translated = [state['messages'][0]] + [\n", + " cls_map[msg.type](content=msg.content) for msg in state['messages'][1:]\n", " ]\n", - " res = await reflect.ainvoke({\"messages\": translated})\n", + " res = await reflect.ainvoke(translated)\n", " # We treat the output of this as human feedback for the generator\n", - " return HumanMessage(content=res.content)\n", + " return {\"messages\": [HumanMessage(content=res.content)]}\n", "\n", "\n", "builder = StateGraph(State)\n", @@ -292,62 +348,84 @@ "builder.add_edge(START, \"generate\")\n", "\n", "\n", - "def should_continue(state: List[BaseMessage]):\n", - " if len(state) > 6:\n", + "def should_continue(state: State):\n", + " if len(state[\"messages\"]) > 6:\n", " # End after 3 iterations\n", " return END\n", " return \"reflect\"\n", "\n", "\n", + "\n", "builder.add_conditional_edges(\"generate\", should_continue)\n", "builder.add_edge(\"reflect\", \"generate\")\n", - "graph = builder.compile()" + "memory = MemorySaver()\n", + "graph = builder.compile(checkpointer=memory)" ] }, { "cell_type": "code", "execution_count": 9, - "id": "06263a07-8a15-4ec3-b692-1c6cef3b1c1f", + "id": "010ce60a-8b7d-4258-99d1-52705146844f", "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "06263a07-8a15-4ec3-b692-1c6cef3b1c1f", + "metadata": { + "scrolled": true + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'generate': AIMessage(content=\"Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\\n\\nBody Paragraph 1 - Love and Loneliness:\\nOne of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThroughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nCritics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\\n\\nConclusion:\\nUltimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\\n\\nRevised Essay:\\n\\nTitle: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\\n\\nBody Paragraph 1 - Love, Loss, and Friendship:\\nThe Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThroughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\\n\\nBody Paragraph 3 - Rejecting the Superficiality of the Adult World:\\nThe Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\\n\\nConclusion:\\nThe Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\")}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a classic novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince\" and discuss how its message can be applied to the complexities of the modern world.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that explores various aspects of the human condition through its whimsical characters and situations. The Little Prince himself represents innocence, curiosity, and the importance of human connection. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. The Little Prince encourages readers to cherish and nurture genuine relationships, reminding us that true happiness and fulfillment come from understanding and empathizing with others.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" also offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today\\'s world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.', response_metadata={'token_usage': {'prompt_tokens': 72, 'total_tokens': 632, 'completion_tokens': 560}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-b39a25ab-24f6-42d0-96c2-0f74c3ecc8f7-0', usage_metadata={'input_tokens': 72, 'output_tokens': 560, 'total_tokens': 632})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"Introduction:\\nThe essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\\n\\nBody Paragraph 1 - Love and Loneliness:\\nThe paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThis paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nThe criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\\n\\nConclusion:\\nThe conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\\n\\nOverall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Essay Critique and Recommendations:\\n\\nTitle: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nThe introduction effectively sets the stage for the essay by providing background information on \"The Little Prince\" and its relevance in modern life. However, consider adding a hook to engage the reader\\'s attention and create a stronger first impression.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\nThis paragraph provides a clear explanation of the allegorical nature of \"The Little Prince.\" To enhance this section, consider offering specific examples from the text to illustrate how the characters and situations symbolize various aspects of adult life. This will strengthen your analysis and make it more engaging for the reader.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe relevance of the Little Prince\\'s message is well-articulated in this paragraph. To further strengthen your argument, consider discussing the consequences of ignoring this message in the context of modern society. This will help emphasize the importance of the Little Prince\\'s wisdom and its relevance to contemporary issues.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\nThis paragraph effectively highlights the story\\'s critique of modern society. To deepen your analysis, explore how the themes of materialism, consumerism, and the pursuit of power interconnect and contribute to the challenges faced by modern society. Additionally, consider discussing potential solutions or actions inspired by the Little Prince\\'s message that could help address these issues.\\n\\nConclusion:\\nThe conclusion effectively summarizes the main points of the essay and emphasizes the relevance of \"The Little Prince\" in modern life. To further enhance this section, consider incorporating a thought-provoking question or statement that encourages readers to reflect on the story\\'s message and its implications for their own lives.\\n\\nRecommendations:\\n1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\\n2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader.\\n3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\\n4. Consider discussing counterarguments or potential criticisms of the Little Prince\\'s message to add depth and complexity to your essay.\\n5. Revise and edit the essay for clarity, coherence, and grammar.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.')}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nIn Antoine de Saint-Exupéry\\'s classic novella \"The Little Prince,\" a young boy embarks on a journey through the universe, meeting various characters that symbolize different aspects of adult life. This timeless tale, published in 1943, remains incredibly relevant in today\\'s modern world. Its allegorical nature, thought-provoking message, and critique of modern society offer invaluable insights for readers of all ages. This essay will explore the allegory of \"The Little Prince,\" analyze the relevance of its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction. By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today\\'s world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society. As the Little Prince so eloquently states, \"What is essential is invisible to the eye,\" reminding us that true happiness and fulfillment come from understanding and empathizing with others.\\n\\nExpanded Essay Recommendations:\\n\\n1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\\n2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader. For instance, use quotes like, \"You become responsible, forever, for what you have tamed,\" to emphasize the importance of forming genuine connections.\\n3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\\n4. Consider discussing counterarguments or potential criticisms of the Little Prince\\'s message to add depth and complexity to your essay. For example, explore the idea that the pursuit of material possessions can provide a sense of security and comfort.\\n5. Revise and edit the essay for clarity, coherence, and grammar. Ensure that transitions between paragraphs are smooth and that your arguments flow logically.', response_metadata={'token_usage': {'prompt_tokens': 1168, 'total_tokens': 2044, 'completion_tokens': 876}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-9bfc9ff2-3186-43f5-8b75-498d532d8d1a-0', usage_metadata={'input_tokens': 1168, 'output_tokens': 876, 'total_tokens': 2044})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Your revised essay demonstrates a clear understanding of the assignment and the source material. Here are some additional recommendations to further enhance your essay:\\n\\n1. Consider adding more nuance to your analysis of the allegory in Body Paragraph 1. You could explore how the Little Prince himself evolves throughout the story, representing not just innocence and curiosity, but also the capacity for growth and self-discovery.\\n\\n2. In Body Paragraph 2, you could delve deeper into the psychological consequences of neglecting genuine relationships. Research has shown that loneliness and social isolation can have significant impacts on mental and physical health. Incorporating these findings would strengthen your argument about the importance of the Little Prince\\'s message.\\n\\n3. For Body Paragraph 3, you could provide specific examples of how materialism and consumerism contribute to wealth inequality and environmental degradation. This would make your critique of modern society more concrete and compelling.\\n\\n4. In your conclusion, you could discuss how the Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships. This would demonstrate the wide-ranging relevance of the story and inspire readers to reflect on its implications for their own lives.\\n\\n5. Throughout the essay, make sure to cite secondary sources to support your analysis. This will add credibility to your arguments and demonstrate your engagement with existing scholarship on \"The Little Prince.\"\\n\\n6. Finally, proofread your essay carefully to ensure that it is free of grammatical errors and awkward phrasing. Consider asking a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nOne scene that highlights the emotional impact of loneliness is the Little Prince\\'s relationship with his rose, which illustrates the often-complex nature of human relationships. The prince\\'s devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\\n\\nStudies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel\\'s focus on personal growth and responsibility provides valuable insights for today\\'s 21st-century society. The novel challenges readers to question the adult world\\'s superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \\n\\nResearch suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry\\'s prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel\\'s messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.')}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\\n\\nTo further enhance your essay, consider incorporating secondary sources to support your analysis, and proofread your work carefully to ensure that it is free of grammatical errors and awkward phrasing. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.', response_metadata={'token_usage': {'prompt_tokens': 2419, 'total_tokens': 3164, 'completion_tokens': 745}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-eabbd349-2b3a-4bcf-a89b-716b25471846-0', usage_metadata={'input_tokens': 2419, 'output_tokens': 745, 'total_tokens': 3164})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Thank you for the feedback and recommendations. I have incorporated some of the suggestions to further enhance the essay:\\n\\nTitle: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. Research has shown that loneliness and social isolation can increase the risk of depression, anxiety, and heart disease (Holt-Lunstad, 2015). By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities (Jackson, 2017). To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\\n\\nReferences:\\nHolt-Lunstad, J. (2015). The Loneliness Paradox. American Psychological Association.\\nJackson, T. (2017). Prosperity without Growth: Economics for a Finite Planet. Routledge.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel\\'s powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince addresses themes of love and loneliness that still resonate strongly in today\\'s world. The novel\\'s portrayal of the prince\\'s relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\\n\\nOne notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince\\'s devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThe Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel\\'s focus on personal growth and self-awareness offers valuable insights in the face of today\\'s challenges.\\n\\nIn response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\\n\\nResearch suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince\\'s assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\\n\\nConclusion:\\nThe Little Prince remains a timeless and relevant work in the 21st century. The novel\\'s exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today\\'s world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.')}\n", - "---\n", - "{'__end__': [HumanMessage(content='Generate an essay on the topicality of The Little Prince and its message in modern life'), AIMessage(content=\"Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\\n\\nBody Paragraph 1 - Love and Loneliness:\\nOne of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThroughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nCritics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\\n\\nConclusion:\\nUltimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\\n\\nRevised Essay:\\n\\nTitle: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\\n\\nBody Paragraph 1 - Love, Loss, and Friendship:\\nThe Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThroughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\\n\\nBody Paragraph 3 - Rejecting the Superficiality of the Adult World:\\nThe Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\\n\\nConclusion:\\nThe Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\"), HumanMessage(content=\"Introduction:\\nThe essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\\n\\nBody Paragraph 1 - Love and Loneliness:\\nThe paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThis paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nThe criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\\n\\nConclusion:\\nThe conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\\n\\nOverall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.'), HumanMessage(content=\"The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nOne scene that highlights the emotional impact of loneliness is the Little Prince\\'s relationship with his rose, which illustrates the often-complex nature of human relationships. The prince\\'s devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\\n\\nStudies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel\\'s focus on personal growth and responsibility provides valuable insights for today\\'s 21st-century society. The novel challenges readers to question the adult world\\'s superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \\n\\nResearch suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry\\'s prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel\\'s messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.'), HumanMessage(content=\"The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel\\'s powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince addresses themes of love and loneliness that still resonate strongly in today\\'s world. The novel\\'s portrayal of the prince\\'s relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\\n\\nOne notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince\\'s devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThe Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel\\'s focus on personal growth and self-awareness offers valuable insights in the face of today\\'s challenges.\\n\\nIn response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\\n\\nResearch suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince\\'s assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\\n\\nConclusion:\\nThe Little Prince remains a timeless and relevant work in the 21st century. The novel\\'s exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today\\'s world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.')]}\n", + "{'generate': {'messages': [AIMessage(content='Your revised essay demonstrates a clear understanding of the assignment and the source material, and you have effectively incorporated the suggestions provided. The addition of research findings and specific examples has strengthened your argument and added credibility to your analysis. Your essay now provides a more nuanced exploration of the allegory, the relevance of the Little Prince\\'s message, and the critique of modern society.\\n\\nHere are some final recommendations to further enhance your essay:\\n\\n1. Ensure that your essay adheres to the required citation style (e.g., MLA, APA, or Chicago) and that all in-text citations and references are formatted correctly.\\n2. Double-check your essay for any grammatical errors, awkward phrasing, or unclear sentences. A well-written essay is not only easier to read but also more persuasive and engaging.\\n3. Consider adding a brief introduction to each body paragraph to provide context and guide the reader through your analysis. This will help ensure that your essay flows logically and that your arguments are easy to follow.\\n4. As a final step, ask a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\\n\\nOverall, your essay provides a thoughtful and engaging exploration of \"The Little Prince\" and its relevance in modern life. By incorporating the recommendations provided, you can further enhance your analysis and create a truly exceptional piece of writing.', response_metadata={'token_usage': {'prompt_tokens': 4034, 'total_tokens': 4354, 'completion_tokens': 320}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-9c805bb5-01f4-4461-acf8-509f7440d31d-0', usage_metadata={'input_tokens': 4034, 'output_tokens': 320, 'total_tokens': 4354})]}}\n", "---\n" ] } ], "source": [ - "async for event in graph.astream(\n", - " [\n", + "async for event in graph.astream({\n", + " \"messages\": [\n", " HumanMessage(\n", " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", " )\n", " ],\n", - "):\n", + "}, config):\n", " print(event)\n", " print(\"---\")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, + "id": "ced83251-8edc-483d-a03f-5bd884ea8d28", + "metadata": {}, + "outputs": [], + "source": [ + "state = graph.get_state(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, "id": "394bf0df-fc28-4104-a278-a56c9cb8b10c", "metadata": {}, "outputs": [ @@ -361,161 +439,170 @@ "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a classic novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince\" and discuss how its message can be applied to the complexities of the modern world.\n", "\n", - "Body Paragraph 1 - Love and Loneliness:\n", - "One of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that explores various aspects of the human condition through its whimsical characters and situations. The Little Prince himself represents innocence, curiosity, and the importance of human connection. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "Body Paragraph 2 - Responsibility and Self-Discovery:\n", - "Throughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. The Little Prince encourages readers to cherish and nurture genuine relationships, reminding us that true happiness and fulfillment come from understanding and empathizing with others.\n", "\n", - "Body Paragraph 3 - The Superficiality of the Adult World:\n", - "Critics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" also offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today's world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet.\n", "\n", "Conclusion:\n", - "Ultimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\n", - "\n", - "Revised Essay:\n", - "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", - "\n", - "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\n", - "\n", - "Body Paragraph 1 - Love, Loss, and Friendship:\n", - "The Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\n", - "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Throughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\n", - "\n", - "Body Paragraph 3 - Rejecting the Superficiality of the Adult World:\n", - "The Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\n", - "\n", - "Conclusion:\n", - "The Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", + "Essay Critique and Recommendations:\n", + "\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", + "\n", "Introduction:\n", - "The essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\n", + "The introduction effectively sets the stage for the essay by providing background information on \"The Little Prince\" and its relevance in modern life. However, consider adding a hook to engage the reader's attention and create a stronger first impression.\n", "\n", - "Body Paragraph 1 - Love and Loneliness:\n", - "The paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "This paragraph provides a clear explanation of the allegorical nature of \"The Little Prince.\" To enhance this section, consider offering specific examples from the text to illustrate how the characters and situations symbolize various aspects of adult life. This will strengthen your analysis and make it more engaging for the reader.\n", "\n", - "Body Paragraph 2 - Responsibility and Self-Discovery:\n", - "This paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The relevance of the Little Prince's message is well-articulated in this paragraph. To further strengthen your argument, consider discussing the consequences of ignoring this message in the context of modern society. This will help emphasize the importance of the Little Prince's wisdom and its relevance to contemporary issues.\n", "\n", - "Body Paragraph 3 - The Superficiality of the Adult World:\n", - "The criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "This paragraph effectively highlights the story's critique of modern society. To deepen your analysis, explore how the themes of materialism, consumerism, and the pursuit of power interconnect and contribute to the challenges faced by modern society. Additionally, consider discussing potential solutions or actions inspired by the Little Prince's message that could help address these issues.\n", "\n", "Conclusion:\n", - "The conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\n", + "The conclusion effectively summarizes the main points of the essay and emphasizes the relevance of \"The Little Prince\" in modern life. To further enhance this section, consider incorporating a thought-provoking question or statement that encourages readers to reflect on the story's message and its implications for their own lives.\n", "\n", - "Overall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\n", + "Recommendations:\n", + "1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\n", + "2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader.\n", + "3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\n", + "4. Consider discussing counterarguments or potential criticisms of the Little Prince's message to add depth and complexity to your essay.\n", + "5. Revise and edit the essay for clarity, coherence, and grammar.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today's society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel's powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "In Antoine de Saint-Exupéry's classic novella \"The Little Prince,\" a young boy embarks on a journey through the universe, meeting various characters that symbolize different aspects of adult life. This timeless tale, published in 1943, remains incredibly relevant in today's modern world. Its allegorical nature, thought-provoking message, and critique of modern society offer invaluable insights for readers of all ages. This essay will explore the allegory of \"The Little Prince,\" analyze the relevance of its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince's depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince's portrayal of the prince's loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Personal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry's exploration of self-awareness and personal growth is highly relevant. The Little Prince's encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction. By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today's digital age and social media-dominated society. For instance, the novel's third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today's world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", "\n", "Conclusion:\n", - "The Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel's exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel's wisdom and the importance of its messages in our daily lives.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society. As the Little Prince so eloquently states, \"What is essential is invisible to the eye,\" reminding us that true happiness and fulfillment come from understanding and empathizing with others.\n", + "\n", + "Expanded Essay Recommendations:\n", + "\n", + "1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\n", + "2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader. For instance, use quotes like, \"You become responsible, forever, for what you have tamed,\" to emphasize the importance of forming genuine connections.\n", + "3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\n", + "4. Consider discussing counterarguments or potential criticisms of the Little Prince's message to add depth and complexity to your essay. For example, explore the idea that the pursuit of material possessions can provide a sense of security and comfort.\n", + "5. Revise and edit the essay for clarity, coherence, and grammar. Ensure that transitions between paragraphs are smooth and that your arguments flow logically.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\n", + "Your revised essay demonstrates a clear understanding of the assignment and the source material. Here are some additional recommendations to further enhance your essay:\n", + "\n", + "1. Consider adding more nuance to your analysis of the allegory in Body Paragraph 1. You could explore how the Little Prince himself evolves throughout the story, representing not just innocence and curiosity, but also the capacity for growth and self-discovery.\n", + "\n", + "2. In Body Paragraph 2, you could delve deeper into the psychological consequences of neglecting genuine relationships. Research has shown that loneliness and social isolation can have significant impacts on mental and physical health. Incorporating these findings would strengthen your argument about the importance of the Little Prince's message.\n", + "\n", + "3. For Body Paragraph 3, you could provide specific examples of how materialism and consumerism contribute to wealth inequality and environmental degradation. This would make your critique of modern society more concrete and compelling.\n", + "\n", + "4. In your conclusion, you could discuss how the Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships. This would demonstrate the wide-ranging relevance of the story and inspire readers to reflect on its implications for their own lives.\n", + "\n", + "5. Throughout the essay, make sure to cite secondary sources to support your analysis. This will add credibility to your arguments and demonstrate your engagement with existing scholarship on \"The Little Prince.\"\n", + "\n", + "6. Finally, proofread your essay carefully to ensure that it is free of grammatical errors and awkward phrasing. Consider asking a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today's society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel's powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince's depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince's portrayal of the prince's loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "One scene that highlights the emotional impact of loneliness is the Little Prince's relationship with his rose, which illustrates the often-complex nature of human relationships. The prince's devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Personal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry's exploration of self-awareness and personal growth is highly relevant. The Little Prince's encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\n", - "\n", - "Studies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel's focus on personal growth and responsibility provides valuable insights for today's 21st-century society. The novel challenges readers to question the adult world's superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\n", - "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today's digital age and social media-dominated society. For instance, the novel's third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \n", - "\n", - "Research suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry's prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel's messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", "\n", "Conclusion:\n", - "The Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel's exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel's wisdom and the importance of its messages in our daily lives.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", + "\n", + "To further enhance your essay, consider incorporating secondary sources to support your analysis, and proofread your work carefully to ensure that it is free of grammatical errors and awkward phrasing. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\n", + "Thank you for the feedback and recommendations. I have incorporated some of the suggestions to further enhance the essay:\n", + "\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", + "\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", + "\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", + "\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. Research has shown that loneliness and social isolation can increase the risk of depression, anxiety, and heart disease (Holt-Lunstad, 2015). By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", + "\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities (Jackson, 2017). To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", + "\n", + "Conclusion:\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", + "\n", + "References:\n", + "Holt-Lunstad, J. (2015). The Loneliness Paradox. American Psychological Association.\n", + "Jackson, T. (2017). Prosperity without Growth: Economics for a Finite Planet. Routledge.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Your revised essay demonstrates a clear understanding of the assignment and the source material, and you have effectively incorporated the suggestions provided. The addition of research findings and specific examples has strengthened your argument and added credibility to your analysis. Your essay now provides a more nuanced exploration of the allegory, the relevance of the Little Prince's message, and the critique of modern society.\n", "\n", - "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel's powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "Here are some final recommendations to further enhance your essay:\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince addresses themes of love and loneliness that still resonate strongly in today's world. The novel's portrayal of the prince's relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\n", + "1. Ensure that your essay adheres to the required citation style (e.g., MLA, APA, or Chicago) and that all in-text citations and references are formatted correctly.\n", + "2. Double-check your essay for any grammatical errors, awkward phrasing, or unclear sentences. A well-written essay is not only easier to read but also more persuasive and engaging.\n", + "3. Consider adding a brief introduction to each body paragraph to provide context and guide the reader through your analysis. This will help ensure that your essay flows logically and that your arguments are easy to follow.\n", + "4. As a final step, ask a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", - "One notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince's devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\n", - "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "The Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel's focus on personal growth and self-awareness offers valuable insights in the face of today's challenges.\n", - "\n", - "In response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\n", - "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\n", - "\n", - "Research suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince's assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\n", - "\n", - "Conclusion:\n", - "The Little Prince remains a timeless and relevant work in the 21st century. The novel's exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today's world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.\n" + "Overall, your essay provides a thoughtful and engaging exploration of \"The Little Prince\" and its relevance in modern life. By incorporating the recommendations provided, you can further enhance your analysis and create a truly exceptional piece of writing.\n" ] } ], "source": [ - "ChatPromptTemplate.from_messages(event[END]).pretty_print()" + "ChatPromptTemplate.from_messages(state.values[\"messages\"]).pretty_print()" ] }, { "cell_type": "markdown", "id": "0fa62df2-e8ee-40dd-ac95-9d982eae6079", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Conclusion\n", "\n", "Now that you've applied reflection to an LLM agent, I'll note one thing: self-reflection is inherently cyclic: it is much more effective if the reflection step has additional context or feedback (from tool observations, checks, etc.). If, like in the scenario above, the reflection step simply prompts the LLM to reflect on its output, it can still benefit the output quality (since the LLM then has multiple \"shots\" at getting a good output), but it's less guaranteed.\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c0e3efd-7f54-410e-bd31-36185a46b9a8", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -534,7 +621,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/reflexion/reflexion.ipynb b/examples/reflexion/reflexion.ipynb index 670e6eb5b..5a2443513 100644 --- a/examples/reflexion/reflexion.ipynb +++ b/examples/reflexion/reflexion.ipynb @@ -392,6 +392,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "MAX_ITERATIONS = 5\n", "builder = StateGraph(State)\n", "builder.add_node(\"draft\", first_responder.respond)\n", @@ -572,7 +573,7 @@ ], "source": [ "events = graph.stream(\n", - " [HumanMessage(content=\"How should we handle the climate crisis?\")],\n", + " {\"messages\": [(\"user\", \"How should we handle the climate crisis?\")]},\n", " stream_mode=\"values\",\n", ")\n", "for i, step in enumerate(events):\n", diff --git a/examples/self-discover/self-discover.ipynb b/examples/self-discover/self-discover.ipynb index 9c4dbfeef..2ac5ca8fa 100644 --- a/examples/self-discover/self-discover.ipynb +++ b/examples/self-discover/self-discover.ipynb @@ -246,14 +246,6 @@ "):\n", " print(s)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20cac598", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/state-context-key.ipynb b/examples/state-context-key.ipynb index 3b6191fe6..e487106b2 100644 --- a/examples/state-context-key.ipynb +++ b/examples/state-context-key.ipynb @@ -465,14 +465,6 @@ "for chunk in app.stream(inputs, stream_mode=\"values\"):\n", " chunk[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "296c7456-da05-4326-95dc-47d6b312da9d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/state-model.ipynb b/examples/state-model.ipynb index 897c3c571..9487c9801 100644 --- a/examples/state-model.ipynb +++ b/examples/state-model.ipynb @@ -439,14 +439,6 @@ "for chunk in app.stream(inputs, stream_mode=\"values\"):\n", " chunk[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "296c7456-da05-4326-95dc-47d6b312da9d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb index 67e56dbf7..7f9ee3985 100644 --- a/examples/storm/storm.ipynb +++ b/examples/storm/storm.ipynb @@ -48,7 +48,10 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python" + "%%capture --no-stderr\n", + "%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n", + "# We use one or the other search engine below\n", + "%pip install -U duckduckgo tavily-python" ] }, { @@ -57,7 +60,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" + "# Uncomment if you want to draw the pretty graph diagrams.\n", + "# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n", + "# ! brew install graphviz\n", + "# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" ] }, { @@ -66,7 +72,21 @@ "metadata": {}, "outputs": [], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if os.environ.get(var):\n", + " return\n", + " os.environ[var] = getpass.getpass(var + \":\")\n", + "\n", + "\n", + "# Set for tracing\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -84,7 +104,12 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" + "from langchain_openai import ChatOpenAI\n", + "\n", + "fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", + "# Uncomment for a Fireworks model\n", + "# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n", + "long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" ] }, { @@ -112,7 +137,64 @@ } ], "source": [ - "from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)" + "from typing import List, Optional\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n", + " ),\n", + " (\"user\", \"{topic}\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "class Subsection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " description: str = Field(..., title=\"Content of the subsection\")\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n", + "\n", + "\n", + "class Section(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " description: str = Field(..., title=\"Content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n", + " for subsection in self.subsections or []\n", + " )\n", + " return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n", + "\n", + "\n", + "class Outline(BaseModel):\n", + " page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n", + " sections: List[Section] = Field(\n", + " default_factory=list,\n", + " title=\"Titles and descriptions for each section of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n", + " return f\"# {self.page_title}\\n\\n{sections}\".strip()\n", + "\n", + "\n", + "generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -145,7 +227,11 @@ } ], "source": [ - "example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)" + "example_topic = \"Impact of million-plus token context window language models on RAG\"\n", + "\n", + "initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n", + "\n", + "print(initial_outline.as_str)" ] }, { @@ -165,7 +251,25 @@ "metadata": {}, "outputs": [], "source": [ - "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)" + "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n", + " \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n", + "\n", + "Please list the as many subjects and urls as you can.\n", + "\n", + "Topic of interest: {topic}\n", + "\"\"\"\n", + ")\n", + "\n", + "\n", + "class RelatedSubjects(BaseModel):\n", + " topics: List[str] = Field(\n", + " description=\"Comprehensive list of related subjects as background research.\",\n", + " )\n", + "\n", + "\n", + "expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n", + " RelatedSubjects\n", + ")" ] }, { @@ -185,7 +289,8 @@ } ], "source": [ - "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects" + "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n", + "related_subjects" ] }, { @@ -204,7 +309,49 @@ "metadata": {}, "outputs": [], "source": [ - "class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)" + "class Editor(BaseModel):\n", + " affiliation: str = Field(\n", + " description=\"Primary affiliation of the editor.\",\n", + " )\n", + " name: str = Field(\n", + " description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n", + " )\n", + " role: str = Field(\n", + " description=\"Role of the editor in the context of the topic.\",\n", + " )\n", + " description: str = Field(\n", + " description=\"Description of the editor's focus, concerns, and motives.\",\n", + " )\n", + "\n", + " @property\n", + " def persona(self) -> str:\n", + " return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n", + "\n", + "\n", + "class Perspectives(BaseModel):\n", + " editors: List[Editor] = Field(\n", + " description=\"Comprehensive list of editors with their roles and affiliations.\",\n", + " # Add a pydantic validation/restriction to be at most M editors\n", + " )\n", + "\n", + "\n", + "gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n", + " You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n", + "\n", + " Wiki page outlines of related topics for inspiration:\n", + " {examples}\"\"\",\n", + " ),\n", + " (\"user\", \"Topic of interest: {topic}\"),\n", + " ]\n", + ")\n", + "\n", + "gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Perspectives)" ] }, { @@ -213,7 +360,37 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" + "from langchain_community.retrievers import WikipediaRetriever\n", + "from langchain_core.runnables import RunnableLambda\n", + "from langchain_core.runnables import chain as as_runnable\n", + "\n", + "wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n", + "\n", + "\n", + "def format_doc(doc, max_length=1000):\n", + " related = \"- \".join(doc.metadata[\"categories\"])\n", + " return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n", + " :max_length\n", + " ]\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n", + "\n", + "\n", + "@as_runnable\n", + "async def survey_subjects(topic: str):\n", + " related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n", + " retrieved_docs = await wikipedia_retriever.abatch(\n", + " related_subjects.topics, return_exceptions=True\n", + " )\n", + " all_docs = []\n", + " for docs in retrieved_docs:\n", + " if isinstance(docs, BaseException):\n", + " continue\n", + " all_docs.extend(docs)\n", + " formatted = format_docs(all_docs)\n", + " return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" ] }, { @@ -280,7 +457,40 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]" + "from typing import Annotated\n", + "\n", + "from langchain_core.messages import AnyMessage\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "\n", + "def add_messages(left, right):\n", + " if not isinstance(left, list):\n", + " left = [left]\n", + " if not isinstance(right, list):\n", + " right = [right]\n", + " return left + right\n", + "\n", + "\n", + "def update_references(references, new_references):\n", + " if not references:\n", + " references = {}\n", + " references.update(new_references)\n", + " return references\n", + "\n", + "\n", + "def update_editor(editor, new_editor):\n", + " # Can only set at the outset\n", + " if not editor:\n", + " return new_editor\n", + " return editor\n", + "\n", + "\n", + "class InterviewState(TypedDict):\n", + " messages: Annotated[List[AnyMessage], add_messages]\n", + " references: Annotated[Optional[dict], update_references]\n", + " editor: Annotated[Optional[Editor], update_editor]" ] }, { @@ -298,7 +508,56 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}" + "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", + "from langchain_core.prompts import MessagesPlaceholder\n", + "\n", + "gen_qn_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n", + "Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n", + "Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n", + "\n", + "When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n", + "Please only ask one question at a time and don't ask what you have asked before.\\\n", + "Your questions should be related to the topic you want to write.\n", + "Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n", + "\n", + "Stay true to your specific perspective:\n", + "\n", + "{persona}\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "\n", + "def tag_with_name(ai_message: AIMessage, name: str):\n", + " ai_message.name = name\n", + " return ai_message\n", + "\n", + "\n", + "def swap_roles(state: InterviewState, name: str):\n", + " converted = []\n", + " for message in state[\"messages\"]:\n", + " if isinstance(message, AIMessage) and message.name != name:\n", + " message = HumanMessage(**message.dict(exclude={\"type\"}))\n", + " converted.append(message)\n", + " return {\"messages\": converted}\n", + "\n", + "\n", + "@as_runnable\n", + "async def generate_question(state: InterviewState):\n", + " editor = state[\"editor\"]\n", + " gn_chain = (\n", + " RunnableLambda(swap_roles).bind(name=editor.name)\n", + " | gen_qn_prompt.partial(persona=editor.persona)\n", + " | fast_llm\n", + " | RunnableLambda(tag_with_name).bind(name=editor.name)\n", + " )\n", + " result = await gn_chain.ainvoke(state)\n", + " return {\"messages\": [result]}" ] }, { @@ -318,7 +577,17 @@ } ], "source": [ - "messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content" + "messages = [\n", + " HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n", + "]\n", + "question = await generate_question.ainvoke(\n", + " {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": messages,\n", + " }\n", + ")\n", + "\n", + "question[\"messages\"][0].content" ] }, { @@ -336,7 +605,24 @@ "metadata": {}, "outputs": [], "source": [ - "class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)" + "class Queries(BaseModel):\n", + " queries: List[str] = Field(\n", + " description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n", + " )\n", + "\n", + "\n", + "gen_queries_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Queries, include_raw=True)" ] }, { @@ -357,7 +643,10 @@ } ], "source": [ - "queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries" + "queries = await gen_queries_chain.ainvoke(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "queries[\"parsed\"].queries" ] }, { @@ -366,7 +655,38 @@ "metadata": {}, "outputs": [], "source": [ - "class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")" + "class AnswerWithCitations(BaseModel):\n", + " answer: str = Field(\n", + " description=\"Comprehensive answer to the user's question with citations.\",\n", + " )\n", + " cited_urls: List[str] = Field(\n", + " description=\"List of urls cited in the answer.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n", + " f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n", + " )\n", + "\n", + "\n", + "gen_answer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n", + " to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n", + "\n", + "Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n", + "Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n", + " AnswerWithCitations, include_raw=True\n", + ").with_config(run_name=\"GenerateAnswer\")" ] }, { @@ -375,7 +695,29 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" + "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n", + "from langchain_core.tools import tool\n", + "\n", + "'''\n", + "# Tavily is typically a better search engine, but your free queries are limited\n", + "search_engine = TavilySearchResults(max_results=4)\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = tavily_search.invoke(query)\n", + " return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n", + "'''\n", + "\n", + "# DDG\n", + "search_engine = DuckDuckGoSearchAPIWrapper()\n", + "\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n", + " return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" ] }, { @@ -384,7 +726,43 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}" + "import json\n", + "\n", + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "\n", + "async def gen_answer(\n", + " state: InterviewState,\n", + " config: Optional[RunnableConfig] = None,\n", + " name: str = \"Subject_Matter_Expert\",\n", + " max_str_len: int = 15000,\n", + "):\n", + " swapped_state = swap_roles(state, name) # Convert all other AI messages\n", + " queries = await gen_queries_chain.ainvoke(swapped_state)\n", + " query_results = await search_engine.abatch(\n", + " queries[\"parsed\"].queries, config, return_exceptions=True\n", + " )\n", + " successful_results = [\n", + " res for res in query_results if not isinstance(res, Exception)\n", + " ]\n", + " all_query_results = {\n", + " res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n", + " }\n", + " # We could be more precise about handling max token length if we wanted to here\n", + " dumped = json.dumps(all_query_results)[:max_str_len]\n", + " ai_message: AIMessage = queries[\"raw\"]\n", + " tool_call = queries[\"raw\"].tool_calls[0]\n", + " tool_id = tool_call[\"id\"]\n", + " tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n", + " swapped_state[\"messages\"].extend([ai_message, tool_message])\n", + " # Only update the shared state with the final answer to avoid\n", + " # polluting the dialogue history with intermediate messages\n", + " generated = await gen_answer_chain.ainvoke(swapped_state)\n", + " cited_urls = set(generated[\"parsed\"].cited_urls)\n", + " # Save the retrieved information to a the shared state for future reference\n", + " cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n", + " formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n", + " return {\"messages\": [formatted_message], \"references\": cited_references}" ] }, { @@ -404,7 +782,10 @@ } ], "source": [ - "example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content" + "example_answer = await gen_answer(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "example_answer[\"messages\"][-1].content" ] }, { @@ -423,7 +804,31 @@ "metadata": {}, "outputs": [], "source": [ - "max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" + "max_num_turns = 5\n", + "\n", + "\n", + "def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n", + " messages = state[\"messages\"]\n", + " num_responses = len(\n", + " [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n", + " )\n", + " if num_responses >= max_num_turns:\n", + " return END\n", + " last_question = messages[-2]\n", + " if last_question.content.endswith(\"Thank you so much for your help!\"):\n", + " return END\n", + " return \"ask_question\"\n", + "\n", + "\n", + "builder = StateGraph(InterviewState)\n", + "\n", + "builder.add_node(\"ask_question\", generate_question)\n", + "builder.add_node(\"answer_question\", gen_answer)\n", + "builder.add_conditional_edges(\"answer_question\", route_messages)\n", + "builder.add_edge(\"ask_question\", \"answer_question\")\n", + "\n", + "builder.add_edge(START, \"ask_question\")\n", + "interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" ] }, { @@ -444,7 +849,11 @@ } ], "source": [ - "from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())" + "from IPython.display import Image\n", + "\n", + "# Feel free to comment out if you have\n", + "# not installed pygraphviz\n", + "Image(interview_graph.get_graph().draw_png())" ] }, { @@ -474,7 +883,23 @@ } ], "source": [ - "final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step" + "final_step = None\n", + "\n", + "initial_state = {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {example_topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + "}\n", + "async for step in interview_graph.astream(initial_state):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name][\"messages\"])[:300])\n", + " if END in step:\n", + " final_step = step" ] }, { @@ -501,7 +926,29 @@ "metadata": {}, "outputs": [], "source": [ - "refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)" + "refine_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n", + "You need to make sure that the outline is comprehensive and specific. \\\n", + "Topic you are writing about: {topic} \n", + "\n", + "Old outline:\n", + "\n", + "{old_outline}\"\"\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "# Using turbo preview since the context can get quite long\n", + "refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -510,7 +957,15 @@ "metadata": {}, "outputs": [], "source": [ - "refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)" + "refined_outline = refine_outline_chain.invoke(\n", + " {\n", + " \"topic\": example_topic,\n", + " \"old_outline\": initial_outline.as_str,\n", + " \"conversations\": \"\\n\\n\".join(\n", + " f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n", + " ),\n", + " }\n", + ")" ] }, { @@ -595,7 +1050,23 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)" + "from langchain_community.vectorstores import SKLearnVectorStore\n", + "from langchain_core.documents import Document\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n", + "reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in final_state[\"references\"].items()\n", + "]\n", + "# This really doesn't need to be a vectorstore for this size of data.\n", + "# It could just be a numpy matrix. Or you could store documents\n", + "# across requests if you want.\n", + "vectorstore = SKLearnVectorStore.from_documents(\n", + " reference_docs,\n", + " embedding=embeddings,\n", + ")\n", + "retriever = vectorstore.as_retriever(k=10)" ] }, { @@ -636,7 +1107,67 @@ "metadata": {}, "outputs": [], "source": [ - "class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)" + "class SubSection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " content: str = Field(\n", + " ...,\n", + " title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n", + "\n", + "\n", + "class WikiSection(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " content: str = Field(..., title=\"Full content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + " citations: List[str] = Field(default_factory=list)\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " subsection.as_str for subsection in self.subsections or []\n", + " )\n", + " citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n", + " return (\n", + " f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n", + " + f\"\\n\\n{citations}\".strip()\n", + " )\n", + "\n", + "\n", + "section_writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n", + " \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n", + " ),\n", + " (\"user\", \"Write the full WikiSection for the {section} section.\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "async def retrieve(inputs: dict):\n", + " docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n", + " formatted = \"\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )\n", + " return {\"docs\": formatted, **inputs}\n", + "\n", + "\n", + "section_writer = (\n", + " retrieve\n", + " | section_writer_prompt\n", + " | long_context_llm.with_structured_output(WikiSection)\n", + ")" ] }, { @@ -663,7 +1194,14 @@ } ], "source": [ - "section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)" + "section = await section_writer.ainvoke(\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": refined_outline.sections[1].section_title,\n", + " \"topic\": example_topic,\n", + " }\n", + ")\n", + "print(section.as_str)" ] }, { @@ -681,7 +1219,24 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()" + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n", + " \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n", + " \" avoiding duplicates in the footer. Include URLs in the footer.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "writer = writer_prompt | long_context_llm | StrOutputParser()" ] }, { @@ -774,7 +1329,8 @@ } ], "source": [ - "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")" + "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n", + " print(tok, end=\"\")" ] }, { @@ -801,7 +1357,14 @@ "metadata": {}, "outputs": [], "source": [ - "class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str" + "class ResearchState(TypedDict):\n", + " topic: str\n", + " outline: Outline\n", + " editors: List[Editor]\n", + " interview_results: List[InterviewState]\n", + " # The final sections output\n", + " sections: List[WikiSection]\n", + " article: str" ] }, { @@ -810,7 +1373,109 @@ "metadata": {}, "outputs": [], "source": [ - "import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }" + "import asyncio\n", + "\n", + "\n", + "async def initialize_research(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " coros = (\n", + " generate_outline_direct.ainvoke({\"topic\": topic}),\n", + " survey_subjects.ainvoke(topic),\n", + " )\n", + " results = await asyncio.gather(*coros)\n", + " return {\n", + " **state,\n", + " \"outline\": results[0],\n", + " \"editors\": results[1].editors,\n", + " }\n", + "\n", + "\n", + "async def conduct_interviews(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " initial_states = [\n", + " {\n", + " \"editor\": editor,\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + " }\n", + " for editor in state[\"editors\"]\n", + " ]\n", + " # We call in to the sub-graph here to parallelize the interviews\n", + " interview_results = await interview_graph.abatch(initial_states)\n", + "\n", + " return {\n", + " **state,\n", + " \"interview_results\": interview_results,\n", + " }\n", + "\n", + "\n", + "def format_conversation(interview_state):\n", + " messages = interview_state[\"messages\"]\n", + " convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n", + " return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n", + "\n", + "\n", + "async def refine_outline(state: ResearchState):\n", + " convos = \"\\n\\n\".join(\n", + " [\n", + " format_conversation(interview_state)\n", + " for interview_state in state[\"interview_results\"]\n", + " ]\n", + " )\n", + "\n", + " updated_outline = await refine_outline_chain.ainvoke(\n", + " {\n", + " \"topic\": state[\"topic\"],\n", + " \"old_outline\": state[\"outline\"].as_str,\n", + " \"conversations\": convos,\n", + " }\n", + " )\n", + " return {**state, \"outline\": updated_outline}\n", + "\n", + "\n", + "async def index_references(state: ResearchState):\n", + " all_docs = []\n", + " for interview_state in state[\"interview_results\"]:\n", + " reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in interview_state[\"references\"].items()\n", + " ]\n", + " all_docs.extend(reference_docs)\n", + " await vectorstore.aadd_documents(all_docs)\n", + " return state\n", + "\n", + "\n", + "async def write_sections(state: ResearchState):\n", + " outline = state[\"outline\"]\n", + " sections = await section_writer.abatch(\n", + " [\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": section.section_title,\n", + " \"topic\": state[\"topic\"],\n", + " }\n", + " for section in outline.sections\n", + " ]\n", + " )\n", + " return {\n", + " **state,\n", + " \"sections\": sections,\n", + " }\n", + "\n", + "\n", + "async def write_article(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " sections = state[\"sections\"]\n", + " draft = \"\\n\\n\".join([section.as_str for section in sections])\n", + " article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n", + " return {\n", + " **state,\n", + " \"article\": article,\n", + " }" ] }, { @@ -826,7 +1491,27 @@ "metadata": {}, "outputs": [], "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())" + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "builder_of_storm = StateGraph(ResearchState)\n", + "\n", + "nodes = [\n", + " (\"init_research\", initialize_research),\n", + " (\"conduct_interviews\", conduct_interviews),\n", + " (\"refine_outline\", refine_outline),\n", + " (\"index_references\", index_references),\n", + " (\"write_sections\", write_sections),\n", + " (\"write_article\", write_article),\n", + "]\n", + "for i in range(len(nodes)):\n", + " name, node = nodes[i]\n", + " builder_of_storm.add_node(name, node)\n", + " if i > 0:\n", + " builder_of_storm.add_edge(nodes[i - 1][0], name)\n", + "\n", + "builder_of_storm.add_edge(START, nodes[0][0])\n", + "builder_of_storm.add_edge(nodes[-1][0], END)\n", + "storm = builder_of_storm.compile(checkpointer=MemorySaver())" ] }, { @@ -877,7 +1562,16 @@ } ], "source": [ - "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])" + "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n", + "async for step in storm.astream(\n", + " {\n", + " \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n", + " },\n", + " config,\n", + "):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name])[:300])" ] }, { @@ -886,7 +1580,8 @@ "metadata": {}, "outputs": [], "source": [ - "checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]" + "checkpoint = storm.get_state(config)\n", + "article = checkpoint.values[\"article\"]" ] }, { @@ -967,16 +1662,10 @@ } ], "source": [ - "from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "" + "from IPython.display import Markdown\n", + "\n", + "# We will down-header the sections to create less confusion in this notebook\n", + "Markdown(article.replace(\"\\n#\", \"\\n##\"))" ] } ], diff --git a/examples/stream-multiple.ipynb b/examples/stream-multiple.ipynb index 1fa7e8533..79c14af1b 100644 --- a/examples/stream-multiple.ipynb +++ b/examples/stream-multiple.ipynb @@ -50,7 +50,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" @@ -175,14 +175,6 @@ " print(chunk)\n", " print(\"\\n\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8cc57240-243c-4d9a-a845-cb55ef973a59", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/stream-updates.ipynb b/examples/stream-updates.ipynb index 95d99ba4f..371883fc2 100644 --- a/examples/stream-updates.ipynb +++ b/examples/stream-updates.ipynb @@ -55,7 +55,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" @@ -151,14 +151,6 @@ " print(values)\n", " print(\"\\n\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8cc57240-243c-4d9a-a845-cb55ef973a59", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/streaming-content.ipynb b/examples/streaming-content.ipynb index ff182ca21..76117c029 100644 --- a/examples/streaming-content.ipynb +++ b/examples/streaming-content.ipynb @@ -68,7 +68,9 @@ " # It's completely optional, but useful if you have many functions with similar names\n", " gen = RunnableGenerator(my_generator).with_config(\n", " tags=[\"should_stream\"],\n", - " callbacks=config.get(\"callbacks\", []) # <-- Propagate callbacks (Python <= 3.10)\n", + " callbacks=config.get(\n", + " \"callbacks\", []\n", + " ), # <-- Propagate callbacks (Python <= 3.10)\n", " )\n", " async for message in gen.astream(state):\n", " messages.append(message)\n", @@ -118,14 +120,6 @@ " # So we only print non-empty content\n", " print(data, end=\"|\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "615cb9d2-bfa2-4f83-90b0-c6c2d1e6df95", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/streaming-events-from-within-tools-without-langchain.ipynb b/examples/streaming-events-from-within-tools-without-langchain.ipynb index e0906c837..ab139c602 100644 --- a/examples/streaming-events-from-within-tools-without-langchain.ipynb +++ b/examples/streaming-events-from-within-tools-without-langchain.ipynb @@ -30,10 +30,7 @@ "id": "47f79af8-58d8-4a48-8d9a-88823d88701f", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph openai"] }, { "cell_type": "code", @@ -49,18 +46,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -84,94 +70,7 @@ "id": "d59234f9-173e-469d-a725-c13e0979663e", "metadata": {}, "outputs": [], - "source": [ - "from openai import AsyncOpenAI\n", - "from langchain_core.language_models.chat_models import ChatGenerationChunk\n", - "from langchain_core.messages import AIMessageChunk\n", - "from langchain_core.runnables.config import (\n", - " ensure_config,\n", - " get_callback_manager_for_config,\n", - ")\n", - "\n", - "openai_client = AsyncOpenAI()\n", - "# define tool schema for openai tool calling\n", - "\n", - "tool = {\n", - " \"type\": \"function\",\n", - " \"function\": {\n", - " \"name\": \"get_items\",\n", - " \"description\": \"Use this tool to look up which items are in the given place.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\"place\": {\"type\": \"string\"}},\n", - " \"required\": [\"place\"],\n", - " },\n", - " },\n", - "}\n", - "\n", - "\n", - "async def call_model(state, config=None):\n", - " config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n", - " callback_manager = get_callback_manager_for_config(config)\n", - " messages = state[\"messages\"]\n", - "\n", - " llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n", - " response = await openai_client.chat.completions.create(\n", - " messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n", - " )\n", - "\n", - " response_content = \"\"\n", - " role = None\n", - "\n", - " tool_call_id = None\n", - " tool_call_function_name = None\n", - " tool_call_function_arguments = \"\"\n", - " async for chunk in response:\n", - " delta = chunk.choices[0].delta\n", - " if delta.role is not None:\n", - " role = delta.role\n", - "\n", - " if delta.content:\n", - " response_content += delta.content\n", - " llm_run_manager.on_llm_new_token(delta.content)\n", - "\n", - " if delta.tool_calls:\n", - " # note: for simplicity we're only handling a single tool call here\n", - " if delta.tool_calls[0].function.name is not None:\n", - " tool_call_function_name = delta.tool_calls[0].function.name\n", - " tool_call_id = delta.tool_calls[0].id\n", - "\n", - " # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n", - " tool_call_chunk = ChatGenerationChunk(\n", - " message=AIMessageChunk(\n", - " content=\"\",\n", - " additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n", - " )\n", - " )\n", - " llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n", - " tool_call_function_arguments += delta.tool_calls[0].function.arguments\n", - "\n", - " if tool_call_function_name is not None:\n", - " tool_calls = [\n", - " {\n", - " \"id\": tool_call_id,\n", - " \"function\": {\n", - " \"name\": tool_call_function_name,\n", - " \"arguments\": tool_call_function_arguments,\n", - " },\n", - " \"type\": \"function\",\n", - " }\n", - " ]\n", - " else:\n", - " tool_calls = None\n", - "\n", - " response_message = {\n", - " \"role\": role,\n", - " \"content\": response_content,\n", - " \"tool_calls\": tool_calls,\n", - " }\n", - " return {\"messages\": [response_message]}" - ] + "source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"] }, { "cell_type": "markdown", @@ -187,62 +86,7 @@ "id": "b90941d8-afe4-42ec-9262-9c3b87c3b1ec", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "from langchain_core.callbacks import adispatch_custom_event\n", - "\n", - "\n", - "async def get_items(place: str) -> str:\n", - " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", - "\n", - " # this can be replaced with any actual streaming logic that you might have\n", - " def stream(place: str):\n", - " if \"bed\" in place: # For under the bed\n", - " yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n", - " elif \"shelf\" in place: # For 'shelf'\n", - " yield from [\"books\", \"penciles\", \"pictures\"]\n", - " else: # if the agent decides to ask about a different place\n", - " yield \"cat snacks\"\n", - "\n", - " tokens = []\n", - " for token in stream(place):\n", - " await adispatch_custom_event(\n", - " # this will allow you to filter events by name\n", - " \"tool_call_token_stream\",\n", - " {\n", - " \"function_name\": \"get_items\",\n", - " \"arguments\": {\"place\": place},\n", - " \"tool_output_token\": token,\n", - " },\n", - " # this will allow you to filter events by tags\n", - " config={\"tags\": [\"tool_call\"]},\n", - " )\n", - " tokens.append(token)\n", - "\n", - " return \", \".join(tokens)\n", - "\n", - "\n", - "# define mapping to look up functions when running tools\n", - "function_name_to_function = {\"get_items\": get_items}\n", - "\n", - "\n", - "async def call_tools(state):\n", - " messages = state[\"messages\"]\n", - "\n", - " tool_call = messages[-1][\"tool_calls\"][0]\n", - " function_name = tool_call[\"function\"][\"name\"]\n", - " function_arguments = tool_call[\"function\"][\"arguments\"]\n", - " arguments = json.loads(function_arguments)\n", - "\n", - " function_response = await function_name_to_function[function_name](**arguments)\n", - " tool_message = {\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " \"role\": \"tool\",\n", - " \"name\": function_name,\n", - " \"content\": function_response,\n", - " }\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["import json\nfrom langchain_core.callbacks import adispatch_custom_event\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n\n # this can be replaced with any actual streaming logic that you might have\n def stream(place: str):\n if \"bed\" in place: # For under the bed\n yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n elif \"shelf\" in place: # For 'shelf'\n yield from [\"books\", \"penciles\", \"pictures\"]\n else: # if the agent decides to ask about a different place\n yield \"cat snacks\"\n\n tokens = []\n for token in stream(place):\n await adispatch_custom_event(\n # this will allow you to filter events by name\n \"tool_call_token_stream\",\n {\n \"function_name\": \"get_items\",\n \"arguments\": {\"place\": place},\n \"tool_output_token\": token,\n },\n # this will allow you to filter events by tags\n config={\"tags\": [\"tool_call\"]},\n )\n tokens.append(token)\n\n return \", \".join(tokens)\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -258,33 +102,7 @@ "id": "228260be-1f9a-4195-80e0-9604f8a5dba6", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Literal\n", - "\n", - "from langgraph.graph import StateGraph, END\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, operator.add]\n", - "\n", - "\n", - "def should_continue(state) -> Literal[\"tools\", END]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " if last_message[\"tool_calls\"]:\n", - " return \"tools\"\n", - " return END\n", - "\n", - "\n", - "workflow = StateGraph(State)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", - "workflow.add_node(\"tools\", call_tools)\n", - "workflow.add_conditional_edges(\"model\", should_continue)\n", - "workflow.add_edge(\"tools\", \"model\")\n", - "graph = workflow.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"] }, { "cell_type": "markdown", @@ -318,14 +136,7 @@ ] } ], - "source": [ - "async for event in graph.astream_events(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n", - "):\n", - " tags = event.get(\"tags\", [])\n", - " if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n", - " print(\"Tool token\", event[\"data\"][\"tool_output_token\"])" - ] + "source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"] } ], "metadata": { diff --git a/examples/streaming-from-final-node.ipynb b/examples/streaming-from-final-node.ipynb index ee7e14a4b..f7a8d8c66 100644 --- a/examples/streaming-from-final-node.ipynb +++ b/examples/streaming-from-final-node.ipynb @@ -13,7 +13,11 @@ "id": "964686a6-8fed-4360-84d2-958c48186008", "metadata": {}, "source": [ - "A common use case is streaming from an agent is to stream LLM tokens from inside the final node. This guide demonstrates how you can do this." + "A common use case is streaming from an agent is to stream LLM tokens from inside the final node. This guide demonstrates how you can do this.\n", + "\n", + "## Setup\n", + "\n", + "First let's install our required packages and set our environment variables." ] }, { @@ -23,7 +27,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langgraph langchain-openai" + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain-openai" ] }, { @@ -33,7 +38,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" @@ -41,7 +46,16 @@ } ], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -55,33 +69,42 @@ { "cell_type": "code", "execution_count": 3, - "id": "1d51c35c-dbf2-4c01-932d-c5d308ea37d2", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "0af37212-e592-484d-9194-35d53fa79678", + "id": "5e62618d-0e0c-483c-acd3-40a26e61894a", "metadata": {}, "outputs": [], "source": [ + "from typing import Literal\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.runnables import ConfigurableField\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.prebuilt import create_react_agent\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "\n", + "@tool\n", + "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", + " \"\"\"Use this to get weather information.\"\"\"\n", + " if city == \"nyc\":\n", + " return \"It might be cloudy in nyc\"\n", + " elif city == \"sf\":\n", + " return \"It's always sunny in sf\"\n", + " else:\n", + " raise AssertionError(\"Unknown city\")\n", + "\n", + "\n", + "tools = [get_weather]\n", + "model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "final_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "\n", + "model = model.bind_tools(tools)\n", + "# NOTE: this is where we're adding a tag that we'll can use later to filter the model stream events to only the model called in the final node.\n", + "# This is not necessary if you call a single LLM but might be important in case you call multiple models within the node and want to filter events\n", + "# from only one of them.\n", + "final_model = final_model.with_config(tags=[\"final_node\"])\n", "tool_node = ToolNode(tools=tools)" ] }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ac9d4f5b-655a-48f3-b514-a4a0815714a6", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage" - ] - }, { "cell_type": "markdown", "id": "9acef997-5dd6-4108-baf1-c4d6be3e4999", @@ -92,21 +115,18 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "3948c6b8-0317-4001-b699-32b25306a023", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import SystemMessage, HumanMessage" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "2efe9fb4-c6c2-4171-becd-d45bbf899209", + "execution_count": 4, + "id": "8c7339d2-1835-4b5a-a99c-a60e150280af", "metadata": {}, "outputs": [], "source": [ + "from typing import TypedDict, Annotated\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import MessagesState\n", + "from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage\n", + "\n", + "\n", "def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n", " messages = state[\"messages\"]\n", " last_message = messages[-1]\n", @@ -135,16 +155,8 @@ " )\n", " # overwrite the last AI message from the agent\n", " response.id = last_ai_message.id\n", - " return {\"messages\": [response]}" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b1a9a981-8629-4d25-a0e1-d666c3968b30", - "metadata": {}, - "outputs": [], - "source": [ + " return {\"messages\": [response]}\n", + "\n", "workflow = StateGraph(MessagesState)\n", "\n", "workflow.add_node(\"agent\", call_model)\n", @@ -159,38 +171,20 @@ ")\n", "\n", "workflow.add_edge(\"tools\", \"agent\")\n", - "workflow.add_edge(\"final\", END)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a7b0251f-dcee-49d6-8133-af50d4a55e22", - "metadata": {}, - "outputs": [], - "source": [ + "workflow.add_edge(\"final\", END)\n", + "\n", "app = workflow.compile()" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "f8b77e74-17e9-4fee-a164-4637013b55ff", - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import display, Image" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "98adcef2-1dec-4503-99cc-48613cdb7a85", + "execution_count": 5, + "id": "2ab6d079-ba06-48ba-abe5-e72df24407af", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAEuAL4DASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBQgCAwQBCf/EAFQQAAEEAQIDAgcIDgYHCQEAAAEAAgMEBQYRBxIhEzEIFiJBVZTRFBUXMlFhk7MJIzY3QlZxdHWBkbTS4TM4UlNydmJzgpKhscEkJkNERVSEhaKk/8QAGgEBAAIDAQAAAAAAAAAAAAAAAAMFAQIEBv/EADYRAAIBAQQHBQgCAgMAAAAAAAABAgMEETFREhMVIUFhkRRScdHwBSIygaGxweE0YlNjM7Lx/9oADAMBAAIRAxEAPwD9U0REAREQHCaaOvE+WV7Y42Auc952DQO8k+YLG+NWE9MUPWme1Yvip97LVv6JtfVOUQ8X8X6Np/QN9igtFop2WEZ1E3pNrdyu8zus9m7Qm77riw/GrCemKHrTPanjVhPTFD1pntVeeL+L9G0/oG+xPF/F+jaf0DfYuDatn7kuqOvZ39voWH41YT0xQ9aZ7U8asJ6YoetM9qrzxfxfo2n9A32J4v4v0bT+gb7E2rZ+5Lqhs7+30LD8asJ6YoetM9qeNWE9MUPWme1V54v4v0bT+gb7E8X8X6Np/QN9ibVs/cl1Q2d/b6Fh+NWE9MUPWme1PGrCemKHrTPaq88X8X6Np/QN9ieL+L9G0/oG+xNq2fuS6obO/t9Cw/GrCemKHrTPavfUuV78ImrTx2ITuBJE8Oaf1hVb4v4v0bT+gb7FIuEcTINNXI42Njjbk7gaxg2AHbO7gu2z2qlalLQTV12PM5rRZdRFSvvJsiIukrwiIgCIiAIiIAiIgCIiAi3FT72WrP0Ta+qcsAs/xU+9lqz9E2vqnKM379bF0bF27YiqU60bpprE7wyOJjRu5znHoAACST0ACp/a/wDw0fGX2gXfs7CR3ooQOOfDdxAHEHSxJ6ADNVv419i438OZ5WRx6/0vJI8hrWNzNYlxPcAOdeb1c8mWunHMxWnuONTWFXLWcHpnUN2jUhtSVsiasbK190DzG9kLnSg8xeCAHhm+x67ArF8LeNmV1Vwcoaty2kM2+8+vA8wY2rFJ7udIduaswTOPZgkbmQtIHU9xKw2i9EaorcWbGQp6Wl0Jpq1Dc9+Kwysdqpkp3lvYzwQMJ7J/xnPcWs332O56mP0dDcRG8DcJoexpeeE6es04bkdTLwxtz9GN7xLHE8PDog5ojJEnJuCW7rr0KeCu4cfHn65HNpTxd/Hh4FjTeERp2lobUmpb+OzOM8XLEVbKYm3Vay7WdIYwwlnOWuaRK1wLXEEb7bnosLrrj1nMFZ0WcfoXPiHM5l1GWC3BXbYmhbXdKOyabA5XPOxHabbCOQODTy7wCTgrqc6V4pUMZoavp2tqGbEWsXiqt2u5rBDKwTMeQ4Na/aMyHbdp59g5xCuDjZpvO5U6NzOnsYM1c09nGZGTGiwyB9iEwzQvDHvIaHDtQ4BxAOx6po0oyV2/58l+RpVJLfu+XPyLFpzutVIJnwSVnyMa8wTbc8ZI35XbEjcdx2JHzldygzeNOjKMccGd1Tp/T2ZYxvuvFXczVE1SXYF0b9n7bg9Oi5Hjnw3HfxB0sP8A7qt/GuTVzyOjTjmTdZThR9z1/wDSlz65yjuFzmN1JjIcliMhVymPn37K3SmbNFJsS08r2kg7EEdD3gqRcKPuev8A6UufXOV/7I3Kqny/JX2/fTXiTRERXpQBERAEREAREQBERAEREBFuKn3stWfom19U5R9zQ9pa4BzSNiD3FTzN4ivn8NexlsONW7A+vKGO2dyPaWnY+Y7FRb4KaPpjN+u/yXLa7MrXThHS0XFv63eRY2S0RoJqXEwvvfVP/lofowvooVgdxXi3/wAAWZ+Cmj6Yzfrv8k+Cmj6Yzfrv8lV7If8AlXRnd2+lkzGosl8FNH0xm/Xf5LG6l4b1sXpzK3IMzmhPXqSzRl1zcczWEjcbfKE2P/tXRmdoUsmEUF8GrDT8UOBmkNU5zN5aTLZOoZrDoLPZsLu0c3o3bp0AVmfBTR9MZv13+SbH/wBq6MbQpZMw76VeRxc6CJzj3ksBJXH3BV/9tD/uBZr4KaPpjN+u/wAk+Cmj6Yzfrv8AJNkP/KujMdvpZMxbI2xMDWNDGjuDRsAstwo+56/+lLn1zlx+Cmj6Yzfrv8lINNabq6VxnuGo+aSMyvmc+xJzvc57i5xJ/KVZWSyKyKd8777uDOO1WqFaCjFGWREXYVgREQBERAEREAREQBERAEREAREQBYXWv3G578wsfVuWaWF1r9xue/MLH1bkBVXgT/1V+Hf5g766RXeqQ8Cf+qvw7/MHfXSK70AREQBERAEREAREQBERAEREAREQBERAEREAREQBYXWv3G578wsfVuWaVfcWeKWjNG4TLYrP6uwWDyljGTSQ0clk4a88rHNe1rmse4OILmuAIHUgjzICI+BP/VX4d/mDvrpFd61n8C7ivomr4OvDzCT6xwEOabCykcdJlIG2BYkmk7OHsy7m537HlbtufMCtmEAREQBERAEREAREQBERAEREAREQBERAEREARdNy5Xx1Sa1anjrVoWl8k0zwxjGjqS5x6AD5SoRc4k27byMLhjNB1At5KU1mO+drOVzyP8Qb8vXpvJGnKSvWHQkhTnUd0VeT1aBfZTuC02Vw+B4n0GOkdjGNxGTAO4ZA6RzoJNu4ASSPaT3kyM+RbaHWerSelbCgfIXTFYPW5zXELSGY01maOFnxeVqvqWGAyh3K4bbtJ32cO8HzEA+Zb6pd5dSfslbI0M+xpcCzr3ipPrnJVi/C6W2dXLx5Mt5w+1j5+zbu/p1Duz+VfqwteuCui7vAXQNTSWmq2NfQglknfYuve+eeR7ty+RzWtBO3K0bNHRrR5lPBrLVu43r4Xb5jMmqXeXUdkrZFkoq9rcRczScDk8DHPX/CmxVntHt+cxPa3cefyXE/ID55rh8zSz9FlyhYbYruJbzAEFrh3tc07Frh3FpAI84Wsqcoq/Fct5DOlOn8SPaiIoiIIiIAiIgCIiAIiIAiIgCIiAIiICs9Q5V2qM/Yh5icTi5uyZGHeTYsN2LnuHnEbvJaD3ODnbEhhHFYbRz3S6dqyv8A6WZ0k0v+sdI5z9/n5iVFuOediw+ioq3PmBey1+vjaMWCtipZmsPfu1gmPSNpDXczu8N3267KS0bqjhwju6eeJ6WnFUqSuLCRamt1ZrvS2huKWFsX8lDJgMljeay3IOyl2hQsNifZLLDo2ukLIy9wJbu3c9Tygry5HV2d0fg+IGW0rnM5k9OW7WFxWIzWevTuEBlkc206OSdrtmt7Ro7Usds534QYGjnuGvS4et/kbeItYNRYLiZw/wBC8QMlYyVihhY9M23xtk1PPlbcV1o3jmilfBE+IcvOCA4jflIA2UnxuPyGnuJukcM7UufyVDV2nr78gy7kZHFk8QrkTQEEdg7aZ42i5WjoQAQCsGyq8i7MPmqGocbDkMZcgyFCcExWazw+OQAkEtcOhG4PULkcq7SN4ZqNxbUBAyMXNsx0PQGUj+1GPK387QR18nanPA+05Xw/A7Tt2G3kLEl+vzSR270s8URbJINoo3uLYh1O4YBuQN+5XNkIY7FCzFNsYnxOa/cbjlIIKlpS0Jq/Dj4C5VqfvLEtVFgdA2ZrmhdOWLBJnlxtaSQnv5jE0n/is8t5x0JOOR5l7giItDAREQBERAEREAREQBERAEREBVD6DtO5/IYqQFsUssl2k4n+kie7me0f6t7y3bzNdH/aAWO1fozDa8wzsVnaQvUjIyYN7R8b2SNO7XsewhzHA9zmkFWpqHTtTUlEV7PPE9jueGzCQJYH+Z7CQQD1IIIIIJBBBINJ6B198IVbL2tOV5NUYvGX5cc/JUYTXEkjAC7ljmLeYAnl3Y5w3G/QKaUdc9OL38eHz+f3Lqz2mEoaFQjmoeAWn6+j9QY/S+Fow5DLMrif3yt2+ysuhl7Rj5XxyCTtAS4iUHn323JA2WJ4YcEMpiW6jraufSs6fy1WOqdNRZG5k6oILi+YyWyXhzgWjZoAHKD1PVW4b+QadjpvNb/NWB/5OXTbzdmhVms2cDl69aFjpJZZa4axjQNy4ku2AAG+617PVyOnSoXp3oi+K4E6IwuDzWIq4eT3BmavuK8ye/ZmfLBs4CMPfIXNaA92waRtudlI5NG4ebN4bLvp75HD15qtGbtX/aYpQwSN232duI2dXAkbdNtyvJpXXVfW+n6WcwOMyeWxFxhfXuVa4dHIAS07Hm8xBBHmIIWWF/IEgeLmaG/y1R/Esdnq5G6qUVg0YfR/DTTegLWSnwGPOOOQkMs8TLEroeYuc4lkbnFke5c4kMDQd1l8zXnysTMPTcW3clvA1zCOaKM9JJf9hpJ/xco/CC9NajqTKuDKmBkotPfZyszI2N/IxjnvcfmIaD8o83fofVWmMfxKzGhzbtWtcVacd61LZoyRMlru22MD9uTs2ucBsHHyidy5wcVvGGqanO69cMepzVrTCnHRp4llVq8dSvFBCwRwxMDGMHc1oGwH7F2IihxKIIiIAiIgCIiAIiIAiIgCIiALGam1DU0lp3J5vIdr7hx1aS3P2ELpZOzY0udysaCXHYHoAsFxV1/Z4caOsZijpzJ6tvCWOtXxWIj5pZZZHBrOY9zGbkczz3DzFebEcOpY+J97XdrPZt8lzGxUYdP2LDfcVFvRzy1jOjnlzRu4k7eVsSCNgMLgxmOMT9Aa7x+cz+jMBFFLbs6WtU44ZrrnDljE5duQwDnPKNw7mY4FpAKs6vWhqR9nBEyGMuc/kjaGjmcS5x2HnJJJPnJJXaiALUD7JLx1+DrhIzRuNnDM3qvmgl5T5UVJu3an/bJEfXvBk26hbfql+P3g08N+LkN7U+rNOe+2cx+Jkr1rXu6zD2cbO0kYOWORrTs57juQT1+TZAap/Yt+O3JLluFWVsHZ/Pk8Nzu6Aj+nhH6tpAB8kh86/RRaeeBZ4L/DObhRw64jHTr26zEJue+keStsPaiR7d+zEoZtyjYt5diNwQdytw0AWPzuFiz2Jv0XzT0zbrSVTbpv7OeJr27F0b9jyuHeD5iAfMsgiAqejLqPgniNC6Xgxuo+JtKxafRvainsRPtUWOf9qkmadjIwc3KXDubGSSSQDZ9PJU8i6wKlqC0a8pgmEMgf2Ug2JY7Y9HDcdD16helVjk+E7dEVtc6g4X43E4rXeozHZmlyXaup2J2OJ5nsa7yS4PfuW7buIcd0BZyKCYzitiqOoNN6N1RkaGL4g5XGtvHEQue6N7gD2ohkLQHcrmv2G/MQ0nbbcqdoAiIgCIiAIiIAiIgCh+tNVakwmotK43A6Slz9XJ2yzJZI22QQ4yu0Aukdvu57jv5LQOux6jpvMFV2Tp6d0p4QeJzN/VFurm9U4mTDY/T0nM6tZdXd7okmb0Ia9rCG9SAQfOSgM7w74UYfhnb1LbxtjI3LmoMi/JXbGSuPsOLz0axvMdmtY3Zo8+zQCTsFNERAEREAUc4kZOphuH2pbt+1DSpw46w6Sew8MYwdm7qSegXv1RqjE6L0/fzmcvwYvE0YjNZt2HcrI2j/AJnuAA6kkAbkrWHD4POeGzm62f1NVtYDglSmE2JwEpMdjUT2nybFkDq2Dpu1nn7+vxiBYngUsczwWOHQcC0+95Ox+QyvIV3LpqVIKFWGrVhjrVoWNjihhYGsjYBsGtA6AAAAALuQBERAEREB5rGMqW7dW1NVhltVS4153xtc+EuHK4scRu3cdDt3hVFYzGe8G/h9Pc1Bf1NxYgdl9mTVKEb7tGnJ13kazbtRG4O3cAD5bQA0Dpcyw+sq2SuaQzlfD3o8Xl5aM8dO9N8SvOY3COR3Q9Gu2J6Hu7kBlmP7RjXbEcw32cNiPyhclH+H9PL4/Q2n62fycWazkNGGO9kYP6OzOGAPkbsB0c7cjoO/uUgQBERAEREAREQBaEcbPsiOmdM8SoMfY4O3cpmdMTyMbLqC7BVsULe7mSCIRtsNI5Q3aRr/ACtzsNgC7fOexFVjMk0rIYx3ukcGgfrK0v8ADz8GrB8ZcC/Wmk7mOGt8ZETYrxTs5spXaPibA9ZWgeSe8jyevk7bKMpYIEy8Bzwm9aeEtj9YXtVYrD46ripasNKTEwSxiV7xKZQ/tJX78obFttt8Y9/m2iWqX2PLT9Hhx4OGPOTswY3J5q7Yyc9a3I2OVgJEUe7XbEAsia8fM/fzrZjxqwnpih60z2rbVz7rM3Myqw2sdY4XQGmchqDUORhxWHoRmWxanOzWDzAecknYBo3JJAAJK8mqOIunNHaUympMplq0OHxkBsWbEb+05GD5A3ckkkAAdSSAtedH6Ez/AIWmp6GveI+Pmw/DejKLGmdE2Rs64fwbt5vc7cdWxnpsf7JJk0aa3MwdWmdJZ3wydRUtY64o2MLwioyixp/SNjyZMu4fFuXB/YPe2PuIPnbuZNrIomQRMjjY2ONgDWsYNg0DuAHmC5NaGNDWgBoGwA8y+rACIiAIiIAiIgC0F8Ojwr+JfCPWme0E3Aadm0dnsSWU7lmvYdYkglhMU4L2zNaHtk7QAcvQchIO/Xe23m8dQfyWb9Wu/wDsyzNaf+JWsHh8cLMVxt4MyW8PapXdVade69Qigma+aeMgCeBgBJJc0BwaASXRtA71uoTe9IzcypfAR8LLiDxH1jpfhkNPYJmk8NjCyxdqwzixBWhgLIiXOmc0udKYGk8vcXdOu4/QZah/Y+OE2N4M8JZMxnbFbH6s1JIJ7Ve1K2OarAwkQwuaTu09XPIOx8sAjdq2uq53G3pBHWyFWw8/gxTtcf2Ao6c1vaYuZ7kRFoYCIiAKI6u1dPUt+9OJ5DkC0PnsyDmjqMPd0/Ckd+C3uA3c7pytfKrE7KteWaQ7RxtL3H5gNyqh00+S3io8jPsbeSPu2dw36ukAIHXzNbytHzNCljdGLqPhh4+vwdtloqrP3sEH6ao25u3yMZy9sjY2cjtM89d+gI5Wj5mgD5l3e8GMH/p1T6BvsUC44cTcrwzp6WlxWFsZh+UzlXHzNgZG5wje7ymN55GASPHRpPk7777dCu7P8a6eCvUMYzTWocrnrFBuSnw2NqxTWaMBOwM320MB5g5oa17iS07AqN1aksZPqXd8I+7hcTf3gxno6p9A32J7wYz0dU+gb7FBLnHzTpxmnLOGq5PVNnUFd9qhj8PXD7DoWbCR7xI5jYw1xDTzuB5ug3KxHwwnVWseF/i9bmr4jM3cnUydG1XaydkletITDI1wLo3skZ1AI327yD111k+8zLnAsm5o/B343MnxNRwcNi5sLWu7tvjDY9xI/Ws9g9UW9MSxwZO1Jfw73Bjbc55pqhJ2Bkd+HH1ALj5Te9xc0ksrzRfF+hr3O26OKwmbdj4Jp67c3LVa2jLJC/kka1/OXdHAgczQDsdt1OpoWWInxSsbJG9pa5jhuHA9CCpI1pYTd69YZEc6UK8S0EUT4Y5CS5pSOvPIZZsfNLRc8kkubG4iMknqSY+Tcnz79/epYk46EnHI85KLi3FhERaGoREQHnyGQr4qlPctyiGtC0vkkd5gP+f5B3qtMnkshq9xkty2cdjHb9njYn9m57fMZnt8on/Qa4NG+x5tt1luJVv3Vk8FhjsYZXSX5mnfyhCWBg+kkY/8sYWOUzk6UU44vjksN3P9XFvY6EWtZJGMr6Xw1VvLDiaUY8/LXYN/y9Oq7feDGejqn0DfYqR01xX1VqfiHr6eTH6io4HS/NBDiK9Gi4WXiGNx55HSl5lJl52Na5rORo5juSFl9Kcea7dOaMqOp6h1hnc1gxl4n08dBFLYjDmtc57BKI4neWDtzcvmDiSAYXUqPe5PqWCqRLX94MZ6OqfQN9i65tMYey3llxVKQbbeVXYf+ih7ONFG/ovE6kwuntQ6ir5GSSJtPGUmusQPjc5sjZg97WsLXMc07u7x03WAucYXalyvCy3py1PVxWdzFujkKtqs1kw7GtYLoZGuBLHMliG/KR8XvIPUqtRYSfU2c4FtYy7kNIuEmPknv49u3aYqaXm8kd/YPd1a/wCRpPIe7yN+cWXjMlWzFCC7TlE1aZvOx4BHT5CD1BHcQdiCCD1Vdr3cObRp57N4kECBzY8hCwb+S55c2UfMC5gd0873frmUnWi3LFcc/Hz8eRW2yhFR1kUWAiIoSoPNkagyGPtVSdhPE6Mn5NwR/wBVU2lZHP05jQ9rmSxwNhlY4bFr2DleD+RzSFcSrrVWBl05kbOVqQumxVt/a3I4hu+tLsAZQ3zxu28rbq13lbEOcWzRWnB01jivL1lcWFjqqnNxlxKx45aXzOpNO4SxgKTMnksJnKWYbj3TNhNpsMm742vd5LXFpOxcduijEtXXGnOIN7W+N0PJlvGHE16lvDnJ1orFCxXfLyEvc7s3RubL15HEgt7irorWYbsDJ68rJ4ZBzMkicHNcPlBHQrsXK71uZcuCbvvNctG8KNY8HZtIZ6liYtW5GDEXcbl8dTtx13RvsXPdgfA6Uta5rXucwglpI2I37lywXCnWen8lpLVcmLr3ct405PNZTEV7jG+5Ir0TotmSO2a8xN5HO225jzbb962LRYvNVRiijdK6O1RT42DMY7S0ui9PyyXH5sNy0c9TLucNoJo67SezmLtnueWsO24JdvuryReWnWm1hYfj8bIRVB5LmRjPkQt32dHG4d8pG4AHxPjO/Ba+SEHUfLi8g3GjFtskvCmuRp23cIIbeyFiwzcbEsDuzafyERhw+YhTNdFKnBjqcFStE2CtBG2KKJg2axjRsAPmAAXepaktObkjzc5acnLMIiKM0CIiAr3iFXdBqzT94g9lLBZpEgdO0PZyM/8AzFL+xeNTnU2n4dTYiSlK8xO5mywzNG7opGkOY8fkI6jzjcHoSq6Zblp3hjMoyOnlgCexDvJmaO+SIn4ze75xvsdipZp1IJx4Y/e/1+S6sVVOOreJAND6Py+HzXFOe5U7GLN5b3TQd2rHdtH7jhj5uhPL5bHDZ2x6b93VRHg5wx1LpTPcObOUxvuWHEaHkw913bxP7K2Z67hHs1x5vJjeeZu7enf1CvlFynfq1u9czWMcJtW1cDputkdMSajwlbNZu1kdMxZGGIWRPZfJUmfzPEcjGtLiY3O3BeDykjYctJcJtZ6UwulZ4tL1o7OA1jeyfvRVvxCN1K1HK0GF52H2vt9uVwaT2Z2HUb7NIl5pqY43+vSC9Ogq5s61zFsA9nWpw1ebboXuc97h+pvIf9oLFTXZJrgx2NiF7LPALawdsIwe58ruvIwec7bnbZoc7ZpsPS+nYtM4ptVknbzve6axYLeUzSu6udtudh3ADc7NAG/RdUE6cHKXFXLz/H/hyW2qlDVrFmXREURSBERARjJ8N8Dk7MlkVpaNmQ7vmx9iSuXnfclwYQHHfzkFY/4J8f6XzQ/+afYpuinVeot2kSKrOKuUmQj4J8f6Xzfrp9ifBPj/AEvm/XT7FN0TX1MzbXVO8yHQ8KsGCDadfyTQQezuXpXRnb5WAhp/IQVLKtSCjWjr1oY69eJoayKJoaxgHcAB0AXai0lUnPdJkcpSl8TvCIijNQiIgCIiALxZfC0M9TNXI1Ibtcnm7OZgcAfMR8h+cdV7UWU3F3oELfwnxAJ9z3MvUZ5mR5GVwH5Ocu2UU4qaHj0lww1hnMfmcw2/jMPcu13SW+Zokjge9u426jdo6K31BOPX3jOIv+XMj+6yKbX1MyXXVO8yHcEtJePXB7RWo8rmcu/J5XD1btl0VvkaZJImucQ3boNyeinEfCjEg/b72XtM7iyTIyNB/wBwtKwvgw/1cuGX+XKH1DFZya+pmNdU7zPBh8Fj9P1fc2Opw04SeYtibtzH5XHvJ+c9V70RQtuTvbIsQiIsAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCgnHr7xnEX/LmR/dZFO1BOPX3jOIv+XMj+6yIDGeDD/Vy4Zf5cofUMVnKsfBh/q5cMv8uUPqGKzkAREQBERAEREAREQBERAEREAREQBERAEREAREQBUx4RnFvQ2H4ZcRNOX9Z6eo6hOAuwjE2crBHbMklVxjZ2ReH8zg5pA23PMNu8K51+bn2UjgYaGaxPFLGV/tN/kxuXLB3TNb9olP8AiY0sJ7h2bB3uQG13gp8UtGZPgvw107T1dgreoI8DTrvxMGThfbbKysHPYYg7nDmhjyRtuAx2/cVeq/Mz7F1wPfm9Y5PifkI3Np4UPoYwnoJLMjCJXfkZE/l285l/0V+maAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCrzLa31ANTZjH42tjfc2Plji57Rk53l0LJCfJ6fh7fqVhqqnfdxrD88g/dIElN0qU5pb0uPikV9vrTs9B1KeO49njfq/+4wn7Zk8b9X/3GE/bMvqKq2hVyXQ8vta1Zroj5436v/uMJ+2ZRjibh8txb0HmdI5+nh5MVlIDDKYjIJIzuC2RhIID2uDXAkEbgbg9ylCJtCrkug2tas10REOE+ncpwZ0BidH6dq4luLxzHNY+y+V00rnOLnPe4AAuc5xJ2AHmAAACl3jfq/8AuMJ+2ZfViYdVYuxqm1pyO1zZmrUjvTVuzeOWGRz2Mdzbcp3dG8bA7jbqOoWdoVsl0Mr2ra3g/ojK+N+r/wC4wn7Zk8b9X/3GE/bMvqLG0KuS6GNrWrNdEc8frjUTNQ4alka2MNW/ZdXLqpk52EQySA+V0/8AD2/WrFVUy/dZpD9Jv/dLCtZWsajq0oTaubTw8WensFedooKpUx3hERCxCIiAIiIAiIgCIiAIiIAqqd93GsPzyD90gVqqqnfdxrD88g/dIFHW/j1PBf8AZFR7V/iy+X3PaiiWoaOu58pI/BZvTtLHEN5Icjh57MwO3Xd7LUYI37vJG3zrGnGcUthtqXSA+X/u9a6//wBy87cszxSgmviX18iE8dc3qfKcSdIaIwT5IK2RpXMhOIcy/EyWnRGNrYm2Y4pHjlD3PLWgEjbygBsY3exXEHCxaD0/qTUV2g3JaulrxSYzLvsWTjzRmd2E1gxRmQh7X7OLeYDlIIc0OFu5HhjFr/BwVOIcWMz1yrZM9S1iYJ8ea/kgDkcJ3yNd8bcteAQQNunX3Y7hRpXFUsDUq4rs4MHcffoA2JXGKw9r2vkLi4l5Ilk35y7q7fv2UynFJI7I14Qgo3YX8PHfj+CiMxq7UWlfGnQ9LUmSZXdrPGYKtm7tg2LlCpcrRTSBssm5c4EuYxz9yO0HXoFL+GekmaM8I3VmPjyuWy8Z0zjpBNmbrrczd7Fkcokf5RbuCdiTsXHbpsBZOW4UaTz0GpYMjhorsOo5IpcoyZ73Cw+NjGRuA5vILWxs2LOXq0Hv6rCUODdHQjrN7QEdPDZ20yKvYu5k28k2WBhcWtIdYa7mBd0dzd3TqNtmnFpr1wMuvCUHFbm1lx3b/oyyEUBbjOKIDubUukSdvJ209aGx38//AG7r03XuwNDX0OWgfms5pu5jBzdtBQwtivM7yTy8sj7cgHlbE7tO4BHTfcQ3LM4nBJfEvr5Eil+6zSH6Tf8AulhWsqpl+6zSH6Tf+6WFay9DQ/j0/B/dntPZP8VeLCIilLgIiIAiIgCIiAIiIAiIgCqfLR5HGaz1JKMJkrkFuxDLDNVhD2OaK0TD13H4TXD9SthFt7rjKE1en5p/ggr0IWiDpzwKl987/wCLeb9VH8Se+d/8W836qP4lbSLn7NZu4+pWbIs3Pr+ipffO/wDi3m/VR/Envnf/ABbzfqo/iVtInZrN3H1GyLNz6/oqX3zv/i3m/VR/Envnf/FvN+qj+JW0idms3cfUbIs3Pr+ipffO/wDi3m/VR/Envnf/ABbzfqo/iVtInZrN3H1GyLNz6/oqWg3I5TVmmne8eTqw1br55prUAYxjfc0zO/f+09o/WraRFP7qioQVyXneWVChCzw1cMD/2Q==", + "image/jpeg": "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", "text/plain": [ "" ] @@ -200,6 +194,8 @@ } ], "source": [ + "from IPython.display import display, Image\n", + "\n", "display(Image(app.get_graph().draw_mermaid_png()))" ] }, @@ -213,18 +209,38 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, + "id": "84d65cbe-4cfe-44f8-b49e-b37632887c91", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "from langchain_core._api import LangChainBetaWarning\n", + "warnings.filterwarnings('ignore', category=LangChainBetaWarning)" + ] + }, + { + "cell_type": "markdown", + "id": "5cfaeb64-5506-4546-96c0-4891e6288ad9", + "metadata": {}, + "source": [ + "### Filter on event metadata" + ] + }, + { + "cell_type": "markdown", + "id": "f218a05d-1590-4d5c-b0b7-97d94c744efb", + "metadata": {}, + "source": [ + "First option to get the LLM events from within a specific node (`final` node in our case) is to filter on the `langgraph_node` field in the event metadata. This will be sufficient in case you need to stream events from ALL LLM calls inside the node. This means that if you have multiple different LLMs invoked inside the node, this filter will include events from all of them." + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "a37c3a5f-5a43-46db-940e-c583df776520", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.12/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n", - " warn_beta(\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -234,15 +250,71 @@ } ], "source": [ - "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")" + "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n", + "async for event in app.astream_events(inputs, version=\"v2\"):\n", + " kind = event[\"event\"]\n", + " tags = event.get(\"tags\", [])\n", + " # filter on the langgraph node name\n", + " if kind == \"on_chat_model_stream\" and event[\"metadata\"].get(\"langgraph_node\") == \"final\":\n", + " data = event[\"data\"]\n", + " if data[\"chunk\"].content:\n", + " # Empty content in the context of OpenAI or Anthropic usually means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(data[\"chunk\"].content, end=\"|\", flush=True)" + ] + }, + { + "cell_type": "markdown", + "id": "b0bb447a-6650-4166-b124-2d5b99a1f88b", + "metadata": {}, + "source": [ + "### Filter on custom tags" + ] + }, + { + "cell_type": "markdown", + "id": "ea4db927-44b6-46ab-8b8d-f237edaf1438", + "metadata": {}, + "source": [ + "Alternatively, you can add configuration with custom tags to your LLM, like we did in the beginning, by adding `final_model.with_config(tags=[\"final_node\"])`. This will allow us to more precisely filter the events to keep the ones only from this model." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "55d60dfa-96e3-442f-9924-0c99f46baed8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Well| folks|,| looks| like| we|'ve| got| some| cloudy| skies| in| the| Big| Apple| today|.| So| grab| your| umbrella| just| in| case|,| and| don|'t| let| those| clouds| rain| on| your| parade|!|" + ] + } + ], + "source": [ + "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n", + "async for event in app.astream_events(inputs, version=\"v2\"):\n", + " kind = event[\"event\"]\n", + " tags = event.get(\"tags\", [])\n", + " # filter on the custom tag\n", + " if kind == \"on_chat_model_stream\" and \"final_node\" in event.get(\"tags\", []):\n", + " data = event[\"data\"]\n", + " if data[\"chunk\"].content:\n", + " # Empty content in the context of OpenAI or Anthropic usually means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(data[\"chunk\"].content, end=\"|\", flush=True)" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph", "language": "python", - "name": "python3" + "name": "langgraph" }, "language_info": { "codemirror_mode": { @@ -254,7 +326,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/streaming-subgraphs.ipynb b/examples/streaming-subgraphs.ipynb new file mode 100644 index 000000000..ca7f0808c --- /dev/null +++ b/examples/streaming-subgraphs.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to stream from subgraphs\n", + "\n", + "If you have created a graph with subgraphs you may wish to stream things occurring inside those subgraphs (or you may not!). This guide will walk through how you can control the information that is streamed back from subgraphs.\n", + "\n", + "## Setup\n", + "\n", + "First let's download the required packages and set our OpenAI API key since we will need that to run the models " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain-openai" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define subgraphs\n", + "\n", + "We are going to use the same subgraph from [this how-to](https://langchain-ai.github.io/langgraph/how-tos/subgraph/)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Optional, Annotated\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "# The structure of the logs\n", + "class Logs(TypedDict):\n", + " id: str\n", + " question: str\n", + " answer: str\n", + " grade: Optional[int]\n", + " feedback: Optional[str]\n", + "\n", + "\n", + "# Define custom reducer (see more on this in the \"Custom reducer\" section below)\n", + "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", + " if not left:\n", + " left = []\n", + " \n", + " if not right:\n", + " right = []\n", + "\n", + " logs = left.copy()\n", + " left_id_to_idx = {log[\"id\"]: idx for idx, log in enumerate(logs)}\n", + " # update if the new logs are already in the state, otherwise append\n", + " for log in right:\n", + " idx = left_id_to_idx.get(log[\"id\"])\n", + " if idx is not None:\n", + " logs[idx] = log\n", + " else:\n", + " logs.append(log)\n", + " return logs\n", + "\n", + "\n", + "# Failure Analysis Subgraph\n", + "class FailureAnalysisState(TypedDict):\n", + " # keys shared with the parent graph (EntryGraphState)\n", + " logs: Annotated[list[Logs], add_logs]\n", + " failure_report: str\n", + " # subgraph key\n", + " failures: list[Logs]\n", + "\n", + "\n", + "def get_failures(state: FailureAnalysisState):\n", + " failures = [log for log in state[\"logs\"] if log[\"grade\"] == 0]\n", + " return {\"failures\": failures}\n", + "\n", + "\n", + "def generate_summary(state: FailureAnalysisState):\n", + " failures = state[\"failures\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " failure_ids = [log[\"id\"] for log in failures]\n", + " fa_summary = f\"Poor quality of retrieval for document IDs: {', '.join(failure_ids)}\"\n", + " return {\"failure_report\": fa_summary}\n", + "\n", + "\n", + "fa_builder = StateGraph(FailureAnalysisState)\n", + "fa_builder.add_node(\"get_failures\", get_failures)\n", + "fa_builder.add_node(\"generate_summary\", generate_summary)\n", + "fa_builder.add_edge(START, \"get_failures\")\n", + "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", + "fa_builder.add_edge(\"generate_summary\", END)\n", + "\n", + "\n", + "# Summarization subgraph\n", + "class QuestionSummarizationState(TypedDict):\n", + " # keys that are shared with the parent graph (EntryGraphState)\n", + " summary_report: str\n", + " logs: Annotated[list[Logs], add_logs]\n", + " # subgraph keys\n", + " summary: str\n", + "\n", + "def generate_summary(state: QuestionSummarizationState):\n", + " docs = state[\"logs\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", + " return {\"summary\": summary}\n", + "\n", + "\n", + "def send_to_slack(state: QuestionSummarizationState):\n", + " summary = state[\"summary\"]\n", + " # NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack\n", + " return {\"summary_report\": summary}\n", + "\n", + "\n", + "qs_builder = StateGraph(QuestionSummarizationState)\n", + "qs_builder.add_node(\"generate_summary\", generate_summary)\n", + "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", + "qs_builder.add_edge(START, \"generate_summary\")\n", + "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", + "qs_builder.add_edge(\"send_to_slack\", END)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define parent graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Entry Graph\n", + "class EntryGraphState(TypedDict):\n", + " raw_logs: Annotated[list[Logs], add_logs]\n", + " logs: Annotated[list[Logs], add_logs] # This will be used in subgraphs\n", + " failure_report: str # This will be generated in the FA subgraph\n", + " summary_report: str # This will be generated in the QS subgraph\n", + "\n", + "\n", + "def select_logs(state):\n", + " return {\"logs\": [log for log in state[\"raw_logs\"] if \"grade\" in log]}\n", + "\n", + "\n", + "entry_builder = StateGraph(EntryGraphState)\n", + "entry_builder.add_node(\"select_logs\", select_logs)\n", + "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", + "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", + "\n", + "entry_builder.add_edge(START, \"select_logs\")\n", + "entry_builder.add_edge(\"select_logs\", \"failure_analysis\")\n", + "entry_builder.add_edge(\"select_logs\", \"question_summarization\")\n", + "entry_builder.add_edge(\"failure_analysis\", END)\n", + "entry_builder.add_edge(\"question_summarization\", END)\n", + "\n", + "graph = entry_builder.compile()\n", + "\n", + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stream\n", + "\n", + "Now let's see how we can stream from our graph!\n", + "\n", + "### Define input\n", + "\n", + "First, let's define the input we will use for the rest of the notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Dummy logs\n", + "dummy_logs = [\n", + " Logs(\n", + " id=\"1\",\n", + " question=\"How can I import ChatOllama?\",\n", + " grade=1,\n", + " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", + " ),\n", + " Logs(\n", + " id=\"2\",\n", + " question=\"How can I use Chroma vector store?\",\n", + " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", + " grade=0,\n", + " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", + " ),\n", + " Logs(\n", + " id=\"3\",\n", + " question=\"How do I create react agent in langgraph?\",\n", + " answer=\"from langgraph.prebuilt import create_react_agent\",\n", + " )\n", + "]\n", + "\n", + "input = {\"raw_logs\": dummy_logs}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Stream normally\n", + "\n", + "First let us examine the output of streaming normally:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- Update from node select_logs ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node failure_analysis ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node question_summarization ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n" + ] + } + ], + "source": [ + "for chunk in graph.stream(input, stream_mode=\"updates\"):\n", + " node_name = list(chunk.keys())[0]\n", + " print(f\"---------- Update from node {node_name} ---------\")\n", + " print(chunk[node_name])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see there are only 3 updates made to our overall graph state. The first one is by the `select_logs` node, and then we receive one update from each subgraph (note if you don't want to see the `log` update from each subgraph that you can set the [output schema](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/) to exclude it). What we do not see however, is the updates occurring *inside* each subgraph. The next section will explain how to do that.\n", + "\n", + "### Stream subgraph \n", + "\n", + "To show the updates occurring inside of each subgraph, we can simply set `subgraphs=True` to the streaming call:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- Update from node select_logs in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node get_failures in failure_analysis subgraph ---------\n", + "{'failures': [{'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node generate_summary in failure_analysis subgraph ---------\n", + "{'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node failure_analysis in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node generate_summary in question_summarization subgraph ---------\n", + "{'summary': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n", + "---------- Update from node send_to_slack in question_summarization subgraph ---------\n", + "{'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n", + "---------- Update from node question_summarization in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n" + ] + } + ], + "source": [ + "# Format the namespace slightly nicer\n", + "def format_namespace(namespace):\n", + " return namespace[-1].split(':')[0]+' subgraph' if len(namespace) > 0 else 'parent graph'\n", + "\n", + "for namespace, chunk in graph.stream(input, stream_mode=\"updates\", subgraphs=True):\n", + " node_name = list(chunk.keys())[0]\n", + " print(f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\")\n", + " print(chunk[node_name])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first thing you will notice as different is that we are no longer just receiving chunks, but we also receive namespaces which tell us what subgraph we are currently inside of.\n", + "\n", + "If you look carefully at the logs you can see we are now receiving the updates made by nodes inside of each subgraph, for instance we now see updates to the `summary_report` state channel from the `get_failure` node which lives in the `failure_analysis` subgraph. When we didn't set `subgraphs=True` all we saw was the overall update made by the subgraph `failure_analysis`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/streaming-tokens-without-langchain.ipynb b/examples/streaming-tokens-without-langchain.ipynb index d31f287f8..1284d0662 100644 --- a/examples/streaming-tokens-without-langchain.ipynb +++ b/examples/streaming-tokens-without-langchain.ipynb @@ -241,7 +241,7 @@ "import operator\n", "from typing import Annotated, TypedDict, Literal\n", "\n", - "from langgraph.graph import StateGraph, END\n", + "from langgraph.graph import StateGraph, END, START\n", "\n", "\n", "class State(TypedDict):\n", @@ -257,7 +257,7 @@ "\n", "\n", "workflow = StateGraph(State)\n", - "workflow.set_entry_point(\"model\")\n", + "workflow.add_edge(START, \"model\")\n", "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", "workflow.add_node(\"tools\", call_tools)\n", "workflow.add_conditional_edges(\"model\", should_continue)\n", @@ -336,14 +336,6 @@ " if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n", " print(\"LLM token\", event[\"data\"][\"chunk\"].dict())" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "adb0f7bc-6e51-478e-bd32-8f72df072d6c", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/subgraph-transform-state.ipynb b/examples/subgraph-transform-state.ipynb new file mode 100644 index 000000000..94c4b9ccf --- /dev/null +++ b/examples/subgraph-transform-state.ipynb @@ -0,0 +1,277 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to transform inputs and outputs of a subgraph\n", + "\n", + "It's possible that your subgraph state is completely independent from the parent graph state, i.e. there are no overlapping channels (keys) between the two. For example, you might have a supervisor agent that needs to produce a report with a help of multiple ReAct agents. ReAct agent subgraphs might keep track of a list of messages whereas the supervisor only needs user input and final report in its state, and doesn't need to keep track of messages.\n", + "\n", + "In such cases you need to transform the inputs to the subgraph before calling it and then transform its outputs before returning. This guide shows how to do that." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define graph and subgraphs\n", + "\n", + "Let's define 3 graphs:\n", + "- a parent graph\n", + "- a child subgraph that will be called by the parent graph\n", + "- a grandchild subgraph that will be called by the child graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define grandchild" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict\n", + "from langgraph.graph.state import StateGraph, START, END\n", + "\n", + "\n", + "class GrandChildState(TypedDict):\n", + " my_grandchild_key: str\n", + "\n", + "def grandchild_1(state: GrandChildState) -> GrandChildState:\n", + " # NOTE: child or parent keys will not be accessible here\n", + " return {\"my_grandchild_key\": state[\"my_grandchild_key\"] + \", how are you\"}\n", + "\n", + "grandchild = StateGraph(GrandChildState)\n", + "grandchild.add_node(\"grandchild_1\", grandchild_1)\n", + "\n", + "grandchild.add_edge(START, \"grandchild_1\")\n", + "grandchild.add_edge(\"grandchild_1\", END)\n", + "\n", + "grandchild_graph = grandchild.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_grandchild_key': 'hi Bob, how are you'}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grandchild_graph.invoke({\"my_grandchild_key\": \"hi Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define child" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class ChildState(TypedDict):\n", + " my_child_key: str\n", + "\n", + "\n", + "def call_grandchild_graph(state: ChildState) -> ChildState:\n", + " # NOTE: parent or grandchild keys won't be accessible here\n", + " # we're transforming the state from the child state channels (`my_child_key`)\n", + " # to the child state channels (`my_grandchild_key`)\n", + " grandchild_graph_input = {\"my_grandchild_key\": state[\"my_child_key\"]}\n", + " # we're transforming the state from the grandchild state channels (`my_grandchild_key`)\n", + " # back to the child state channels (`my_child_key`)\n", + " grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)\n", + " return {\"my_child_key\": grandchild_graph_output[\"my_grandchild_key\"] + \" today?\"}\n", + "\n", + "\n", + "child = StateGraph(ChildState)\n", + "# NOTE: we're passing a function here instead of just compiled graph (`child_graph`)\n", + "child.add_node(\"child_1\", call_grandchild_graph)\n", + "child.add_edge(START, \"child_1\")\n", + "child.add_edge(\"child_1\", END)\n", + "child_graph = child.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_child_key': 'hi Bob, how are you today?'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "child_graph.invoke({\"my_child_key\": \"hi Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

Note

\n", + "

\n", + " We're wrapping the grandchild_graph invocation in a separate function (call_grandchild_graph) that transforms the input state before calling the grandchild graph and then transforms the output of grandchild graph back to child graph state. If you just pass grandchild_graph directly to .add_node without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between child and grandchild states.\n", + "

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that child and grandchild subgraphs have their own, **independent** state that is not shared with the parent graph." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define parent" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "class ParentState(TypedDict):\n", + " my_key: str\n", + " \n", + "def parent_1(state: ParentState) -> ParentState:\n", + " # NOTE: child or grandchild keys won't be accessible here\n", + " return {\"my_key\": \"hi \" + state[\"my_key\"]}\n", + "\n", + "def parent_2(state: ParentState) -> ParentState:\n", + " return {\"my_key\": state[\"my_key\"] + \" bye!\"}\n", + "\n", + "\n", + "def call_child_graph(state: ParentState) -> ParentState:\n", + " # we're transforming the state from the parent state channels (`my_key`)\n", + " # to the child state channels (`my_child_key`)\n", + " child_graph_input = {\"my_child_key\": state[\"my_key\"]}\n", + " # we're transforming the state from the child state channels (`my_child_key`)\n", + " # back to the parent state channels (`my_key`)\n", + " child_graph_output = child_graph.invoke(child_graph_input)\n", + " return {\"my_key\": child_graph_output[\"my_child_key\"]}\n", + "\n", + "\n", + "parent = StateGraph(ParentState)\n", + "parent.add_node(\"parent_1\", parent_1)\n", + "# NOTE: we're passing a function here instead of just a compiled graph (`child_graph`)\n", + "parent.add_node(\"child\", call_child_graph)\n", + "parent.add_node(\"parent_2\", parent_2)\n", + "\n", + "parent.add_edge(START, \"parent_1\")\n", + "parent.add_edge(\"parent_1\", \"child\")\n", + "parent.add_edge(\"child\", \"parent_2\")\n", + "parent.add_edge(\"parent_2\", END)\n", + "\n", + "parent_graph = parent.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

Note

\n", + "

\n", + " We're wrapping the child_graph invocation in a separate function (call_child_graph) that transforms the input state before calling the child graph and then transforms the output of the child graph back to parent graph state. If you just pass child_graph directly to .add_node without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between parent and child states.\n", + "

\n", + "
\n", + "\n", + "Let's run the parent graph and make sure it correctly calls both the child and grandchild subgraphs:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'my_key': 'hi Bob, how are you today? bye!'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parent_graph.invoke({\"my_key\": \"Bob\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect! The parent graph correctly calls both the child and grandchild subgraphs (which we know since the \", how are you\" and \"today?\" are added to our original \"my_key\" state value)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/subgraph.ipynb b/examples/subgraph.ipynb index ce8cfb3cd..e26b50f20 100644 --- a/examples/subgraph.ipynb +++ b/examples/subgraph.ipynb @@ -1,686 +1,652 @@ { - "cells": [ - { - "attachments": { - "71516aef-9c00-4730-a676-a54e90cb6472.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to create subgraphs\n", - "\n", - "For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", - "\n", - "![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" - ] - }, - { - "attachments": { - "9145adc1-ce9d-4a22-8183-e13796d4a388.png": { - "image/png": "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gAACCCCAAAII1LMAQfp6BqZ6BBBAAIHGK6CBzaefftoO4Ne//nW5gVx99dU2QN+mTZty2/XD2rVr5bvvvhNNSRMeHi5Tp06VoKCgcsdt3brVBlg14K+B4/vuu8+mqXnhhRckICCg3LFVfcjJybGHxMfXfDFBDfLecsstkpJSuhiiVqQL4mrffVwWQHT6MGvWLHnggQckMzPTbho2bJhon9u2bescYuv685//bIP+ulHT7+gxNb2ZsWfPHvn0009l3759tv7zzjuvxkF1XVD3F7/4hXzxxRdl/dOUQDpGZ7Fd3aGGr776qlx66aXSvXv3smOdfXp8ZR76HXG2640ODfrr4r0Vi+tx+j45OfkUj4KCAjteTavUsmVLGTBggKhvxaL79Tulixc7Rcf0t7/9TZYtW2Zvrriul3DkyBEZO3as5Ofn2+/lmW6sOPXxigACVQuk52SImH+fYyIbdm2SyUMny5uzXpcFJlA/YcC4qjvqpUc4f/Z66fAYFgIIIIAAAgg0MQHS3TSxC85wEUAAAQSqL6ALsGogVNPdtGrVqtyJN9xwg00tUzEwrovLXnzxxfLEE0/Ic889J/fff79dxPXAgQPlztf9Wm9qaqrcfPPNsmXLFvn666/l448/LndcdT5oznwtsbE1T7fw3nvvlQXodUw6k37lypXyySefnHKzQJ8quO2222yA/sorr5S+ffva2em6gK1TNN3OFVdcYQP048aNk8mTJ9tUQepQk6Kz3s855xz561//Ks8//7w8++yzctNNN1nzmtSj43EC9PrkgS6w66QE6tq1a1lVOl5tZ/r06WXb9I0+NTB69GhxxqhPF+gNDb25oYFvHaMG+7VevYlw2WWX2fP1vCeffFLy8vJk7ty59smLP/zhD3afjunCCy+Uxx9/3H7WX1qfWukaB5pGSdv70Y9+ZK9D2UEn3/zjH/+w30udPa/fsUceecQ+raBPeujCxnpDoX379mWnqYHeVNGnIlxvppQdwBsEEKiVQGauCdKb0j70h3/falVRDU9q16adjOg/RpK3r5XktE01PNubDuevst50NRkLAggggAACTV2A/7Np6t8Axo8AAgggcFoBDbBqiYysXhoDDdBrwHTIkCE2aKsznnXBWU1L8tRTT5Vrx5ktf9VVV4neDNDjtKxatarccdX5oEH6083grur86667Tnr06GEPe/311+Xll18uN0PbOV9T/tx99932owaQH330UdG+a3GcNIXMHXfcYcdz1113yb///W+5/fbb7TE6G766RZ8s0AC1Bpq1LU2t8/DDD9tgvwaiT5w4Ud2qZOjQoXbWuZ6wfPlyGxjfuHHjKefr+LT06dOn3D5NjaPXx3lyQGfbq5MG9PUc/dE0Qc4416xZI1q/jveZZ54RffJArbSO1atX21nur7zyim3jtddeM5NwS6z3RRddZOvSpxQ2bdokn332mT3ml7/8pQ3gO53SGwIzZsywgXi9cXHJJZfY747eZPjwww/ttejdu3e52fx6vBZdU6GyWf5O3bwigEDNBDJ0Jr0p0SbtTEOXAQn9zAz+eFlsZtPnFeQ3dPMe0V6zSp728oiO0QkEEEAAAQQQQKAWAgTpa4HGKQgggAACTUNAZyRrGhGdXe3kfT/dyA8ePGgD9Brw1uCrvmpgVoOnWv73v//Z9DjO+U59GvzVwLYT3K9NkF5zkDuBdqf+6r5GRETI559/boPOGujXALKmWXnsscdsChinHiftj37W9Dias96ZGX7NNdfYw3QhW70xoUWP79Kliw0i6+ef/OQn+lKtojPNteis9fHjx4uvr68NQOs2feLAMdXPVRXNwa590XPGjBkj3377rUycOFE0+O2a4kcXCNaifXaKBsQ1wK7l3HPPdTbbXPbO4sG60QnQa6ocLbq4sFP0xobTjq5hoGb6vdI0Nvr9yM7Otjci9IkNDeqrbfPmzW3aG6cOV/v9+/fbzQMHDiz3dIeOU28waJ0tWrRwTrVPDWjwXq+tc53KdvIGAQRqLbAvf5/kHsiW4DYhEtk2vNb1nM2JF54zWQ4dzpNFGxaeTTWciwACCCCAAAIIIOABAgTpPeAi0AUEEEAAAc8U0GDn73//ext41rQmGuA93SxuJzitucI1n7gGSzWPvb5qoF+L5gx38qA7M7M1YKwpTrRo4FYDuocPH7afq/NL06roOZrzXmdla95xbbMmRYPgmmrlq6++srPfNeCvM8U1EKypWnTM33zzTdkTAprqRhep1aC0bnduEGiqFS36qjceNB2OBsb1psX1119f7S7pbHQ9TwPm6qUpXbQfjqPeQHDNx16dirUv2g8N1p9//vn2ddKkSTYwruc7qYI0pY8WzQ+vueydAHtlps7CvbrvpZdeKltYWGf+uxYNkGtgXouOQYPmTkBf0x05M/svuOACe4yOVZ8ccIqmJHJu3uj11eJ8j5xj9NXf39+2o+l9dHa/ztzXlEpamEVvGfiFQJ0JpJ9MdRPVwKluXAfQIiBQzhsySTbvSpFVW1e77qr0fWp2mqzYukq+XvWNvP/9hzJ93vsy7bv3ZfaKr2RB8iJZumm5bE7fKkcKj1V6vqdt9BEfT+sS/UEAAQQQQAABBGotcOrKZrWuihMRQAABBBDwPgENLmtAVNOQaO54DbZqwDctLc2mQElKSrIz7Z3c9BpcnT17tg28avBW07ZoPvYbb7zRbtcZ5ZqL3AnqP/TQQ3amuMppXVo0IDtixAj7/nS/NEitwdiZM2faQ9555x3RH6f88Y9/lFtvvdX5eMZXnS2uY9Tj9aaBBrF1DBrY1X5r0F3HosF8DchrPv3Kis5y16LpgX73u99Vdki1tulMfg1Ua851TaGjVmquTyNoOh69gaA3Q3Tmef/+/ausU1PO6Cx/TZWj9Wgdmpdeb8Bo3nitQ2eZ63ZtY968eTZtjBqPHDnSpivSpymcwLo2qL4aYNcUOnpDQgP+aqhBeE1X41q0DW1b69MbDh07dhRn5r4uNqvXXdcj0Nn6erNAb1JoPe+++67s3bvXfu+mTJli+6qpbLTs3LnTvlb8pWsG6FMZurCxa1FLCgII1J1AWaqbsJi6q7QWNfWI7y67snfZtDftTdqdqJCIslqOHDsia3asl6XrF0hgQHM5Vlj6xJAe0KplG3NjL0ASohOloPCwZO3bI1k5mVJ0vNCe3yEmQRJjukinqM6iNwM8sZRIiSd2iz4hgAACCCCAAAK1EiBIXys2TkIAAQQQaEoCumCpLgarKU50EU7Nma5Ba51J7eSSDw4OtsFjnRWvgVsNsv7pT3+yaV58TN5cza2u9WjwWWfUa6BZg7qu+e41INyhQwebz7wqX12o1Ek34xyrM7Y1iKv90pn/1S0aMP7vf/9rg9R6Xnh4uB2fjlFLTk6OTZeis7P1RoDeeHAtOpt/z5499kkAzdGuNyV0trtr/vPi4mKbMkhn/FdchNe1Ln2vQWYNNjtBf71Rct9999nz1FefNHBm569bt67sJkfFepzP+oSBBur1R206depknw5wFtzVOkaNGmVvemiqHT1ObxRoqhoNdutaA06KHb0Z06ZNGxs411n2UVFRNkivbel4NRivQX4dp34HtO96E+C3v/2taEoa57p0797dvtcFhfXmyNq1a+13w7kxoDdvtJ9aNN2N5vjXmyYauNeifaisaOodbUdz52vRJzb05oJrCpzKzmNbwwk8/+UOGZHUTvrEBzdco7RU5wKZOem2zhg3B+m1E5MGTZQX01+U+evmyxWjShevXr8rWZZvXCYH80tTZGmAXm+0Jpqgfnfzk2/y2MdHxEuQCda7lvR9GZKWnS4ZObvl66WzTCA/UHp0SpIxfUa7HuYR70+YP9spCCCAAAIIIICAtwj4mL+48n833nI1GQcCCCCAQIMJaAoYDYZWLBqw1rzhGujW4Lxr0f/kamqTxMTEcgFs12M0cO8a3Hbd5/peA+iaS15zymsAXFPDOLP5XY+rznvt8/vvvy8vvPBC2Qx/PU+fGtAc6T//+c9lxYoVcvnll9vqNBWNBrU1JczKlSttGiDdoTPRNcitM991jDr7W4PDOjN8/vz5dia5Bpo1kF1V0THpLHLtQ2Ue2kbbtm1Fb45Up2jgXGfe6w0U16JBdA38V7cevYbat8quvVOv892o6lpqPfodcb4nuthsYGBgpTcx9DulNyd0pr2msdHjnDRDTrsVX/UpizvvvNMG7JlJX1HHPZ9X7TggP3t2hW28dasAGdrDrE/QPUQGJWhe8+bu6VQNWr381SkyMOkcGZ70wxoNNTjdaw5N35cpH3z7noSFRMq148vftHTXIHeZVDYz5n0gfbsNNDeAC2XDtnVlXWkR2FK6dUySnh2SJDSoXdn2M74xfzZty9gua7evlbTMHRIVHitXjb7ijKc09M6YwFi5963fSmJUkjw6+YcUYQ3dD9pDAAEEEEAAAQTqQoAgfV0oUgcCCCCAAAJeIqAL4Orsbp2prYFrnXnpFJ05rrnXNXe9M8te9+msc32iQFPQ6HZN6aIBYifvvh6js/wvueQS+elPf3raWeB6XH0XfWpAF2vVdQM0yF/ZDYD67kND1a9PIyxYsMAG9TVfPcUzBNJyjsjizftkyaZ9snKzuflytMh2rEenYBmSGCIDTMB+UGf9bv7w755n9FyEIH3plVi6ebksXjtf+iT2lzF9PWeG+WdLZsr2tNK0Y9rTkOAwM2veBOc79pCWgT8sKF2j75MJ1q82+e7nrZpjvpMBcv3EGySoRek6KzWqpx4Ojg6Mkd+9dTdB+nqwpUoEEEAAAQQQaHgBgvQNb06LCCCAAAIINGoBnSGuOd01wK3peTStS2VF09Vo0D4uLk7CwsIqO4Rt9SSgTyFoah+9KaJplyieKzAvea98n5Irizfsk+z9R2xHQ9oEyjlJoXLRoCgZ3CXEYzpvg/Q9hsjwnsM8pk/u6MjHC2dIasYOmXTuxdLV5G73hLJp9xaZtbh0jRK9uTq093AZlDiwzrq22yyU++GcaRJg8tj/7NI766zes6koKiBa7nv7HoL0Z4PIuQgggAACCCDgMQLkpPeYS0FHEEAAAQQQaBwCGpzXBVirKpryhuIegRkzZtiGXRe7dU9PaLUqgVFJ4aI/YrJJacB+7vpcWbg+R75ckmF/hph9Fw2OlIn9IquqqkH2O+mZGqQxD2xEE4VmmpztzZr5SawH5KNXItcAfZcOPWTrrhTZd3BfnerFmkVpb5/yC3np43+K3qSYMuzSOq2/NpWxcGxt1DgHAQQQQAABBDxVgCC9p14Z+oUAAggggAACCNRS4K233pL27dtLnz59alkDp7lDwAnY519yXOas04B9jixev1eWmuD9m3NSZeq5MXLZ0Gh3dI02Twrs2psqRSbne7RZdLVloPvXEXAC9IEBLWTC4PMkoX1nWRYULovWzZNIkzO/b+e6+zMg0NygnTz8Epm54BNZu2O99OnUy63fC7NCiFvbp3EEEEAAAQQQQKAuBTwv2WVdjo66EEAAAQQQQACBJiawfv162b59u4wYMaKJjbxxDbfYzMguOlEiRwuL5dDRE5JXcFz2HTome/OOmTz1x2WAyUt/10UJ8sRtfWTKyFg5cKhIHnsvRS54YL488uFm2Zp5yC0Dbu7n/sC0WwZ+stFde3bZd9Hh7r9Z4gTog9uEyKUjp9oAvXZucLcB5iZCnAnULzTfrcMne143L3oToHdiP5m74mtJNYvVekNZtnW/XPTgQvl4aYY3DIcxIIAAAggggEAjFWAmfSO9cHQbAQQQQAABBBCoTKB589Igqq4XUJfl+S+31WV1bq1LU5YcN1HyEyZIrj9FZkPxcfPevJ7Q7WaCrn3V/WbbcbOv2O4rluNmW7E54IQZgXN+6TnmOHNuidmvrzrH1+4/2U6JfS2WYn01ddW27Dt4TD7+Pk0+MT8RoS1kxh8bNj98ZLuo2nbdK87LyEm349D0L+4sR4uOyfy139kuDOs9QqJCIsp1Z9KQSfKfz16W2ctny2Ujppbbd7Yfhpk1CdZtWS3fr/terht/7dlWV+vzT5Tov4VnX16fk2ZumBXLI++kyIqteXLHBR0lOqSWC+2efXeoAQEEEEAAAQSaqABB+iZ64Rk2AggggAACCHinQEJCgrz66qsyePDgOhtgtpndvTWzQBasza6zOqno7AT0JsCe3CNy9+vr5e83NGDakdrfXzi7AXvA2UcKzZMO+/ZI88CWJh99rFt7lJK6UQ4XHLKz2hOjT128tnXzVjJu8Pny7bLZsihlqZxrFvytqxLoFyAJ8d1kW+omWbNjnfTt1Luuqq5RPSVncbPLtaHWLZrJAXPzS8vsZRmy2sys/6kJ1F862L03Ylz7yHsEEEAAAQQQ8H4BgvTef40ZIQIIIICAhwhkZGTI9OnTJTg4WK677jrx9/f3kJ7RDW8S0IU9J0yYUKdDiggOlCdv7i2aFsLXJEv0NW3YnIn62qz0vd3m62Nzbh/CAABAAElEQVTb1ZfSzydf7WZznHOuOUDP9zEbfMycc18f83ryHPO29FynDvOqY9IfPd9WVaej89zKVmw/IN9t2CsLN+RKWtYPaUt6J4TIOd3ayYCEtjLQpMVpyOLN/vsP50l+wUHLmWsWXs3MzZDMnEwTDD8onWK7SKB/oN3X3qS68dUvoxvLRrM4bLu2YXJu0umfpOjVIUlSs1Jl2YaF0iW6s4QHh9VZj3t17GmD9Ou3uzFI71M3d4weub6nfNkzVJ75eKtZcPeYZO8/Ig+/bWbVbzsgd07qLFFtm3aKpzr70lARAggggAACCJxRgCD9GXnYiQACCCCAQN0InDhxwgbmNVe4lg8//FCee+45qeuUJHXTW2pBoHKBwV1CKt/B1joTSMs5Ip8u3yMLTHB+6+78snq7xAbJ8J7tZHTPcOkZF1S2vaHf6I0Vbytfr/xWcvL2SnZu5mmHtmP31rJ9uXm5kpK2UXrEdS/b1pBvNqdvtTP6B5jZ8c39A87Y9AWDJ8lLmTtl1vJZcv346854bE12djAL58ZGdZDdJkf/mu1r63SB2ur2o7hYnyepmzKpf5RM6BMhj324RT5ZuNtWOmtJpqzZekB+OqmTXDyofd00RC0IIIAAAggggMBpBAjSnwaGzQgggAACCNSlwMaNG+1inomJiXLBBRfIs88+KxdeeKFMmzZNevbsWaOmHn/8cenRo4dcfPHFNTqPg90nkJ+fL3/605/kzjvvlK5du9aqI8uXL5fPPvtMsrOzJSsry9bRqVMnueKKK2To0KG1qpOTPEdgzc48+WxZpskhnmUWkz1uOxYb0coE5kNlVK8wGdTZvTdItuWW3mDU3PreUHabGfJbMzZLyvYNUnS8UALMLPm+XQeIv5+/HCk8IuP7jbPD3LM/WwqPH5Ptmdtlw7Z1Zp2B43Iwf798teRLWW3ysvdO6C29OtTsz/Cz9UvN2mmriDeLw1ZV9AmV88+ZKJ/NnyHfrp4j4/qNreqUau/vEtPFBumTdya7JUhfUsePdfg185U/XNlNxvYOl398tFl2Zx+2KaX++layLDdPEd15QYJEmqeKKAgggAACCCCAQH0IEKSvD1XqRAABBBBAoIKAprrRcv/998uoUaPkvPPOk5tuukmuuuqqGgfq3333XdHgbGVB+lWrVkl0dLRERkZW6AEft23bJvHx8W5JM5STkyMfffSRbf83v/lNrS6GBuj/+9//ljtXA/eaQumnP/2pvQlQbicfGoXA3PV75dNle8wioKX5/tuZ1BrnDYyU0b3CZWRSqMeM4VBhabodk3jIY/pUm47oDPjknSmSnrWr7PQu8d1lYNeBEtk23G47WPDDEwzOgqxtW4fI2s2rpFXLIImPijfB/fV2Nvu3Jkd9Rk6G6GKqmge+IUqOmcnvZ24mxIdXHaTX/nSO6iS9u/STdVtXS0J0F+lQjeB+dcaRGJ0oc1d8Yx3yTEqgYGPTkMUs01wvzQ3r3k6G/X6oPP3ZVnnnm9LvyZdmVv1qM6v+VjOr/iJm1deLO5UigAACCCDQ1AUI0jf1bwDjRwABBBBoEIG9e/fadtq0aWNf+/TpI++//74NtP/4xz+WhQsXSvPm1ct7e+zYMdmxY4ccPHhQ9uzZI4cPH5aQkBDp2LGjvPfee3L06FF5+umnG2RcjaWRefPmiTqPGTPGLqrq59ew/wvkpGXQ65abm2uvm6ZAioqKkoiIiGoxanD/8ssvl9DQUHue1vnBBx/IPffcI3rjRmfqUxqPwAeL02WmCc5vMHnnmwf4ybiBUSaVTaiMMelsmgc089iB+J5cd8BjO3iajuXk5ZiUL7Ml18yMd0pkWLQMSBwgiWZGuGsJaln653S5bS1aS1LnXjKw2yAJadVWenfqI+t2rLXB+o07Npj89ZkyNOlc6Rab6HpavbzfZ8YS0S6qRnWP7TdGUrNTZbZ5AuDWi2+t0bmnO7hFYHOTqz9RduzeImnZaRJs8tQ3ZCk261nUZ7nroi4y1twse8LMqt+cav57axZqfsjMqtdc9T8zueqZVV+f+tSNAAIIIIBA0xNo2L+hNj1fRowAAggggIAVOH68NH2FK0fnzp3lgQcekLvvvlt0pr1+Pl3RGfKazz4tLU0OHTpkf3r37l3u8A0bNkhAQIAN+JfbwQc7g11TDc2dO1fefvttueGGG+pdRa+53nzZvXu3rF692rb3ySefiP44ZcCAAXaGvfP5TK9BQUHies01SK83H7TExVVvRu2Z6mdfwwhMX5guH5mfben50tMs+vqLKYlyXt+IRrM4ZUld5xhpAHYN0M9c8rnkmcVgtbQ0Qfj+Jjg/MLF/jVqfMGBC2fE6wz4qZEK5YP2sxTMlJ+kcGW6C9fVVcvP3yfHjRRIYUPO0KxOHXCDTvn7LWHwhk8+5oE66mHgySL87Z7foYrINWUpK6mcmvesY+nYMljd+PVhe+XqnvDxzm931+eIMWbllv9x2QSeZPJBc9a5evEcAAQQQQACB2gsQpK+9HWcigAACCCBQLQHNR+7MpK54gqas0UCDpq85XVm8eLFcffXVp+zu1auXDB482Oa01/etW7c2waeWsmvXLhPEOW7SIVT9n/mioiI7K79du3YSFhZ2Sht1sUH7ojcZlixZIjoWfX/NNdecdua33oTQmxG6qK6Ox7Wkp6fL999/b29qaFqfCRMmVKvf+pTB119/LZryRp1qUvT6+Ghi5xqW559/Xv7xj3+cctbIkSOlb9++kpSUJBqkd0pNnFJSUuS3v/2trF+/3t7cee2115xqePVgge+T98prX+8ys7fbyl2XdpEhie08uLeVd63m/yZUXk9Dba0YoO8S382kphkubVvVTWoWJ1gfZxZSnb34c1mRvMQOrb4C9YePlKYd0hz6NS1RJp3PkF7nytL1iyQlurNZ+LZbTas45fhg81SBloKCglP21feGknqeSe/a/59O6Ghn1T/+4WazFsE+O6v+wTfNrHqTAud2ZtW7UvEeAQQQQAABBGopUPXf3mtZMachgAACCCDQVAQ0/YwGSWfNmiWaI1zTkVx22WVy7bXX2gDq6NGjbYoT9fjiiy/kyJEjkpCQYNOctGjRotIAvKudBtA1sNy9e3cblH/jjTfs7pkzZ7oeZt87KXP0xoCmwNGi7Wk7FYv2V2fyZ2Zm2l3Dhg2TF154Qdq2LQ26VDy+4mfNsb5//3655ZZbyu3SYLPTvs5c1zQtmuLFKeqjZhWLnvfvf/9bHnvssbJdN998sw3mN2vWzN58mDp1arm61OVvf/ubTJkypeycM71Rdy0aeJ8xY4ZNf6N1aKqYOXPmyKOPPmq36TF6Y+XNN9+0+/SYH/3oR3LHHXfY66v79WbDt99+K7feeqvoLHfXoumN9CaDnjdkyBDRWfwvvfSSXHrppXbRYNdj9X1NnFasWGG/X3qepu955plnqn3N9ByK+wRGJpk88/eX5j13Xy/OsuVa3LA6yxbP6vTFG5eUzaAf3nd0jWfPV7fx7rFmQWizfnNDBOq1T4G1CNLreUO7nyM79+w0C99+IYnRCeLX7Oz+OtjcP0CrlcDAmt80sCeexa/iBphJ79q9hKhW8tKd/eWd+bvl2Q822VsEM3VWvUl/c7vJVX/BgJqlIHKtm/cIIIAAAggggIAvBAgggAACCCBQe4EtW7bYRWA1UKwBep0Z3b59e3n55Zdl7NixNnCvs72dooFaDfZq4FZnd994441Vpqfp2rWraCobzT/+u9/9TgYNGmTT3Th1ur76+/vbj4WFhaKz5P/whz/Y4P6dd95pA9POsdOmTZPbbrvNBuivvPJKO7NbU7P85z//cQ4546vmU7/rrrvkL3/5i+zbV5pCwjlBb1iMGDHCzuZ/7rnnyoLqGgDXcaxcuVL++te/OofbVw2I69g0QK9BfM0fr466UOqiRYvsMTorXYP96qn1PvLII9bwV7/6lVR2w6JcA+aDBrf1xoKWjRs3ip6nqWe0Hk2BozcrXBd1ffjhh8tm++vsfr2mkydPFp3Nr0Vnyj/77LP26QC74eSvdevW2WukT0foeHUMv//97+1evWFSWamuk57r2OnTE6+++ioB+spA2VaPAvWfYqSuOr9+V7JsT9tiq7tk5NR6C9A7/dVA/flDL7QfdUb9guTSP7uc/Z7yOr5/6X+TPl7wQ+qt2vatuX/pWiqBJ19rW09tzisuqd+c9Kfr049GxMrHfx4hw/uUrieSubdA/vzGBnnwvRTJyjv1BvTp6mE7AggggAACCCDgKkCQ3lWD9wgggAACCNRQQAPOml5GiwZ7NQisAWOdpd6jRw8bCNeZ5hrQ1aIznzUYff3119tArgb2nX32gGr8cmYs6szzisWZSa+Bc52FrjPBtWifdOa4Fs1tr3nwtWhQXoPnV111lf2cl5dnX6v65SyEq4F0nenvWrQdDWrrwrb33ntv2cxzDS7rjQa9gVCx6Pbp06fbY3VBXb25oIF+LTorX8eq9WoAXwPjl1xyiX1SQYPsH374Ybm0MRXrdj5//vnn9saCfnZuZugCu6+88op94kGfJNCbALo+gM7016C8U7RtvW4ayL///vvt5k2bNtlXDZa7Fm1HS3b2DwtUOuly1KWyUl0nPdd50mHSpEnVSmlUWXtsQ6DWAiWN468PRwqPyoqNy+wwe3bpIx0jO9R6yDU50TVQv3bLask9+MNTRDWpp6pjcw6ULkZe1XGV7Q8PDpXBPYdKhllINvss6tG6ndz4zd0wk948E1XZ8Bpkmy4a++TNveUP1yZJi+alN8d1Vv0d/1opX67a0yB9oBEEEEAAAQQQ8C6BxvF/2d5lzmgQQAABBLxIIDg42I5GZ8RrehunaGqae+65x35cunSpXbhUP4wbN86mTNGZ9xqY1pnWTiDdObeqV53FrsXJc6+pWzTtigayneDz7bffbnO3a3oVJz2OM4tcA9NO0RsImgJGg+JaNFd8dcrRo0ftYX369Cl3uN6cWLNmjb1BocF7fWJg/vz5ct9999njNMA9atQoeeutt8SpQ/utNzi0aJBcn0BQPydoP378eJtWR/cPHDhQWrVqpW9t0TQ4uk1vFlRV2rRpYw/RGwxO+h191ZQ0r7/+up1ZrwckJyfbQL1Tn6az6devn71uepNFc9s7C/jqMZob3yka4Hduuuh6ARWL642Vw4cPlz1FUR0np65//vOfNnXOdddd52ziFYEGE2gs2W42pW2SvPz90jmuq4zvN67BfLQhJ1BfVHRMVmxZWadtR7aLtPXlnmVw/dweQ2XKqMulXZvStGi17WTBsQJ7auvmpX++1rae2pxX4sYgvdPfSwa3l88eGC4Th5T+NyjdzKp/4PUN8tfpG5lV7yDxigACCCCAAALVEiBIXy0mDkIAAQQQQKByAQ0Sa3ENHOtnDcA6i3lquhonP7wGd8+26GxyLTrDXMvs2bPtrHldRNRJp6Kz+zWtjs4616C45kTXvOc6Q/ybb76xwfMvv/xSNNWNzgTXYL5u19n/1Snh4aV5tRcsWFD2JMG8efPK0sXoIoJ680CLLv6qNw30ZoWmx9GZ9Bq0P//88+25OiNdg+WaakZntet2XVhVz9GbDwEBAWVjdQ1yV6efrsc4bqtXr5asrKyyXXqTJC4uTpwbDnqTwUlpowfpYrPODREnIK8z4p3FfnWdAS1q7ho41++Aa9EbCZrD3ylPPvmkvUZO3VU5Oefpd00Xz33ooYecTbwi0GACzlMhDdZgLRtKzS79s3ZY0rm1rOHsTtNAfVJCb9m4Y4PsyCp92ursaiw9O9AvQCJC28sx86TA3rycs6oyPiLurHPSr9m+1vYhqI4W4q3JgDwhSK/9bd28mTz4oyR55JbeEta2NP3PpwvT5c4XVjGrviYXlGMRQAABBBBo4gIE6Zv4F4DhI4AAAgicncDQoWalQFN09rTOEteFT//v//7PBsE1KK6BVD3GmemtOezPtjh1aaocnbn91FNP2Sp1RrwTfO7cubNdFNZp6+KLL7ZvdVa7Bph9fX1tQP6JJ56wqXA0mN+lSxfn8CpfNVCsee61rgsvvFA09YrmkdeiOfM1YK2pgLTozQK9YaFt3nTTTaKB/T/+8Y/2GM1r76QL0v3nnXeeTTOjaWw0kO+kdnGC8zt37rR11uZXRESEPU3T8Bw4cMC+/9nPfib9+/e37zVIrjcz1EjT2jhFbxToGPTJCM1dr0Xr+vnPf27fax06fr0ZoqmExpjZ9lo+/vhj++r8iomJEb3+eo00P77ekNAZ9DpuLVU5OfVoGh29saBpfireCHCO4RWBehNo4MU6azuODBOkH5R0zlnPFK9t+3reOd2Hir8JqqfsSjmbak45Ny4i1m7blrnjlH0NueFgQb6k7FhvmwxuVfpUWUO2766c9Kcb47jeETLTzKq/bFTp9dmdddjOqv8bs+pPR8Z2BBBAAAEEEHAR8HN5z1sEEEAAAQQQqKGAplDRwK0uMurMnNcqdHFTzQnv5FXXGfc6a33z5s01bOHUw50Z37rwq1MefPBB0b5ERpamQtCge4sWLZzdcsMNN9gbCDqTWwP4ixcvlnfeeccGhssOMm90sdk9e/bYFC7OUwKu+13fa9A6KipK/ve//9n0L5ruR/ukwW5Nm6P1aNFXvYHx+OOP2wC2BvidmwkayHdm7+sNAw1w9+7d27UZG4h28tg7KWvKHVDND069On69IaEBdb3R4Fo0/c+LL75Y9hSApuVRp88++8yaaboiXXBWZ+Xre92vN0n0hok+AaA3IfSmjAbunfE79WtaHj1OA/NO0ZsVTqnKSQ30qYKwsLAyx4pPcDh18YpAvQk0gnw3S00u+kKTasZds+gd+zYtWsmApCGyZO18yes1XIJb1k1KmLiIeFmRsky2pG2Uod1PTavltF/fr0s3LZWCI4eleWBLCWoZVN/NnVK/p8ykr9ixe6d2kzG9wuXvH2yWNBOo/8TMql+17YDcOqmTTOxX+t/oiufwGQEEEEAAAQQQ8DGPortvxR38EUAAAQQQ8CIBndWs6WR09rczO9p1eDobXNPRnE2gWevT/3RrTnudSa2ztzUg7szo1/26aGzFxVx1+9q1a+0M8N27d8vll1+um2ywV+vQ9DQaMNdZ41ree++9cnXajbX8pf159dVXbTDfdeFUTbOjTx1MnDjR7tcbDVquvvpqm4JHnxJYtmyZDWzrdl1QVvPHO0F93VbTou1rHWcqmvNf29KbEF999ZVouiK9tn5+fuIs2num80+3T1P66OKz69atk4suusiuYaA3DJxSHSfnWF4RaGiBNZnr5MHP/yTXjbtRQtudXR7z+u77m1+/IYH+LeXK0aV/ztV3e2eqf5/Ji//mrNdk3KDzpVfHpDMdWqN97855T7JzM2XCkEmSFN+9RufWxcE6i3/mghm2qoHmiYXhbkor9Oz7T0tiVJI8OvnhuhhWndfxry+2yeuzd5bVe+mIWPnJhI6iC89SEEAAAQQQQAABVwGC9K4avEcAAQQQQKCJCKSkpNjFRzUI7Ro4HzBggA0eT5061Qal65pDF23VGeFBQUGn3KzQ3Po6M13TBLkWnbGuM9PPOecc1831+n7atGly9913lwXp67WxSio/k1Mlh7MJgXoXcIL014+/SdqFtK339mrbQOGJ4/LSjOclLqqDTBl2aW2rqdPz3vjqDQkPiZRJJlBfV2Vr5nb5fMEn0t6kvjmnx7kmP322ZJqgfVZuhoSZti4ccqH4mye46qu8N3eaZOVk2Fn01064TlqbpwbcUTw9SK8mG9IOyuNmVv3GXXmWKC6yFbPq3fFloU0EEEAAAQQ8XIB0Nx5+gegeAggggAAC9SGgs9Gffvpp0dn9GzZssAH5Dh06VDnL/Gz74iw4W1k9mi5Gf3TWut5E0CcS4uPjbYqXyo6vz23OQr86u90d5UxO7ugPbSLgCJwoKXbeeuRr7sFcKSkuNgFqf4/pnz5ZlZ69u9b90RsPOi792W9m5u/P3yf7D5b+2ZRp6v04e7q0Mql0YkzAvpuZVZ8Y07VeA/QfmRn0GqDX0tMsjuuuAL0D2qldR+etR772jAuS1+4aJP/9dpe8+OlWmwLn/tfWy4rtB+Qn45lV75EXjU4hgAACCCDgBgGC9G5Ap0kEEEAAAQQ8RUBTuPTt29dTumP7oXnWdfFZdxYnt39ycnKdpf1x53hoG4G6EvDwGL3k5uXYofr7eU6Q/sixoyalWH6Vl6DIpNrKOZhjx5BrAvE6Fg3GH65wbmjbCIkKi5aEmARZv229yb9/VHqYVDoNkYN/2nfvy569pTcc4tp3knPN4rjuLq0C3DOLv6bjvnlcBxnfO1we+2iLLE/JkRnf75Y1Ww/ILed3JFd9TTE5HgEEEEAAAS8UIEjvhReVISGAAAIIIIDA2Qnokwaau/6NN96wCwD7NILFMs9uxJyNQPUESnw8ezmrQ0cL7EA8KUh/rPDoKbi7slMl/8ghyTucVxqUz8uV/EMHTjkuuE2IJJrZ8ZHtosxPpLQPaS++Pj8cFt42UmYtninLk5dITFiMdDCLytZXeeubtyV3f7atPsA/UCYNnmTWX3HpTH017EX1xoe3lH/d1lemL9wtT72/WXZmHhKdVb/SzKq/hVn1XnSlGQoCCCCAAAI1FyBIX3MzzkAAAQQQQAABLxfw9/eXKVOmyJtvvmkXA27evLmXj5jhIVBdAc8O0ge3amMH4ilB+gMmCH/ieFE53DwzM37GvA/LbdMPQa3bmtz1ERLR1gTkQ8JtUD7QL+CU41w3dItNFBk62Qbqtc7xgydKzw49XA856/fF5pK/8+1bJkC/t6yua0we+hYBLH5aBlLDN1cOi5VxZlb9ox9skXlrsuRjM6t+NbPqa6jI4QgggAACCHiXAEF677qejAYBBBBAAAEE6kjgvvvukyuuuEII0NcRKNV4hUCxh+e7ad2iNEifd+igR3jvytp1Sj+CTf74Gy/8iWxK2yRtTWC+batgaWdmzPs1q91fzVwD9d8smyU5eXtldJ9Rp7Rbmw2Z+00A+bsPpOh4oT09MKC5XDHmKtPnoNpUxzkuAqFtAuXvN/WSL1aGyd+n/zCrfs2OPLnRpMaJDOYmiAsXbxFAAAEEEPB6gdr9n6DXszBABBBAAAEEEGjqApobv3///k2dgfEjUEHA02fSB9v+7t5zanC8wkAa5GNqdpptR9PWuBYN1A/pVndrb2ig3nfYRbJo/UJZs3mlpOekS9+Efmc1q37N9rXy3cpvy7qdENdNJp9zQdln3tSNwAUDosys+gh59MNN8vniDPlgXpqs2LpfbjmPXPV1I0wtCCCAAAIINA4BgvSN4zrRSwQQQAABBBBAAAEE3C5QIp4dpA9q0doaHTOLqabuTZP48Di3mR06eljSs1Jt+53NIq/1XRKju0hMaKwsTF4gydvWyTf7ZsnG1BTp26WfyVXfQfybNatWFzJN3vnFyYskLXOHPT7SLFLbs1Mv6dUhqVrnc1DNBQL9feWBq3vI2N5h8pjOqs8ozVW/1syqv4FZ9TUH5QwEEEAAAQQaoQBB+kZ40egyAggggAACCCCAAALuECgu8ewgvZp07dhDNu9MkVQTIHdnkH6tmYleWHTMXqbO0fUfpNeGWgY2lwn9x0t0aIws2rDQ3iTQGwX+Jrd9RGh7iY2Ik/ZmAdqKZX/+ftlr0uRk5mTIfrOIrZ+fv3Tv1FO6xXaVDpEdKh7O53oSGJUULqMeCJcnPt4s079Lk/fNrPpV2w7IjRM6yMR+p163euoG1SKAAAIIIICAGwQI0rsBnSYRQAABBBBAAAEEEGiUAj6e3+uk+CQbpNd88CN6DXdLh3UW/YZt623bYSFREtOufYP2Iym+u3Rq31l27tkhqdmpsiV1ownY77I/Z+pIG5Mjf1jfUZIY00WCW5J3/kxW9bnvt1O6yrg+4fLY+5tlW3q+3P/aemFWfX2KUzcCCCCAAALuFyBI7/5rQA8QQAABBBBAAAEEEGgUAv+fvfOAr6JK2/hDeu+9ERIINfSOgiiCZW1rQURl7a69rN1PVte6iq7dteuKriuufW2oiEivoYUSAum998Z33rmZm5uQhJt+y3P8TWbmzKn/ieHe57znfZuONlr8OGOUtbi4aMlVVuG7j+ztkV/27k5WrOirayu16uOHje9uMz2q5+7sgpHKj7wc8ybMQ0p2CrKUr/ry6nJUVleioqocVdUVCA+JQriysg/xC0WCEueZLIPAxDh/fHz3NLz2wyG8820qreot47VwFCRAAiRAAiTQZwQo0vcZWjZMAiRAAiRAAiRAAiRAArZGwPLd3QjxxLhETaTfuHcDYpW7Fk83j357ESWVZUYr+piIIRCr9oFOjg6DNAGeIvxAv4mu93/9/DgtsOzjnyRjr/JRT6v6rjNkDRIgARIgARKwBgIO1jBIjpEESIAESIAESIAESIAESIAEzCUwKmYkJo2civKKEhUEdb251Xql3FrlC163op84dGKvtMlG7JtAQrgX3r1lMm46d5gGQnzV3/lWEr7fnmvfYDh7EiABEiABErAhAhTpbehlciokQAIkQAIkQAIkQAIkQAIGArNGz4SXpy92K9czyRn7+wXLDtXXQeX/XdKkUdMgrneYSKC3CFw2JwafPTQLk0cE4EB6mWZV//Rn+5FbaghQ3Fv9sB0SIAESIAESIIH+J0CRvv+Zs0cSIAESIAESIAESIAESIIF+IHDRSRdpvfyw/n99LtTnlxZg/a51Wn9jh03ArFEz+mGG7MLT1cuuIET4u+Hl6ybgnotHQtwY0arerl4/J0sCJEACJGDDBCjS2/DL5dRIgARIgARIgARIgARIwJ4JeLl74qJTLgEGDUJfC/Xi5qa2rhojhozGSePm2DP2fp17XGBcv/ZnKZ39cVoEvnt0NuZMDKNVvaW8FI6DBEiABEiABHpAgCJ9D+CxKgmQAAmQAAmQAAmQAAmQgGUTCPMPweWnXQFfb39NqBeXNL2ZisqLsfyn5TiSdQhx0cMwf9Kpvdk82yKBDgn4uDvh75eNxqNXJCrXTi6aVf1f6Ku+Q158QAIkQAIkQAKWTIAivSW/HY6NBEiABEiABEiABEiABEigxwT8PH1w8SmLEBsVj1+3/qxE9Q+Rp9zT9DRt3LcZH3z/HioqyjFv2un4w7Qze9ok65tJIC0/3cyStl/s1LEh+OGRE3HmjEjsp69623/hnCEJkAAJkIBNEnCyyVlxUiRAAiRAAiRAAiRAAiRAAiRgQsDVyQVnTz8L6QWZ+H3X7/j3jx9gaMxwDA6NxdDIeMhzc1JNXQ0yVBub921BXmEWwkOicOHsC8ypyjIk0GcEHJX53UMXjcCp40Pw+L+TNav6XUfKcMlJ0VgwPrTP+mXDJEACJEACJEACvUOAIn3vcGQrJEACJEACJEACJEACJEACVkAgOigSF6uAsuL2ZvfhPTiY9j1WbXVCVGgMhseMgK+nH1ydXdThqh2VNZXILMhAlhLk84vzUVCSh6NNTXBSov6kkVMxa/RMK5g1h2gvBGYkBOCrh2bi2S8P4ONf0vDQe6VISi3F5ScPRqivq71g4DxJgARIgARIwOoIUKS3ulfGAZMACZAACZAACZAACZAACfSUwLi4sZBDLOtTsw8hOXWP5lfenHZHq3oTho1HgHeAOcVZhgT6ncAdZw/DPGVV/xit6vudPTskARIgARIgge4QoEjfHWqsQwIkQAIkQAIkQAIkQAIkYBMExLJejtmJJ+KgCv6arSzma+vrUNdQq4461DfUI8g3SDv8vQIR6O0Hd1d3m5g7J2HbBMbG+OLju6fhnz8cwtvfptKq3rZfN2dHAiRAAiRg5QQo0lv5C+TwSYAESIAESMBWCRzMrsDQcC9bnR7nRQIkYIEEhkbEQQ4mErAlAtfNj8N85Zf+bx/TV70tvVfOhQRIgARIwLYIqPAyTCRAAiRAAiRAAiRgWQRe+TYF97y7y7IGxdGQAAmQAAmQgJUSGBLiibdvnoRbzktA8pFSZVW/C09/th+5pbVWOiMOmwRIgARIgARsiwBFett6n5wNCZAACZAACVg9gc2HivHeD4eRkVeJpqNWPx1OgARsikBeSYFNzYeTIQF7I7B4djS+/usJmDoyCCtWp+Pud3bi++259oaB8yUBEiABEiABiyNAkd7iXgkHRAIkQAIkQAL2TcBx0CAjgKajVOmNMHhBAgNIYFx4otZ7bX3NAI6CXZNAC4H0/IyWG151iUCwrytevHYc7ls0EgfSy2hV3yV6LEwCJEACJEACfUOAIn3fcGWrJEACJEACJEAC3STg6Ngi0oMafTcpshoJ9A2BzDwKo31Dlq12l0B8YFx3q9p9vXOnRmDl43Mwd2Iorert/reBAEiABEiABAaaAEX6gX4D7J8ESIAESIAESKAVAUe0iPRHaUnfig1vSGCgCdQ10H/1QL8D9m8gUFCSr114uXgSSQ8IeLg64snLxuCxK8Ygq6CaVvU9YMmqJEACJEACJNATAk49qcy6JEACJEACJEACJNDbBBwdWkR6+qTvbbpsjwR6RqBI+aSvqa+Dm7NLzxpibRLoIYGC0gK4Orv3sBVW1wnMGxsKOR79JFmzqt91pAyL5kTjtAmhehGeSYAESIAESIAE+pAALen7EC6bJgESIAESIAES6DoBBxN3N/RJ33V+rEECfUVgaOhIren0/PS+6oLtkoBZBEory1CpjujAWLPKs5D5BB68cASe//MElFTUYen7u/Dof5KRpizsmUiABEiABEiABPqWAEX6vuXL1kmABEiABEiABLpIwMkkcCy93XQRHouTQB8SSAwfo7WeknmwD3th0yRwfAL6QlFimOF38vg1WKIrBKYnBOCLB2di4dwYfLUuE7e/sQP/+Z3xKLrCkGVJgARIgARIoKsEKNJ3lRjLkwAJkAAJkAAJ9CkB08CxdHfTp6jZOAl0iUBcUJxWPrMgs0v1WJgEeptAfqnBH73+O9nb7bM9A4E7zh6Gl26cqN0sW7EPdynL+oPZlcRDAiRAAiRAAiTQBwQo0vcBVDZJAiRAAiRAAiTQfQIOrSzpj3a/IdYkARLoVQJjw8dq7YmbkdzmoJ292gEbIwEzCSSn7tH80eu/k2ZWY7FuEJgy1B+f3jcd58+Oxuptubjhla1Yvpour7qBklVIgARIgARIoFMCFOk7xcOHJEACJEACJEAC/U3A1JKeEn1/02d/JNAxAS8XT8wcepJWYNuBrR0X5BMS6EMCuw7vRn1DHSYNngb5nWTqHwJ3n5eAZ64Zi/BAd7zw2X7c9lYSdqeX9U/n7IUESIAESIAE7IAARXo7eMmcIgmQAAmQAAlYEwGTuLFoor8ba3p1HKsdEJiXcIo2y/1H9kKCdzKRQH8T2Lh3o9bl4kmX9HfXdt/fiaOC8d5tk7FkQSzW7crHDS9tw5srD9s9FwIgARIgARIggd4gQJG+NyiyDRIgARIgARIggV4j0MqSnqb0vcaVDZFAbxAYF56IYWGjtKZ2K6GeiQT6k0BafjoqKku1HR1hXiH92TX7MiFww2nxeOGGCYiP8sIb36Tg2pe3YuOBYpMSvCQBEiABEiABEugqAYr0XSXG8iRAAiRAAiRAAn1KwHFQy8cTavR9ipqNk0C3CCyauEirt3nPOohoykQC/UGgpr4O/1v7DdxcPEAr+v4g3nkf04YF4M2bJuLy+bHYcbAYNytf9S8owb6+kf9yd06OT0mABEiABEigfQIt34Lbf85cEiABEiABEiABEuhXAqaW9E1Hm/q1b3ZGAiRwfAJiTX/e+Au1gqu2rIKIp0wk0NcEvlr7Jerqa3Dj7JtBK/q+pm1e+xLo/cbT4/H8nydgVJwflivXN1c8vxmrdueb1wBLkQAJkAAJkAAJGAlQpDei4AUJkIAlEEhKK8U1L21FdV2jJQzHOIa80lo8/81BbFKWQkwkQAJ9S8BhUEv7R5tMblqyeUUCJDDABC6dtFhzOVJSUYifN/wChyZ+rRjgV2LT3X+3+Qdk52doi0MzB8+w6bla4+SmJwTgjRsnYvG8WBxQwWTveTMJT/x3H0oq661xOhwzCZAACZAACQwIAacB6ZWdkgAJ2BSBo2pX6+6MMny5MRsb9hYiv6hGm198tDdevn4CfNzN/1Oz/VApklKKsW5fIU5OHHhfow0qaOX7v6ThTbV9t1FN9LPfMrHqyTkD+v7E52dUkDsi/N0GdBzsnAT6ioCTY4vYdxTcNt9XnNkuCfSUwHUzrsGRokM4mLMXK1ZV44qTr0QpinraLOuTgJGA7NIQC3oR6GcOPQmyOMRkmQSc1Ar7LWfGY/JQX7z2XSo+/y0Dm/cV48r5g3HmpHDLHDRHRQIkQAIkQAIWRMB85cyCBs2hkAAJWAaBC55cj+KKelRX1WsCtj4qL08XNDY0YX9aGXakluLEUYH6o+OeG5UoLulQbhUSIqqRU1wDJ8dBiA/zgncXxP7jdmRGgUM5lbjxn9tRVGJYdJAqF82JMqNm3xb5YFU6ahoa8PqfJ/ZtR2ydBAaIgPpf3piaZBWQiQRIwCIJeLl44vEzn8Czv/4DO9I24ekvn8RNp94GZ29HVDaUW+SYOSjrISDxDn7e8hPKKkpw6bQrcN6Yc6xn8HY80pnDgzAlPgAvf3cIH/10BI98sAcblIHJdfOHIDLA3Y7JcOokQAIkQAIk0DmBQUdV6rwIn5IACZBA+wSm3f6T9sBR+aM864RInDc1AvHhXnBWCpuI7R/+lq5ZzgR4ObffQHNuYXkt1u4rQnp+FX7ekY/03Mpjyv9hRiT+76IRx+T3VcaPO3Kx9L3d2uKDv7crzp0VgZkjAzE2xrevujS73bve3YVNyYUDbtFv9oBZkAS6SEBizs28w/D35bOHZqpdI/xS30WELE4C/U7gs11f4IMN72j9zhp2EsYPGQ+fAG+K9f3+Jqy/QxHnN+3djMy8I1qQ2Lvn3QeJg8BkfQTWqB22/1RivRjuBKodoFeeGosL1Gd6JhIgARIgARIggWMJ0JL+WCbMIQESMJNAdKinJqh/sXQWgn1dW9VyVFteL5sT0yqvo5urX9qGrLzWwrwI/wmxPkiM9cVo5TZn5oggY/X0giqsTMpHVmE1IgLd1UJAGEKa+5dn4cpKR7bcmqZ6Zdnv7NTiQsP0Wdvrf/5wCG9/m6pl3/rHBFx8QjTaNNe2CmS58/vtOdh5uAwS9HL8EF/MHRMCNY1eT+5uDqiubYC44mk7z17vjA2SwAAQaGVJb1nhKQaABrskAesgIFbOY5WQ+sb6N/H7gVXaMSxsFOaMmI2QoBDUDKpGw1H6p7aOt9n/o8wtyUeGcmmzLy0ZBcW5mjgvwYnPTTwXsmODyToJnKAMXKYM9des6j/++Qie/k8yNigXONcsiEWCMuxhIgESIAESIAESaCFAkb6FBa9IgAS6SCDEz1UT6XWB/kB2BX7dXYAdyq+8iOWNDUcR7O+Kp/80RrmsccQ3W7Jx4cwoFJTV4mblRqZUucp5+7ZJiA31QL4S3EcpYVvSDhWc9Zo/xCvftoOPGZG4oLn8mY2ob2wyPnvj6xTcrMR0EdKfXbEPIqxfcmK08fm1L2/V2nxUjePUcaHG/PYuvt+eqwn0skjw5NWJeOWbVPym5nT21HBMTwiEfwe7ApZ+vAffb8g2NvnxL8CwaB8suzIRoX5uEC8+/1PzF/c/g5QCGRvs0aH4vy21BK8qq6M9KaXaPGUx5I+zIjV2skvBzdlR66e0sg6ByspfUlVtIzxcDflaBn+QgJUTkP8HJQ6E+l+HiQRIwEoIxAfG4ckzH0dK4SF8vutLrD24Cgdy9mijjwqIxZCQOESGROKoo1p9U/+ehfoF99rMROStra/ttfbssaEQvxD1GcOlT6cuPubzSvK0PjKUxXy+em/Z+Vmoqze4FnRz8dCCw1Kc79PX0K+Nuzo74I6zhmJSnC9eV77qV6vdqlvUDtolylf9krnHftbv18GxMxIgARIgARKwIAIU6S3oZXAoJGBtBPJL67Qhz7nnV9TXNx7jl97DxRG7D5VgX1Y5dh4p08Tv+DBPPPpRMvKKq7W6jytR/VUVXFZPGw4U4RYl0ouleHvpyU/3acK1CODnTI9AeXUDPlmTgX+o/BHNIv/keH9jVfGZL6K/pNiQzi2xROh++P3dWtnnb5igWeenqrGnqpyt6suEJPG3Hx7ghlHKyv+MiWEYryz992VVGAX6y+fHIlJZ969XVkK/bM3BJc9swjcPzcJ1r2xF8pFSrQ1dfNylmDy2eHQra/ul/96L7zZkGcsNifBGmlr8eP6/+/HFuiy8ftMkuDQH1axViyCyQ+Du93dh7c58jIrzw9vqeV9Y72sD4g8S6E8CsgtFU+gp0/cndvZFAr1BQMT6O+fchsWTLkFS1k6kFqZgd84u/Jb8M5B8bA8uzm5GkfbYp8yxdQKy42JM2GgkRoylWxsbftlzRgcrq/oAvPJtCj75NR2vfHkQG/cX4Zr5cdoOVBueOqdGAiRAAiRAAmYRoEhvFiYWIgESaI9AvgrqKqmmrkEZxA3CbGWlfvqkEJwwMgguza5lxDe9uL5JzjC4s7nznzs0kT0hxgepmRXYk1rWqmnXZivxyppjfVyIexcR3J2VSP3qnycYA8kumRuDTSr/JWVRLylOLQTo6avNBuv20UrAHnacbbXr9xdqCw1SVrbmigubJQvisHZPAQ6kG8ZZoazXD8ih7r/4LQP3LRoJse6XJAL9jafHa9fnKv/8WWo3wIHscvzp+c0QsX9mYjDuVVb+YlkvCxs/bcnBlGH+OG9ahFZHfvy40TDeAFXm7VsmIVz576ypa8Ir36Xg41/ScPtbOzBpqJ9WvkD58r/19R1Iy6nQ7veoBZF//56BRScMfHBbbUD8QQI9IODo5IhG9bdFdqEwkQAJWCeBMK8QhCWcogYvB1BRV6lZ2Ws36sfOrCT9kmczCMQFxcHTxbJchBxSuyYqaw2fQ8yYAkK8wxDqHWIsSl/zRhR2cSG7Pv9ybgImq8/ZrynXkpuTi7Btf7H2Gfp69ZmbiQRIgARIgATsmQBFent++5w7CfSAgAjYYu0eogI6LpobjXOUKO3ZjrsVEegliYsbSeKmJkxZmr9x4yQ8+/UBTeguUtbuenDZJmlYyjWf5VpE8BplqR/obdiCPSTSyyjQy3PpY3pCAJ6s2actFsiCgaQKJfS/3izcP3jh8YPOlimrfEm6n3dp5obThmjHu78cwavK4keEeHHZI0FxP/rpCD5alY6YZgv9WSMCtfr6jwglsO9OK9UE+hlKoH/uyrHaI9ktIAsbkp7/7AD+MDlcC7Yr9+4ezpCFgLeaBXrJc3NR24TPHoYft+VpOxOmN/dzyyvbtXcwTn3RuVAF7n1QBZT9fG0mRXqBxmT1BPQIEkdbPFtZ/Zw4ARKwdwLiW9xUlDW9tnc21jp/vkNrfXMDO+6TxgQroT4Aryqr+hWr0/GOcoOzQYn11ygXODOHt8ShGthRsncSIAESIAES6F8C+nfg/u2VvZEACVg9gaIKg+geHuiGiUP8kJxZhp935mnitQRefVl96BYhe+2+Am2u2c1W9+6uTnj71kma8DxdWZFL2nbI4I5GrgM8DEJ8WWWLu5vrXtqKRz9ORmmVIa+xsX3TWnFDIz6sX/jfQRzOrcIVyoJdFgVmjQ1pZV0v/bSXxHpekljrf6Rc6NQ395NXWqv5kpdnHmr8EqR2xvAAuUWoWqQorTa4/WloZ1z7lSscSYtOjNLOKWrB4Z63dmrXspggCx3CSk/OzYsaut95PV985ReV1CBCLQhU1hgC70ndaaOC8fqNEzVf+z7KFc9h5RpHH7del2cSsEYCjk6GxTZ94c4a58AxkwAJkAAJkAAJtE/Ay80Rd52XgMevSESU+nwrO0Jvf20Hnv3yAD/Lto+MuSRAAiRAAjZOgJb0Nv6COT0S6CsChcr6XZII2kuWbey0m/89cgKKygxC9j0XDTcGO02MMQSKXau2up6ihHRJoSrQrKSd6oO6WLavUJbhZcqyfO7EENQ1B4vNLWk/MNzlKtCs+I7/cOUR7dAaUj8umBmuX3Z6jgxwx5kzIvHNukzNx/3Lnx+Aq5uTZtkuFWXXwMJZBrF9xe8Gv/FzEgPxzaYcrd20gmpt+65pJyMivbXbW17ZhnAVLDY7v0q7v/GcYZit3AJd+vQGzSI/X83pwYtG4OQJIfhUWRRd8vcNmD8lFCWV4lO/BDkqsK74w39d+cpfpiz6Jcn9k5eP1q7lhzASFzzbFLupzQsgxoe8IAErIzCoeUcMRXore3EcLgmQAAmQAAl0gYB8BxBf9S8r146fq8+x4t5xg/o8f/WCITi1+ftBF5pjURIgARIgARKwWgIU6a321XHgJDCwBPyVWxbTJBbyicqX+5hYbwxWYnSQtyuClcV5kI+r5gbnrvMT8F8V+PR0FWxVT/J8rAryWlRhEPAl310Fm/VXdQuU1fip9/+qFXVzccKNp8UbXcS4Obe/CWiGcnnzsBKt//HZQVW2UbNSF//104a1dkOj99/e+SEllE+M88W7SujPUaK7WLZLQNY/zojAmZPCIYbuhcoX/OoduZprHQkeu1WJ6BJu1lNZBLVN8sVjwbRwLbCsCPQirN97YYJm+S5lX1O7Cq5/fgtWKv/08cpn/i1nDoXsOtiwqwAf/5ymNSfzv2B2NK6YN1jjGupn2G3w9FWJyrK/pc/rTo3F12sylR/8Cor0bV8E762OgO626igMFvVWNwEOmARIgARIgARIwCwCPh5OuO+PwzE53g8vfJmCw2on6oPv7MR6ZTxz61lD4eNO2cIskCxEAiRAAiRg1QQGHVXJqmfAwZMACQwYAfH5Xl5dr/lwF8G9O0kPLGtaV4LAPrR8D44q9zGnTQ3D5ScNNvqsl2fOKijt+FiDFb5pPdPrLCV0n/fI75g3KQyPXdpibW5aprvXIoJfqizdJRCs+Jmvqm2EBKg9X32R0P3Zt227XO0KqGsQv/rHcqpraMJ+1eaYaB9jNQmWKS6F3JydINuB2yYJGisLIW3TFuU6KEwFnZVdAUwkYM0ETvvrGhQrV1P/umsaEiIsK1CiNXPl2EmABEiABEjAkgmUKPeWryhXkF8o15OSJJbVFfNjca6Kf8VEAiRAAiRAArZMgEvStvx2OTcS6GMCIh63JyB3pVs9sKxpHfEN/+3SWaZZxmvdb7wxo4OLLzca3NFcOCuygxLdzxZrf826//R4rRGxZtfd4HTUqrdmAdT+n1wXtehgKtBLG2Kx354Ir7ff0bNJcQa/+no5nknAWgnoC15NsmLFRAIkQAIkQAIkYBcE/JRV/f3nD9d2tj6vrOrF5eMTH+3VAsvecmY8wv3d7IIDJ0kCJEACJGB/BNr3GWF/HDhjEiABGyPw6W+ZkECq41VQ295OUcqi59en5mBouGdvN832SIAEmgnoPukp0fNXggRIgARIgATsj8BpE8Kw/M6pOEvtVJX0s3INueTZTfio2cLe/ohwxiRAAiRAArZOgCK9rb9hzo8E7JCAuHyRYLNnKz/yTCRAAtZJwNHR4IuegWOt8/1x1CRAAiRAAiTQUwIBXs54UMWLkphTYnxTquJY/ePTfbjtrSQczK7safOsTwIkQAIkQAIWRYAivUW9Dg6GBEigNwj8e3Wm1szZU8N7ozm2QQIkMAAE9MCx9HYzAPDZJQmQAAmQAAlYEAGxqv/onqk4s9mqft2ufFz1j814c+VhCxolh0ICJEACJEACPSNAkb5n/FibBEjAwgjUq2CzvyflwdnRAYODPSxsdBwOCZCAuQQcJDCDSkfVf0wkQAIkQAIkQAL2TUDiMT2krOofunQUJB5UTV0D3vgmBde+vBXbD5faNxzOngRIgARIwCYIUKS3idfISZAACegE9EC0wcpvPBMJkID1EnBo/oRytMl658CRk4C9E6iur8bhoiMory2zdxTdmj/5dQsbK9k4gTMnhePje6djQfOO2R0Hi3Hd85vxghLsmUiABEiABEjAmgk4WfPgOXYSIAESaEtAjG+XXT8eIT6ubR/xngRIwIoIODWr9EeP0pLeil4bh0oCGoG8ygI89dOTOJx/0EjEzcUDf5p+FU4ddooxjxftE+gKvyd+fgql1aV4aP6D8HDu+Q7C3m6v/RkylwR6RiDEzw2PLBqFKcP88dTHyahvaMJy5fpm474i3HDmEMwcHtSzDlibBEiABEiABAaAAC3pBwA6uyQBEuhbAjMSAhAf5tm3nbB1EiCBPiWgW9IzcGyfYmbjJNAnBP657nVNoHdydMbY6ImIDhyiXFNU4bXVL2JbVlKf9GlLjXaFX1LGNhzI2YP6xvpeQdDb7fXKoNgICXRA4KzJ4VjxwAzMnRiqlTiQXobbX9uBJ/67D40MatMBNWaTAAmQAAlYKgFa0lvqm+G4SIAESIAESMCOCTg66j7pDWc7RsGpk4BVERAr8O1HNmpjfvWi1xHg4a9dr0/biOzyHEyIGKvdywJcY1MDHB0c4TCoxW6oQeVJcnIwfE1pampEo/J75awE/+yyXAR6+sPF0QX5qh9vV2+4ORl2zplbTtquqKtEbUMtXFRdb5f2F/X19qS8oxqfgxqnxMiQOUR4GwLTy31DYwMGDRpkHK+Ul6Q/c1DPHJvnYnjS+U9z+bUV5euUSK/ntR2POfPV6+qj66w9vYzUySrPQqBHELw64KiX5ZkE+opAmLKqf/KyMfh8eACe+mgvxEve579lYPO+Ylx/xhCcOs4g4PdV/2yXBEiABEiABHqLAEX63iLJdkiABEiABEiABHqNgAhbkppoCddrTNkQCfQHgaPNgSRE1PZ18zZ2OT1mqvFaLtanrceylU8hUVna/3X+Q8Zni95bqP6/b8THV6zQhO+lPzyMPZlJiAmKR1pBCsQ6/6SEeVi591tNOL9/wVJN+De3nHT04pqXsDl1ndanjDMqIBZXTbsSY8JGG8fxzK/PYcOhNdq97ARYOGEhXlr9grYjwNvdD5dNXYKZg6fj0vcXaWVeu/hNBHu2uNhYf2QDnln5JMJ8o/HyBS8a2z3ehTn8CquKcO1HV7Zq6vp/X93q/q3F78HPzVfLO958u9peuVrkWLZqGXambzX2KfN84NT7EeFrWMAwPuAFCfQTgXOnRmDKUH88+8VBrEnKQ0ZeJR58dxd+m1aIu85JgLc7pY9+ehXshgRIgARIoJsEWsxWutkAq5EACZAACZAACZBAbxNw1EV6+qTvbbRsjwT6lECoVwiCfEI1of3mT29Gct6+TvsTa/r2Utt4FA2NdfBy81GW6/XYoVy8DA0dqfWxOmV1q+rmlIvxj0FC+GhEBsRobYj4v/SbB5BadNjY1oSoCZgUO127L6zIx4dbliPCL1pbLCivLsFrv72kWfdPHjJDK/O/vd8Z68rF2sOGRYAZ8YbnrR52cmMOPw9nd0i/+vikOVnskHv90HciyLPjzber7T3242NGgX5ISIK2cJJTmo7/+/ZB0EWZEGcaKAKRAe5YdkUi7r14pHEI32/IxsKnN+CLTdnGPF6QAAmQAAmQgCUS4HKyJb4VjokESIAESIAE7J2AQ3PAWMaNtfffBM7fCgncMvs2PP7D35Bblo0HvrpHE7bvOvkvRjcx3ZnSH8ddAHGFI37tzxp7jubq5vncvShUbm9MkznlFk9YBEww1JJFgr8rq3CxrP9i1xe4bfat2gMJcCvH+W+di6raCiRGjsfdc/+iPVv47oXaYkFeZT7OGX2OVndl8ndYMvlS41C2pm3SrmfHzTbmmXtxPH7uSqS/7+R7tOYWvX8x6uprcLsat2+z5Xzbfo433660d6g4VfOBLzsQ3lz0ttanuL25/j/XoaSyEEnZOzG+2aVR23G0d//At/+Hw2qRpKMUFzwUfzvtkY4eM58E2iVw3rQINyQsvAAAQABJREFUTI73x9Of78OG3YUoLK7B4x/uwRp1fee5QyEucphIgARIgARIwNII0JLe0t4Ix0MCJEACJEACJKD5eBYM4luWiQRIwLoIjFZW7v9c+AZmxBsEarFUv3XFTfhu/4/dnoirk4uxroujays/9sYH6sKccmLtvSVjKz7b/SU+2vEfhHgZfFanl6SbNtXq+uzRZxnv75n/IG6Zeyd8XH0wKnQE/DwDNSF/e3NQ3LSSNKNbnBjfKGM9cy96m1935tvRWFMKDmmP4oIT1AJJEQ4VpiK9JAODg4Zq+enKor4rydTiv716XfHn31595tkvgeggd7xw9XjcddEII4TVO3JxyVMb8dGaDGMeL0iABEiABEjAUgjQkt5S3gTHQQIkQAIkQAIkYCSgu7vR/TMbH/CCBEjAKghIING/nHQHipWv97c3vou1B1fhrd9fwynxJ2luYgZqEtX11bhpxY0oUX7d26ZG5Qu/oxTl1yK2T4wY16rYmWPOwvIN7+KrPV9rVuS/K3/0kmbFndiqXFdueotfd+fb0VjFf72kg2oXw12f335MsYq6qmPyOst4WMUUYCKBviRwwYxITFW+6p/4dB+27itCZU09/qGu1+zOxx3KV318WPvBo/tyTGybBEiABEiABNojQJG+PSrMIwESIAESIAESGFAChrCxDBw7oC+BnZNALxDwV0FW75xzG9KLjyBdWV1vSN+EE2JnttuyuJ6RoLF9mV5f/5Ym0A8LG4XzEs9DuAp0ujc3Ga8rH/OdJRHNO0rzE07VRPrtRzaiqr4KG1LXakXnNO8k6KieOfnH4+cwyLAxuryuol13N12d7/HaC/MO04bt4eqFS6ZcdswURoeMOiaPGSQw0ARigj3w6vUT8MnaTDzzSbI2nM3JRbhs/wZceXocrp4XO9BDZP8kQAIkQAIkALq74S8BCZAACZAACZCAxRHQRfqjR/lRxeJeDgdEAp0QyK3Ig7h7MU0ivot/etMU6xer3e7P2WsU5n9O+dW0SJ9c78vZrbW7aOIiTIuZAnFHU6B8y/ckiYA/sTnI7Bd7vtEWI9xcPJAQPKzLzZrLT2/Yzz1Qu1zVAbuuzvd47Q0PStD6Ez/9nmrepw9f0OqI8Y/Wh8YzCVgcgQtnRuLje2cgUfmrl9TYdBRvfJOCK17cgqS0UosbLwdEAiRAAiRgXwRoSW9f75uzJYHjEsgsqkZWUc1xy3WlwKaDJV0pzrJtCEQEuCIywL1Nbu/fjoj0hrc7/1nofbJssTsEBjkYxPmmo/RK3x1+rEMCA0Xg98O/K6vy99S/J36IDojBIGUTdCAvWQtu6uTojLFho7WhRSgLdvHlLsFGF3+wGD7uviiqKIAEJBVr+ru+uht3zLmj16cRHRirLRg898syTIyZivyKHMhCgYuzGzKL01S/9+DmE27CJ0krUFFbbuz/4R8e0a5vmHUDgj2DjPn6xTnKZ/3Ww+uxYvNyLWtKs2ivPzf3bC4/vb3Jg6fg66R0fLb1P1i59wcMCYpDWU0Zrpx2FcS3vbnz1cX147UX7hOKuSMW4Jfk7/H8z8/gVeeXMELtSpAY3/UNdXjsjEf1ofFMAhZJIDbUA2/eNBHLV6fjhc/2a2Pcc6gE1zy3GZfMG4xbzxxq9rivfmkr/nxGHCbF+ZldhwVJgARIgARIoCMCNq/GHFWBofSjqcnwRV+/18+NjY1wcXHRytXW1sLNrXW090GDdHs+GAPZCVDTfNN7adehWVwwzZdrJhIYaAIiwm8/XI7dGeXYcagUBcXVKCnrXVF+oOfI/ntGwN3VSbMwmprghynK0mhElHfPGmRtEugGAQcHkXyg/dvcjeqsQgIkMEAEwr3DERcyHBnKvc2ezCTjKCKVYH/DrBvh4+ZrzLtqxtV4Tgm9dfU1KGqsx5Uzr8WKrR9r7mjENU5RdbGxrOmFxKxwbHbzYprf9rq9ctfNuBa1DbXYnbkDv+77QRPnr5x5Hf6z9SNtweBQ3j6t3/WH1hgt/KXdpPStWvPVddVAO55vxqjFBz+PAKOv+wXKwrw7qSv8pP1L1Y6A2oYa/LLvR5RXlxjHmVp0WBPpzZ2vLtIfrz3p8wbFK8I3DJ+odyXvTmcjzyRIrYPJdyfJYyIBSySweHY0picE4JF/70XyEYMV/Ycrj2DtnkLcfs4w7dnxxh3s54onP9mH12+aBH9Pm5dWjoeDz0mABEiABHpIYJASlA3fgnvYkKVUnzBhAnJzczUBXZ+aLqabCuem4xWBvq6uThPW3d3dIUK9lHV0dNSKmV5LW9Kui7OzoQl1L3lSxrQfuZajqqoKnp4tn+TbjkGv5+Pjg7KyMq1NfdxyIwsLsoggZ+Oh7usbGtDQfMh4r776aixdysBLhpfCn20JiDD/6vdpWL0jR/1+Nxgfe3u7IjrMD77ehoWp8CAvuLo0/24bS3XvorauHtkFFd2rbKW1/BRHP+++t3jvTTyHswwCyJEsw5eTrJxjdz0E+rvh2tOG4NypEb3ZNdsigU4J3PZWEtbtyseTV43F3DHBnZblQxIgAcskUFZTimoliAe4+3cYLFZ2y+RU5CLMM0R9nnZERV2lJvK6OrrA0aHvRC/pV4Kg6lbxvdHvhrRN+PuPjyHUJxyvXPhqj1+KOfz0TmT3QZbaFSAiuY+rN/xMFkOkTFfne7z29H7FB79wdFHvK9gjUHuH+jOeScBaCLz7Sxpe/fJAq+Gec2IU7j0vodNFp5ScSlyjXOVMHRGAJy8b06o+b0iABEiABEigqwRsSqQXcTsqKgppaWlGgd1cICKE19fXa8K3nNseIojrednZ2fDz8zOK5LpY3t5ZxhIeHt7Kml/GqQvuci2H9G8q8usLA05OTnBWCwKykODq6qpZ+ctCgoeHB7y8vLQjJycHV111Ffbu3WvudFnOTghsPFCMt35Kw/Z9BcYZxw0OwuAIX4wYEgR/L1djPi9IQCeQmlUGEe9FuDcV7YP93XH1abEU63VQPPcpAV2kf/zKRJySGNKnfbFxEiABEugJARGqf1E+4d9f/zYa1I6AW+beiTlxJ/akSdYlARLoZwIHsiuxdPlupGS2uLkKC3THLecM7fRziB6MdrFylXNLF1zl9PP02B0JkAAJkIAVEOg785QBmLyI3W0t1c0dhljN65bz5taxlHIi7ovIz0QCpgQeVVsvv1qbYcwaNSwUc6bEUpg3EuFFRwSGRPhADj1t25eLTTszkF9UiSc+2otVuwrwt0Wj6L9eB8RznxBwaPY016SCujGRAAmQgCUSqFE+2K/7+GpUKB/wejpnwoUU6HUYPJOAFREYFu6JD/8yFW/8eBhv/i9FG3lOYTXuf3sn5k+JwN3nDWv3s68Eo92pgs4uV65yIpWof/70SCuaNYdKAiRAAiRgSQRsSqQX63RJItbbUxIf+mLFz0QCQkBc29z6RhLScwyuZiKUO5tzTh5OcZ6/Ht0mMGF4KOQQC/sNSqxftzMfVxduwcNKqKe/+m5jZcXjENB3l1GjPw4oPiYBEhgwAq5OzppA7+HqhZjAIThz1BmYOXjGgI2HHZMACfScwDWnxmL26EA88P5upOdWYuQQX/ywKQsb9hbgprOH4uwp4cd0cufZCUhVlvgvfHYQQ0I9MXEIA8keA4kZJEACJEACxyVgU+5uxK98fHw8UlNTIW5i7CUVFxdj4sSJSElJ6fZOAnthZevzFIF+8d83orrZ77xYz587d7itT5vz62cC63ZmYu3WI5qPztdunEChvp/520t3d727S8XRyMUjS8ZgwfhQe5k250kCJEACJEACJGAhBF75NgXv/XAYf5wdpYIkH8XX6zJxwtgQ3PPHBIT4tnYbuuNwKW5/YwfCA9zxyp/HwdfDxUJmwWGQAAmQAAlYCwEHaxmoOeMUS3qxvLM3S3o9+KwEkGWybwJiQa8L9KfPGU6B3r5/Hfps9jMSI7H4D+Pg7uqIpR/tQXk1d/L0GWy7btiwK87e/k2361fOyZMACZAACZCABRG44fR4vHvnVKzdW6QJ9D89cRLWJOXhwsfX46M16a1GOi7WF3colzgHM8qw7IuDrZ7xhgRIgARIgATMIUCR3hxKFl5GRHo5KioM7k0sfLgcXh8RePzTfUYXNzMmDtbck/RRV2yWBBAW6IlLzxiD3KJa3P7WDhIhgV4n4NDslL6R/m56nS0bJAESIAESIAESMI/AyChvfHH/DCw6ZTBOuW8VFs6NweXzY/CPT/fjz69tQ6pyiaOnP0wOx5L5sfh+Yzbe/vmIns0zCZAACZAACZhFwKZE+p4EjjWLloUW0kV6Whta6Avqh2FtPFCML9YYgsSKD/q5kwf3Q6/swt4JeHl7YOG8eOxMKcGTn+23dxycfy8T0EV6oDmCbC+3z+ZIgAT6nkB941HIIalBLbjJtTmhox7/7z5c+eIWpBdU9/0gzeihu/Mwo2kWIQESsBICt/1hKN68fQp+2paH178+hE8fnImt+4pw8ZPr8dZPqcZZiPX9yZPC8M+vDmL1nkJjPi9IgARIgARI4HgEbEqk1wPH6ufjTd5WnouLHznq6+ttZUqcRxcJfPR7llbD2dkJC08b3cXaLE4C3ScQERGCkWp772er07Wgxd1viTVJoDUBXZpvMkfRa12VdyRAAhZAYGtqCU74y8849YHV2mgWPLRGu9+cUnzc0W3dX4zdh0pQWj3wn217Mo/jTpQFSIAErIpAYowPvlk6CxfMjsb5j67F4nmD8cAlozTRfsk/NmNnWpk2n7vOTcCQCG88+/l+fj62qjfMwZIACZDAwBKwOZHeEnzSyyJBdXU1ampqUFVVhfLycpSUlKCoqAj5+fnIyclBVlYWdu/ejezsbOTm5qKgoEB7XlpaqpWXelJfguE2NDR06mdfF+mlLJP9EZBgsWt35GgTn6x8hbu7ONofBM54QAn8YeYQrf8vNhoWiwZ0MOzcZgg4qMVnSRTpbeaVciJ2RkD/f9it+XOJ/qXD1UW/sg4gtjIP66DNUZKA5RFIzihHXkkNGpp3BckI7zovAa/dMglfr8/GYx/u0Szsc4tqcPVzm/DiNykI8HLGHecOQ15hNZ75/IDlTYojIgESIAESsEgCThY5quMM6owzzsDevXvh6OgIZ2dnODk5wd/fH+VlZZrYfdJJJ2nPxA2MnkyvRdQW0dzX11d73NjYqIng4i5GPyorK+Hq6mq4V6J7ozp0dzIiwuvW+nLW86UxaUval7q6Zbvet+Tr11JWxHe519vQz/oYTO+lrMz7rbfekqqtkrQhbev9tXrIG5sn8J91LVb008dG2fx8OUHLIxAQ6IvYcC/859dM3HBavOUNkCOyagLqn1kmEiABKySgGw04OhoW3FycDZ/L3ZyONSaQDTMZhVXw93KFl9uxz02nX13XqMpWIyrQvVPDBGkzRwlrEtdCynaU6hqaUFheh9LKeniqvn08XODr0fIVqSvz0PvQx6ff80wCJGCdBP7yzk78pgLF6ik+0htThvsjcbAPxsb44oe/nYjHPknWxPnF82IRG+KuRPu9WL2rAHeqILI3njMMLyiXkK98l6o+IxuMWvS2eCYBEiABEiCBtgRaPoG2fWLB9zt37sSGDRtQW1trtDqXoKkiUov1uZxF1BaRWyzVQ0NDtdmIkK2n/fv3Y8SIEZqluwj8IvSL6K8L/6mpqRg+fHirfClTXFwMNzc3bVFAyuoCuX5tKsKL2C4W85GRkcZyevmunpOSkrBkyRJ9+K3OuvhPkb4VFru5+WETrejt5mVb6ETLa4/i9GlRePXzZHymrOnPmxphoSPlsKyJgEPnOp01TYVjJQG7JODqaBDlXZot6Z2bRXrX5rMO5eedeXjovd3KX71hRe6EsSH6o1bnEiWi3/3uTuw42OIuZ9xQf/z9T4nw83Q2lq1XovsL/0vBp6vS0ShKvUrOaiynTA7DXxeOVIYthqIHsyvx0PLdSMksN2SY/FzzzMmqjqGgufPQqz/wwW6s3JKDU1UAyUcXj9KzeSYBErBCArecNRSjY32w41ApdqaWan8v5G/Gv5vnMnKIL8aq45oz4/HhL2morKrHsmvH443vU3Hrq9twnnKLc+aMSLz3/SGMVcL+CSMDrZACh0wCJEACJNBfBKxSpBeB28PDAxERli8ERUdH98q7lDmbWuybNioiPS3pTYnYz3V5dQOKSmu0CY8fEWY/E+dMLY7A0MHBakzJ2JdZYXFj44Csk4DuYoLubqzz/XHUJKC7tXF1Moj1xrOJSJ9VXIP73t6pwRKxq1G5k1ijrFYddSXdBOOtb+xA8pFSLScixBNZeZWaYC/579022VjyfuV6YvW2XO3ex9MFwX6umrD23YYseLo64m7lpkKs669+fjOqaxvg5uKEMXE+8FYW9CLwy98cXaCXRsyZh7FzdZHRHOw2Pb/KNJvXJEACVkggJsgdV8wdDMw1DH6vcn2zQ/0dEsF+t/I/v1ed5ZDkov6WBPi64s7Xt2NmYggeVIt0jy7fgyD1NygswB1Pf7oPJ6hgs0wkQAIkQAIk0BEBqxTpxWpdrOjtKXUm0gsHeU6f9Pb0G2GY68YDRdpFUICntkXc/ghwxpZCoLJ+kPoS4ob96ssLEwn0BgFdpG82hO2NJtkGCZBAPxLw9XDGwrkxyv2Dp9brRbOjcCSvCj7uLVbv7/1yRHsmFvGv3zhRu96gPtvc8sq2ViMVn9C6QP/enVMxIsobIpb9adlGLX9fVgWGR3jhcG6VUaB/+PLROG2CwYBhk7K+f+XbQ/jTyUpsUylbLQ6IQC/p3/dOQ7i/m3bd3g9z5mFa75krEvHDjjzMH9f+jgDTsrwmARKwLgIj1d8eOS6e1eJidOOBYmw5VKwJ98nphs/Ba9UOITn8lWjfhEHIUTHEJIn7HPkbwUQCJEACJEAC7RGwSpFeBGl7E+lNXfW0fZHyTHzhiyseJvsisKv5g2BMhL99TZyztTgCNUrrEL/0m/cWWNzYOCArJdCszlOkt9L3x2HbPQEPZbV+x9nDjBzOnx5pvNYvDmVVapcnjgnSszBlaIDmnkZ3fyMPkrMMwleAWgwWgV6SCGVyX6T8zicr9xMi0u/OMFi0urs6GQV6KTtFLQK8c/MkudRSpLJq9VJW9hWVdbh82SbMnRCCmcMDMD0hUFnWt8S0ksLmzMPQquFnsBLlFisXF0wkQAL2QWDqMH/IYZpEuF/23/04nNN6h6n4t39S+ai/V+3oYSIBEiABEiCBtgRafwpt+9RC70WkF9/z9pQ6E+mFg4sKoCvBapnsi8Bm9QFQ0vDYli+39kWAs7UkAqFBXtpwMputhSxpbByL9RHQLenp7sb63h1HTALmEsgvM+yMHR5hEN6lnoNyBR+mXEyYpvzSOu02Xi0Gm6a4MA/tNr/U0E52seE8rFnINy1rei3edJ6+KhHRoZ4oU0L9F2sycM9bSZh3/6/4ZG2maVFekwAJkECXCYho//E90/DElYna3zRpYNqYYG0B8kBGWZfbYwUSIAESIAH7IGCVIr24u7FH1y4SCLe9JAL+ILVwwcCx7dGx7TzlulVLQyJ8bHuinJ1VEIgK89PGmVloX4uoVvFyrHCQDs1BGynSW+HL45BJwEwC/t4uWsnD+QaL+o6qhSifzpIOtnGplpJpqBfa/DwywOC2ZvehEhWItvlDUgeNThzihxX3TsdXfz0B9148ElNGBmnBa59bsQ9VtY0d1GI2CZAACZhP4GTlm37ds6dgtnJ/tWFXPtY8Mxdv3dwSQ8P8lliSBEiABEjAHghYhbub8vJyVFZWakd1dTWGDBmCpKQk5OXloaGhQXP1Iu5eJMlzCSqrCdciXpscUj4qKgpOTk7HHCL6+/j4wFlZpLd9Lq515JlY8MsCgRwDkToKHCtjkXlSpB+ItzKwfaaklSKiWRgd2JGwdxIAauo6F0TIiAS6QuAo9N8nZfLKRAIkYJME4pQl+x4lqP+SlA9xh+OozOiLK+pVUNjWQVfFtY2k4vJaJKnPPmNjfLH9cKl2L/nDIw3Px6h8SY3KT9ZbK1Nx5SmxcGkOXKs9aOdHiHJPc960CCwYH4r5D6zWhPrth4uV+5vu7VIUf/ffbs3F6RNDO/V1bzoUWVD47/pMDA72UC53Akwfaf7zO2pv/f4iHFEBav+oxu98nHm2apQ3JEAC/Urg6T8l9mt/7IwESIAESMA6CVi8SH/GGWdg9+7dmnguArqI5SLMv/DCC5qYLnm6cC5nsTZ3cXExCukirIuv9sDAQE3kd3d3h4jdcoiwL+XlWkR6Katbq0ueXOv3IoDr93qelJckArmXl5fWvlzredqF+iFtSZI2pI6Mv0n13dDcv9xLGX3BQVz5/PnPf8Zf//pXrZ780PsyZrS5kOcU6dtAsZNbP2/7dnNUXFGL/YcL1Rf1GhWrol4FgmtU/z93ZAF3FMPjgjAsJgAB3h0HibOTX51en2ZdI8XUXodqxw0a3d006WK9HcPg1EnARgn86eQYfL0uE1v3FWH+Q2swMsYHO1NKNJHddMoJys3NqDg/TdC/5rnNWqDyAuWLXtJolS/PJUUrNzkLpobj+43ZeOe7VLz//WGMS/DX2stTrnBeu2ECwpQf+/SCalz78laEKct7L+W/vkS5vEnLqdIEekf1Wd7U/Y7WcBd+3KUCQx5IL1MLD3n41+1TzKr52YZMPKss+CV997fZ8PdqCa5797s7sT+tDD+rYLQf3NHSnixm3PpqS4DdhbNaAlma1SkLkQAJkAAJkAAJkAAJWBQBixfpt2/fjsOHD2sitojjpaWlcHNz0wRtEbXT09MRHBxstKYXoVrydcFbzlu2bMHYsWONbYggrovwba+lD8kTi3w568lUfNevTZ9LeVkA0J+Z1tPzZGze3t7GBQdZTJBFBjnLIT7l5bxt2zYsXbq0lUgv45J25NyeYE+RXiduf2dfOxSbf9lyBOvU4e3lhvKKY12rBPh7wsfTFeIgqrSsSh0tZTKyS/DT79B2IAyO9MWMsdFwc7ZKz1/298vOGdsVARHKJNHdjV29dk7WzghEB3ng0T+NwcP/2qMFcd2kgo+L6O6q/l0W4d40vXD1ONz7/k5sTi6CLtBPHhGAJy9vbaH60MKRiFDi+wc/HtFEd9N2JGaKiPRyloCzcpimsEB33HvRCAT2wABiWKSXJtIPU4FszU1iQS9JAt56ubf+eibtiEgf36Y9KSflq2sbNAt8c/tiORIgARIgARIgARIgAcsk0PpToGWOUROydaFbhHDTFBYWZnrb7vXMmTPbzbfUzOTkZG2XgOn4dHFerP/bivSyWECR3pSWfVzrwTnDm4N12sesZWcKkFdo8EFrKtCHBnljRHwwRqgguoG+bigsrdHOwqWyugFFytI+I7cUGdmlyMkvR1ZOiXYkp+Rj2tgoTBxx/L8l9sKY8yQBSyCgW9KbrJdbwrA4BhIggV4mcOq4UMghbmJ8PZzh4eqo+YSXvwFuLi2L6N5KlH75uglKeD+KXCWuhyqx3bk5doXpkJyUy5zrF8RpR3Flvfr3v05rJ8TH1egSRlzKrHrqJBSU1aGuoUmJ3Y4I8HRp1Z9pm125XqoWCW48Iw5BXRD6pw0LwHePzoaXm9Mxc5JFhz+fFodg5ZbHNMncf3xstsbK18MqvtKZDp/XJEACJEACJEACJEACbQhY/Cc6XZxvM26bvhWr/LZJzxOxvr1UUVGBvXv3tveIeTZKQA/O6erSsiXaRqfaalrvfZWkiet6ZkJcMMYMDVHifKCepZ1FqNeTp/pi7+nuhegQZdWWGKll5xZXYffBfGzbk4nvVu9XW+gLMG9qLMLsbNFDZ8QzCVgeAcNutpY9bZY3Qo6IBEig9wiE+7f8uy1CfUdJxOkoZfFuTvL3dIYc7SV3F0fNPU57z3qa1xWBXu+ro3HK87YCvV5HWFCg12nwTAIkQAIkQAIkQALWTcCiRXpTdzLWjblroxef9G2t5XXf++2J9MLJ19cX8fHxXeuIpUnAygi83yzQe7i7YHRCGBKHBiMs0LNbswj190DolMEYmxCKjbsysX13Jj4pqcJZJ41AbIRPt9pkJRIggd4j4KCsYSU10Sd970FlSyRAAiRAAiRAAiRAAiRAAiRAAhZJwKJFeosk1g+DEqt5Eel1FzfSpQjxjs15bYegL2bo57bPeU8C1k4gKSUPazYfQUlpNWZOisWkkeHwVlvieyMFKYv7M2bFIybcF1+u3IMVP+7CWSePxPBo/95o3q7aEHcBTCTQWwT0nXR0d9NbRNkOCZAACZAACZAACZAACZAACZCApRJocfRoqSO0w3FVVlZqwXEl0KyeRLAf1IlIr4v6enmeScBWCKzbmYmvf0qGl4crbr50Bk6aFNNrAr0pozFxQTh73ijUqQBsn367E+l5FaaPeW0Gge7uajCjaRaxQwLqnzwmEiABEiABEiABEiABEiABEiABErALAhb9FVi3DNfPdvFG1CSrqqoQHByMoqIi45Q1kV4F0OrI3Y1YHNobJyMcXtgsARHof1mXgqhwP1x+1tg+EedN4YlQf9qc4VrWFuX+hokESGDgCTTRlH7gXwJHQAIkQAIkQAIkQAIkQAIkQAIk0KcELFqk79OZW3DjItKLyxtPzxZf2yLAd2QtL88o0lvwC+XQukUgTwV2Xatc3OgCfbca6UalicNDER8bhD0HcnEos7QbLbAKCZBAbxBwgO6Tvv2A6b3RB9sgARIgARIgARIgARIgARIgARIgAUsgYPEifdsAqpYAra/HIIFjJVBsY2OjsStdpG/PWl63sm/vmbEBXpCAlRH4/vcU+Ch/8WJB399p1oQYFQNikBZQtr/7Zn8kQAIGAo7Nn1AYN5a/ESRAAiRAAiRAAiRAAiRAAiRAArZOwKJFensVnXWR3tS1jX6tn01/MTsT8E3L8ZoErIXA2qRMpGcV44L5owdkyFHBXpg2YTAOHSlAfkn1gIyBnZKA3RMwGNLbPQYCIAFrJdDQ1AA5JDU1NWrXR3HUWqfT7+Mmv35Hzg5JgARIgARIgARIYEAJWLRIP6BkBrDzuro6ODo6tvI/L+K87Cpob+FCf9aegD+A02DXJNBtAtv3ZGHGxFj4e7l2u42eVoyPDtCaSM+hy5uusmykBtNVZCzfDoEbTovHkgWx8PV0aecps0iABCyZwK6c3Vj4zgVYsvxybZhLPlyi3e/M3mXJw7aYsZGfxbwKDoQESIAESIAESIAE+o2ARYv07QnS/UZmADsSkd7FxeUYkb4jv/MiztfW1qKgoGAAR82uSaB3CKTnVaCkrBpx0f6902A3W4kO8YK/nwcyKNJ3mWB6IXcfdBkaK7RLQIT6RSdEtfuMmSRAApZLwNHBSRucs6Nhkc1hkOErh0vzveWO3DJGRn6W8R44ChIgARIgARIgARLoTwIWLdL3JwhL6ktEevFJb2oZLwsWnYn0EmQ2IMBg+WtJc+FYSKCrBFIzi+Hh7oLBod5drdrr5eNjApGZV97r7dp6g+kFNbY+Rc6PBEiABEigEwKuzWK8o4OjVsrJ0Vk7uzi3vzOmrrEOeZX5SCk8hKzSbJTVVhhbb1Kfgesb69F0tHUQaVN3MFJYXOpIOUnZZbmQNiXlVxagpqFWu5Yf5paTshV1lSisKkK5OneU9Pa0MaoxSBK3Plnl2cYqci/Pdfc/xgfNZeVZY7NrIHnWVX5SR+bMRAIkQAIkQAIkQAIkYL0EDGYuFjp+e7Wkb2ho0AT5/Px8hIeHa2+nM5FeAsx25ArHQl8th9ULBMqrDV9Ee6Epi2oiS4niAcqC3RLSMCXSb05Kh7hvcaR/bLNfCQN9mo2KBUmABEjAJgk4OxlEeV1sdnZoFunbWNIfKTmC51b9A+mFqcdw+PiKFXBSFvnr09Zj2cqnkBg9EX+d/5Cx3KL3FmqCu15u6Q8PY09mEmKC4pFWkAJZGDgpYR5W7v1WfU52xP0LlmJCxFiYW046enHNS9icuk7rU9qICojFVdOuxJiwlpg5z/z6HDYcWqOViQ4cgoUTFuKl1S+gpq4K3u5+uGzqEswcPB2Xvr9IK/PaxW8i2DNIu5Yf649swDMrn0SYbzRevuBFLd9cfnojy9QY1h78FTOHnoQ759ymZ/NMAiRAAiRAAiRAAiRgRQRoSW+BL0tEdw8PD/j5+RlHl56errm0aW/hQizuxYd9e8+MDfDC5gjsy7JNC++6OoMVmiW9sNo6Q+A7SxoTx0ICJEACJEAClkrA1ckQU8bF2U0bom5B7+LYEmtGLOPv/+o+TaCXcmOiJmBa3AmYFDsd4wdP1QR60/mZWpqb5rf9/NugLOi93HzQoKzTd2Rsw9DQkZqYvzpltWk19fz45WL8Y5AQPhqRATFaGyL+L/3mAaQWHTa2NUGNW8YsqbAiHx9uWY4Iv2htsaC8ugSv/fYSnNWCweQhM7Qy/9v7nXbWf6w9bFgEmBFveC755vDT68s5uyxbu80uyzLN5jUJkAAJkAAJkAAJkIAVEbBoS3qdY9sP33q+rZ5FpHdyclJfBlq29UZERGjCfXtzlvJtA822V455JGAdBCwv6mhNvVo4c7WKP5cW8Yot7w1aBBYOggRIgATshoC3ixdOH3suonwjtTmfMepMZCgh2dvF08ggtzxfszaXjOfPfxEhnsHGZz25+OO4CzS3Mq+tfhFnjT0H3q7eeD53LwqV2xvTZE65xROU9fsEQy1ZJPj7qmWaZf0Xu77AbbNv1R6cOuwUyHH+W+eiSrnpSYwcj7vn/kV7tvDdC7XFAnHlc87oc7S6K5O/w5LJlxqHsjVtk3Y9O262Mc8cfsbC6uK+U+7Fb4d/x4mxs0yzeU0CJEACJEACJEACJGBFBCxadRJx3t3dXROrRYS2lyTivLivMRXp9ev2Fizo7sZefjM4z4EiUCOuhbxarP8GahzW0i9Femt5UxwnCZAACfQNAXdnd1w95U/Gxk8bvsB4rV+E+YSoBXAvTdi+6/M7MW3ITEyImqhc0oyHm1P7vuv1up2dXVXdhuYdcGK5rwetbVvHnHLiD39b5jaklWagsq4CIV6hWjPpJeltmzPenz36LOP1PfMfRHlNGXxcfRDhHQ4/z0CUVBZie1YSxivXO2klaUa3ODG+LUGyzeFn7ERdBHoE4NxRLf2aPuM1CZAACZAACZAACZCAdRCwaJFeAqhKkoCp9pREiG9rSS8ifUccKNLb02+Hfcy1orLWIiZar3apSKpVlvRM5hNobzHR/NosSQIkQAIkYA8EBmEQ7pl3H15d8xpyStPxk3IDI4f4kl8y/SqcMeK0AcVQXV+Nm1bciBIVOLZtamwOENs2X+6j/FrE9okR41oVOXPMWVi+4V18tedrTaT/XfmjlzQr7sRW5XhDAiRAAiRAAiRAAiRgfwQsWqR3cXFBTU1NK4tye3hFZWVlmiCvW8/LnEX06kykp096e/jNsI85hgR6ISO7xCImm19UqY3Dy8MQ8M4iBmUFg6AlvRW8JA6RBEiABCyAgARglWCphVUF2JyxHesOr8XO9K14Z90bmBs/B2JR3l4S1zNNnQjl7dXpat7r69/SBPphYaNwXuJ5CPcNx97cZLyufMx3lrxMXPq0LTc/4VRNpN9+ZCOq6quwIXWtVmROfIurm7Z1eE8CJEACJEACJEACJGAfBCw6cKyI0nKYitX28Fp8fHw0H/NiIa+n41nSU6TXSfFs7QRiI/0tYgpfrd6PjTsM29l9PQ2B7yxiYFYwCLWmyEQCJEACJEACZhMI9AjCgoR5uGfuXZolvQjwe5QfeUmxfrHaeX/OXqMw/3PKr1peX/7Yl7Nba37RxEWYFjMF4o6mQPmW70kSAX9ic5DZL/Z8owXNdXPxQELwsJ40C/F5/5+kT7Vzjxpqp3KDWhD5eu+32Jq145innfW7Tbn0kXr1KoAvEwmQAAmQAAmQAAmQwPEJWLQlvT58exPpZd5tfdJ35j5CxPy27nF0djzbPoFBsC1FNMjPYDWXX1KN4ObrgXiLO5NztG6dnZ3g4mzR65kDgafTPm3rN7LTqfIhCZAACZBANwlklWfj/75+AEHeofBwcUeZ8t2eVZKhBVp1cHBEXGCc1nKEsmDXfbkv/mAxfNx9UVRRoD4rO2qi/V1f3Y075tzRzVF0XC06MBa5Ktjtc78sw8SYqcivyIEsFLg4uyGzOA13fXUPbj7hJnyStAIVteXGhh7+4RHt+oZZNyDYM8iYr1+co3zWbz28His2L9eypjSL9vrz7pyfWPkk0gpSsOHwOiw7+5nuNNFhne/3/4h31v5Te/724vfg6+ZrLPvUT0/hcP5BrFM7Ap47Z5kxv7SmFI9++5Dx/g8jTzde84IESIAESIAESIAESKB9AhavPLUVq9ufhm3lyu4BcXmzb98+48Q6s6RvaGhAYWEh0tM7DmJlbIgXNkfAwdG2Yjb4+bjC3c0Zuw7mDei7mjo+Ruu/vr4BR7LLBnQsVtd5k9WNmAMmARIgARLoZwK55fmaO5mDymI+Sbm4EbG3rr4GQT6huG/+/8Hf3c84oqtmXK2J8vJcBPorZ14Ln2axOL0wFUXVxcaypheO6jO146Djf91pr9x1M65FYvREVCoB/td9P+Bg3n7V73VqQcFTWxw4lLdP63f9oTXa+PV+ZS5yVNdV61mtzuLix08FetXTgnaC6urPzD3HBgzRig5uPptbz5xykT6RWjGx+Pds48pH729wwOBWTcmOASkvKdLXUL9VAd6QAAmQAAmQAAmQAAkcQ2CQstC2WKPH+vp6JCQkYMeOHRAXMPaSLrjgAowYMQILFy5EYmKiNu0vvvgCL7zwAv71r38hIiKiFYpt27bhpZdewujRo3HHHb1vSdSqM95YDIFXvkvBe98fxuXnjEdUqG39//GlcjWTcrgQV5w3CX7eLgPCfN3OTPyyLkXrO1j5yb/m/IkDMg5r6/Tx11fjDyfG4v/+GG9tQ+d4SYAESIAE+plATUOtErqLNOt5Vyc3ZaXtBzen9v/dbzrahJyKXIR5hmiCfUVdJRyUCO/q6AJHh77bHCz9FqrgsbpVfG/0uyFtE/7+42MI9QnHKxe+2ivUZaEiwL1vXAaWKct4WZxwaoezsAk0WXTQJyNxAyrVooqPq5eexTMJkAAJkAAJkAAJkEAnBI5vWtJJ5f56ZI/uboStaaDYzizpZTFDdhzIwWR/BBxsy5Bee4EjhgSjuqYem/dmDtgL3XMgD66uThB3N/mFFVi5MXXAxmJtHVvsyq+1geR4SYAESMDGCbg5uSLCOxwxfjEI9QrpUKAXDA7KIl7KipsbSWKt7eHs0acCvfQj/eoCfU/7lWCx3yR/i2d//rs0hYWTLtHOvfGjrwR6GZvsWmhPoJdn7Qn0ki8LJxTohQQTCZAACZAACZAACZhHoO/MTszr/7il7DFwbHtQ9A0PpsK9Xk7c3YhAL8FjmeyPwKBBtieJJkT7IyjAE9t3ZWFcQli/+6bfm1qA3IJyTEqMgr+PO1b+fgAbt6djSGQA4iNbfLHa32/b8Wfs7e0KFfL7+AVZggTaIVBe3YDMwmpkFlUjq6gGqXlVOJwr1rLAzpQSXHHaEJw/PRLBvq7t1GYWCZAACVgmgZqGOlz38dWoUH739XTOhAsxJ+5E/ZZnEiABEugRgVW78/HFhhwV58MZYf7uCPFzQaj6vDR1WIt7rR51wMokQAIkQAJ9TsCiRXoRpOWQwKj2lGTOuiivz1vfTdCZSE9Lep0Wz7ZAYNiQIKzbcgSbdmfhjFn95zqlpKIWqzYdhrenK8aPCEOovwfSc0uxT/nIF7E+9sJJyr8tReiOfse8PSXwr+0tHHU0X+Z3n8CB7ArsSivDriNl2KvOKZktgRc7avWd71KVm69UjI7zx7ghvpgY74tZI44NzNhRfeaTAAmQwEAQcHVy1gR6D+X6JSZwCM4cdQZmDp4xEENhnyRAAjZCoLSqTgWMrkFGQRWyi2tRU9+EtTuPjekVHuyBs6dH4IyJoQjzc7OR2XMaJEACJGCbBCxapNeR6wK1fm8P57Yi/datW7XgsO3NXRYxRLynSN8eHdvPs7G4scYXNnfSYBQUVWGHEunHDQ9FZFDXfJoWldUiQAWh7Wr66H87UVxShQvPGKsJ9FL/5KlDkKWE+kKVL25vFkyL62qzdlW+vcVEuwLAyXZIYG1yEb7bloMt+4tRUFLTqpyXhwsGR/nDT+1ecVTe28R6Xs7VdUCF+pGbX46Cggo0qlA6O1OKteODlcDUkUE4f2Y4ThoT0qo93pAACZCApRCQHWafXvW5pQyH4yABErAiAkp7R0ZpE0prj2J/ZgW2HChA0v58FBVXmjWL7Pwq/POrg9oxcXgAQpVQHx/mifhwD8wcTkMHsyCyEAmQAAn0EwGLF+lF7LFHkV7ev6nQNW7cOGzcuLFVnv47oru7oUivE7Gvs+nvia3NfMGsoTiQmo/PftyLc08ZiagQ84T61KwyfPT1dlx/8bQuCfWvfbJFE+gXzE7AsCg/I05/L1fMnjwE3/ySjC07MjA4zB8jBvdNcDZjp1Z8kV9cZcWj59D7gsCXm7Lx+fos7D5U0qr52KgADI8Lwgi1c6a4rAZRwZ3/P96oNmlk5Vcgt7AcB44UITWtEBv3FmgHxfpWaHlDAiRAAiRAAiRgxQTKlCi/Pb0aW1LLcEh9tzlwuFD7nmI6JS+189ffzwOB6vD3doO3+s7i6WYIvh2idgO7ODsqo4hKdVSjSH3OKigoxbcbsrQmgpVLnK8fokhvyrM/rxtUcG1JEu+jqakRTWonsqOKuUK3of35FmyvL/5eWf87tXiRXhDbm0gfHh7erosfsa5vT5ClJb31/4/YnRlkFdZq1Ww5XrCPhzNOnzMc3/66Dyu+34WzTx6JODN8wm/alaGxcXU1L05DWm45flp3CKWlVZg7PR6TlJubtmncsBBk5pZh+54s/LzuIAaHT4S7i3ntt23L1u8LS1tbSNv6fDm/jgnUK1X9uS8P4NPV6a0KjRgagiljohBtsvDmeRyBXhqQnUNSR47JI8ORfKQY2/ZmtRLrTxgbgicuHa2+mDKYeivovCEBEiABEiABErB4AttTS7Bifa6ymC82Wsv7+3ogQInuI+NDEOjrjiA/OTzh7HR8F5zhajeyHIYUjfUq5tfPaw+itLIe//o1DZfNibF4JrY2wF05u7H0mwfg5uKB5Zd9iCUfLkFVbQWWnvE3jA1PtLXpcj69QOCJn59CaXUpHpr/IDycPdptkb9X7WKxukyLF+nFOtzeRPr8/HwMHjz4mF8mEenbs5bXffYzcOwxyGw6I0sFVpRk6zLUBOXqRpII9Z9+vxNnKaF+RGygltfRj7TsEs1lhqdr53/iGtT20U27MrFqwyH4qICnF5yeqALDtljQt21/rnJ7k6Hc3hQUVmp+60/vR1/5bcfCexKwdALbD5fiBbW92tR6Xiznp4yNarVTpSfzkB0tcohYn5SchYNHCrEmKQ93vNOEx5RQ7+vR+d+AnvTNuiRAAiRAAiRAAiTQWwQ+UxbuX23MNn5u8nB3wdTxMZg6JhJiuNRbydnJAWfPG4UtOzPx0ucH8O2mHDyyeBSGhutCfm/1xHY6IuCorOclOTsadj04DDJ8o3dpvu+oHvPtl0BSxjbU1degvrFe/eK0z4G/V+1zsbZci/72qluN25tIL2K7zNnUL71+rTMx/UWjJb0pDfu7drCDIKYi1Ms2zq9XJeO/P+zG+NGRHQaTzVfbOetqGzAspmMhv1gFh03an4vdB3JRUlqNiDA/nDl7GIKVVUpnSSznZ0+Ow3/VYsG23ZmIDvfBmLjgzqrwGQnYJYGflFD+yPK9qKkzbOUNDfbGZPUlU3ak9EXSxfr//Z6C7er/zU3KBc7FT2/Au7dO0nyv9kWfbJMESKBzAtxy3TkfPu0eAf5edY8ba1k2gYc+2oPvlUAvKSjAE6OHhWLyqAi49sGuQH3H8BjlbnDN9gys3ngI97y7C/+6Ywo8zNyFbNk0LX90rs1ivLi3keTkaFBdXZwNon3bGdQ11qGkphTlNeVwd3KHl5s3fFQgcklNypCzUbnOkbZ0sV/yTf9WauWUS53Go01qYcAZ2WW5CPT0hywK5FcWwNvVG25Ohlhu4nrHnHLSZkVdJWobauGi6nq7eErWMUlvTx44qsUIBzXOo+q/7PIcRHiHa+XlvqGxQfMaIe5/TJP+TDQPXYQ2fd7RtTn9tq0rAnhWeRYCPYLg1WY++jj0OVTVV6GkqhThvmEduiiqUWxyynIQ5hNm5Kv3ae74NFFer6TOdWqMep5og6a8uvp7Jc3K70K4j8Eo0qQbXg4ggdb/BwzgQDrr2l5F+vas5inSd/abYp/PJLiiPaSYUG8sOXcCflZW7yLCyREV4Y+Jo8KVUN7iTzFDuaSRFBXu2wqLuGDJyCtDunqerAT6uoZG7fkY5dpmvnJx42am6xoRA6cpq5YN29PwqwoiOzjcH97uVvGntBUP3pBAXxFYt78ID3+wG7Uq0plYgYnl/KxxUX3VXat2z1C7W3yUf1b5wlmkAtOe/fDv+Nfd05BA67BWnHhDAn1NgFuu+5qw7bXPrfy29045o+MTyC2txZX/2Kx8xhtcRYrl/Lypscev2EslThgfpXYfu+HLlXtw2bOb8Ol903upZTbTGQFnJ4Mor4uqzg7NIn0bS/ojJUfw3Kp/IL0w9ZjmPr5ihSbQrk9bj2Urn0Ji9ET8df5DxnKL3luo+brXyy394WHsyUxCTFA80gpStIWBkxLmYeXebzXh/P4FSzEhYizMLScdvbjmJWxOXaf1KeJ7VEAsrpp2JcaEjTaO45lfn8OGQ2u0++jAIVg4YSFeWv2CMuSpUt+h/XDZ1CWYOXg6Ln1/kVbmtYvfRLBny3f79Uc24JmVTyLMNxovX/Cisd3jXZjT7ylD52rNlKvFhmWrlmFn+lZjs9LfA6fejwhfw0LCvrz9eOCrezBWcXZS72nr4fVaWVlguW3unZih5qCn8toyPPnT35GcvUvPwojwMbj3lLvVgoiPlmfO+MZHjMO1H11pbEMurv/31a3u31r8HvzcDLqHub9XegPL1LtZe/BXzBx6Eu6cc5uezfMAE7B1TxkDjJfdkwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEDHBKzC/LO+Xvld6kL6+OOP8frrryMgIACFhYVoaFDbf5QLmcDAQIi/d911jJ+fH4qKiozPJd/JyQnSn17Gw8MDJSUlcHY2rG6KVb+XlxfKy8u1fF9fX3h6emr3MkSxfpc2hg4diuXLl3dh1C1Fpb7ev54r93K0Z12vu8Zp75len2cbJqC2rdlLEh/zZ81OUNbzEdiqArjuTM5BRlYxvlQAxKrexdnRGGApPacUpeW1yC+uRF5+OcorDYF2dVYJyk3NlMQoDFYW+l1NJysLl0wVbDYjuxi/bPp/9q4CPq4qe39t4+7ubZK2qaYuVGhLS5FS3N3lz8LCsuiyLAssuizOLizuLMUKBUpLqbulHnd3T9r/OXfyJpM0MklmkpnMOf29eXbfle9NM+9995zvpOBs6pOYICAIAHtIh/6R93Re9CxLdeqUKKOjVEyFH3uFjYz2w5ufblVVXvH0Fnz1yEyEeHctZ2Wq9qUeQUAQoJB20duVr0EPERC93R4CJsWtHgH2oj/7UZ2HMQ/mnEVjMDrap9/HpSKSSaOevenPe3KzeNP3wx1wbJGWcbB3Uq1pMjcOw3SSM3zwOL3jP/Dt/crjnMvFBY6CK0mwsIwNy9EYypxweZa86cja80pNJJ3j5uSBqroK7CGd8xFU77H8g1iXvE550mt1GFMuwjsCFVRPdX0lsksylIc+J8R9dvk/EU1e9WwTwyaqPu8gz/PiqkJ8tONDhHiFq2Ps0f/67y9jbswpmBw9Q3nlrzz4I66afLm6lj82puk89WcMn6E/ZsyGse0yjn//+e84mndAVRsdEKciF/LKM/HwDw/h3xf9h2SEWqULDuTsI2meRnC5IfQvpeCw8vR/7eJ/I8BVJ4P7158eRyp53rMFegQjvyJXedXz8WfPelodN6Z/b136rsKF7yHjx8YRE4a5Cwy/B8Z8r1QlLR+51C+23IqcliOysgQELJ6k7w3xfO655+Lo0aMKX76+oaEBzs7OiuDmfSbsec3SMbxdV1eniHfDY7yt7VdXV4PJeMNjfN3OnTsxdepU1Q4T63z+2LFjiI2N1ZP6vbnJXHdHEj98jNtob5omPV8nZnsItEjZ2dTAQ/3cENpC1icdK0QBJXLNK6yg/+utDycHSNKmvXmTrn1okAcReP6IC/duf9roff6ZnkdE/cfflmM/TRREBHliQpxouWkADmlJfqTty9o2EDiaW41HSObGyckOV5yTCE/XjnU1+wMNP08n3HX1LLzwzgbV3HX/3IEf/jq7P5qWNgQBQYAQ0EL4RW9XFyavadm214/lL4t2TvR2df91RG9Xh4N8Dn4ELnpqi36Qt18+w6TJYfUVG7lhSNTf8999ePaasUZeKcV6g4C7gxtOH3cOwjxD1eVLR5+BLCJMDXXd8ysLFUHPBV487yU9Adyb9gyvOXf8+Yogf33dSzhr3DKlR/8ikfTFpE1vaMaUu2ziJcTC667iSYKnSTKG5W++3v81/jDnTnViUewC8HLeW+egpr4KY0Mn4E/z71HnLnrnAkV4F1QXYlnCMnXtL4fakvQ7M7apsnNi5ugaMvLT2HbrmuoUQc9yPf+55G14knQMa77f/NlNKKsuxt7cfZhAMkCaMUH/yNK/YXyw7v/I/SsfxJHcJHy1bwVumn4DkotT9AT90+c8j+G+MThWRHkfvr5bHU8h6aIYkv0xpn9VDZW4/9T7VNOXvHexShx7F+HKfezIjPleGV53/4I/4/e0DTglapbhYdkeYAQsnqRnfDoirLvCjb3eH3rooa6KmOTcsmXLTqpn0aJFJx3r6QEm2zXiXbtWmwHtSJOe8UlNTYWDgwMuuYT+UIrZFALDDGZ2bWrgNFhF1hNhr1lOcRXe+XInaSs6Y9SI1gSV3u5OiAj2go9Hq3eCdk1v16yRP4u8hNeRRv5a0qaPoOSzpqy/t/2yhOtO2FB0hyXgbQl9WLu/AE99dgS19c1E0E8YUIJew4MTPV+1PBHvfrUTJRX1uJeSoj1z9RjttKwFAUHAjAgYq4sqeruA6O2K3q4Z/ytK1RaKwEMfH0ZtnU4t4NZLpw0oQa9BxER9AzlB/bjuCP753TH84cwR2ilZmxgBZ3tnXD/lan2tS+IX67e1jSCPAErk66aI7XtX/BHTomeSV3oiebtPoCSkvXeEcaRrm1oc29hz3zDZrNY2r40px0lrd2XvQkZ5FqobqhDgpnNayyzLNKyqzfbZCWfp9+877SFKhltBSXA9VBJZL1dfRYzvztmriPGMsgy9dn2EZ5j+ut5sdNZuUosHfYx/HE1UlKiF64/0G4GyjK3IJI96Q5KeNejHBrW+T0yJmKJI+oySdNWtlBJd/gAvFx9F0PPBEX4x4P2ymhIk03km6dtbZ/1rX66rfWO+V4bX+1Kfzhndej8Mz8n2wCFwslv2wPXlpJaZkOaFCWtbMvbK78i6kruJjo7We/V3dK0cG7wItAZfDd4xGjuyeiII2RLigzB/cqR+mRAfaBYCfTYlwxwe5Yeamgas3X5yQh9j+z3YytnwvNFgu5VGjYfDtZ/76hhKSV7q1JnD4U8RK5Ziof5uOJtCuNnW7cnHB+syLKVr0g9BYFAjYEzItRbKzwnxOJR/DIXET4uZjUlR0zEhcmqfQ/nZ200L5T9+vFmF8huCroXyd1WOQ/njghMQ6hOhEvBxaD6H8qeWpOmr4pB17jObYSg/J+errC1Tofz29FLPofxsHMpvaH0J5e+uXZZFYONQfi0hHofoM8mghfIzyWJoHK/QIs8AAEAASURBVMrPCfG4XExAvPJy5KR97OmoGYfsawnxOJSfjff5uGbG4eKgcNHGwddyKD/va4uE8muIynqwIPDKzxn4eWuWGs7SufHwcjOdE1FfMUocGYToCF98vDodP9Fzk9jAIcBSKvctvF8lTGVpmtX02/Hsz0/gqg8uw0ryNh9oq22sxQ2fXIcnVj2GDza/ja92foaV5E3O1ky/uZ1ZmFcr2Z5IiVFZ6saNZHzYzhijI4y/PfCd2t9ASWPZZlGZvlpn7RYTcc7Gkj/3rrhLv+whgp6tihLcGpqfW1Ab+ZsRvsPV6VLyumcraqkv3DdK7WsfYT6RarOk5bx2XFt31j/tvKxtB4GO2WALG39PPektrPs97g570rfX4WeCvjO5G83LviMpnB43LhdYHQJjo7xwpMh2dOm7ukFZpBPPFh6oy5reVVlTnZtP3vQ5pH9/6FgBtgd7YvIo3cuqqeqXegQBS0fgjR9TUFBSi7H0YmeJsk/sGVYxPQZrN6fg07VZuHBGGBGCFu2jYOm3XPonCHSLgDEh1xLKr4NRQvkllL/b/1BSYNAgsCutAu+t1MnyTp0QDnYksjSbMT4cqRnFeOXbZIyJ8KScPjrddEvrpy30Z0xQAl45/yUU1xRhe9ZubErbqCZc/7vp35g/fC7Yc7ojY+kZnpw2p725+S3lGR4bNBrLxy5HsGcwDuYfwpukMd+VaYR8R2VOi1uED7e8g93pW1HTWIMtqRtVsbnDeyZ101HdnbUb5B6kinPUwqVTrjjp0oQAnbPPSSdaDqSTtz+bu7OXWvu5+Kp1GkncGFo6OSSw+VG0QEfWWf+0slrEQyVFLHQmd6OVlbV1I2DxJD0Tz7ZI0tfX17f5ZmVnZ4O18TvSnWd8OvOyb1OJ7AxKBMRrufW2ZuWXqx3WiO8vC/B2wZyp0VhFoaG/b0tTsjoBXh0/MPVXnwa6nSEQAnSg70F/tb9qdz6+35wDX/p/MG/yyaGb/dWP7tqZOS4MyemFyMytxC/7CrA0UfdA3t11cl4QEAR6h4AxIdcSyj8OEsrfM6cKY75Xht9YCeU3REO2LQGBD9blqG4kEDm/kN4fLNGigj2QOCYMO/dn4YVvj+GZK1ulPSyxv7bQJ18XPyyOW4g50bNw9YdXqginA+T5PYnkb6K8ohQER/IOKmKetdV/Tf7N7LAczktSbVySeIlen/33lN/71C4T1YkUScXRXF8f+F4lcHVycEGcf2yf6u3q4ni/OHWa9fI5Me+c6NldFUdBZQ4lyy2HB+nCc0Tg1hZv/3CKvGMbQRr0bBxJd4iSyo6kiLSDtOZ9thif3v2/93L2RR559a+le3v5xEtVXX394Ai5tZQweB5NgmhJb/tap3Z9KY139bFfMS1iKsLbSRUxHoeLjmBx7MI2E02M509HV8PT0R0zIqdrVdnc2uJJer4jtkjSM+luqD8fEhKiT37b/luqedJ3ROC3Lyv7gw+BYUOHDL5B9XJE2UTS+/u6UYh8Lyvo5WWTyIM4i7zpkyhZLevTX3ha1zPuvWzGai6juB+r6asldPSuu+7C1q1bcfXVV4PzmkRFRVlCt7rtQ0VtE/6zKk2VmzUpCu4u9t1eM5AFxsWHEkl/CKv3FApJP5A3QtoWBFoQ0EL5X1v/upJe4VB+XliK5arp12HpyCUDihWH8t/+xW3KU7B9R3oSym94LYfys5cgh/Kzxq05Q/m1dtuH8mvHtbUpQ/lZb9fYUH6tfVkLAraCwK70KmzZm4tAP3csI5kbS7aZ5OV/lJwb1u3Kx5dx3jhvui7BqSX3ebD1LacyFw9/9yD83APh4uBMxHAFcsqyFEHPRHxMCxkcQh7smpb7ZSSF4+HsiZKqInAZ9qa/99s/4e65d5scHpZzyadkty+seQ6JRMQWVuWBJwpYvi67NIPavQ93zL4dn+/9AlX1umh37sRff3pM9eXWWbfC39XvpH4tI816Jum/2P6hOjelRU7upILdHHjutxeMajfYIxDzRy7GmkOr8OKvz+I1+5cxkqIDWAiusakBf1/6eJuWGNNbPr8FsYEjkVxwROUM4ALnUiJgtmifKIwIHKXkcx4kDLR7w+c46oDPsxnbP1WYPiZHTsF3ezOVrNAvB39CNOnc83fi2mnXIYHa6409SVJ2LOO3JW0Tnjv72d5U0ek1L294VUVE/JD0Pd66+C19OU7K+8j3D6jvJucjuCLxMv25jemb8e/fX1H7L53/Gvi7bYtm8SQ9E9W2RtKzJn1HY+ZjHUnaMEnf2Tlb/FKbasxFRUVqYsTVVaeRZqp6TV3PMFNXaKX15RVVoaG+CWGxulCz/h7G/CnRyM6vwLG0Imzal40ZY+Vhtr/vgbW2t2TJEri4uOD111/H448/jnnz5uHiiy/G6aefbtFD+mZrLjLyqjBpTChYUsbSbXxcAA6l5tP/z0KkF9Yg0t/F0rss/RMEBj0CEsovofzal1xC+TUkZD1YEfjf5lw0Nh/HpIQQix+iBzleTB0XjtUbjmElPe8JSd//t4wl4XjikxdD8yNS+aaZt8C7RV6Fz10343q8QARzQ2MdSogEvXbmjfhi56fqWs77UlJbaliFfnsYcW3DhnTv3dZRuZtm3Ij6pnokZe/Bb4d/UuT8tTNvwmc7P1bJX1PIW5rb3ZyyXhGyWqN7M3eqzdqGWqADmoWfC7Qkq1xwcQdJdbW6ulr3pN1bqd8hnkH4nDBjDLU+cv2ct2Uo4aRZkGc4/D389Xle2LHgjnl3qcS3WpmHKSHuM2uexf6sXQoLPs55d+6df49WpMe4XE4RC/VNdVhz+Gflla/1kXPk9JakjyKvfibpI3vp3a8fTAcbUaTBz7JF4S1a/FoRztHjTQlrOYdPuEF+Aj4f4q4j5RlTD+eeRdpp9Q+GtUWT9JoneUeE9WAAv7MxsEc8S9ukpKRg5MiRqhgnz62srOyUpOdCHRH4nbUhx7tHYNKkSarQlVdeidNOOw2nnNL3hCXdt9rzEqQIJUYIpObopG7Cg9wHBA8PVwfMJaL+618OYP32dJK98USon9uA9EUatS4EFi9eDF5uvPFG/PLLL/juu+9w8803q0Gce+65irSfMmUKOKLKkmzd/kI4ONhhGmmXWouNiwsm2ZtS/LynANcvjLLobueV1ZFH8RDyoGpNKLc3oxzjSB9WTBAYbAhIKL+E8kso/2D7Xy3jMUSAf9N/35WDsGAvi9ShN+yrtj15dAhJ3uRgf2o51uwvwPwxAdopWfcDAhMp2urDqz4lortEec872jmRFrkXnOwcTmp9ZuQMTL/6c+RV5SPINUB50Z9Csi1MLjsOc8CwoXYqesvwwkWxC/S7X163Qr/9tyU6T3ftQGflfJy98ehpjyjJF47Y0rziZ0XNaNPu59d8qVVl9PrGWbfgaUp4zonJR5FcTG+sJ+3yJPG5Y5arhbXweTwOhJs/6csbEvRaP3jcHG3H+vAdycSwbM9fF/8FnLy9sKoYAW6+6h5o1/O6J/3j8kxu30wTIzeS53wORS3w5IEHycJ4kexOb+3OU27HFZMvA99LUxt7yC8deTpYdq69vX7hGygnL3rDiSYuE+Mbjfev/Igwt+vwe96+nsG6b9EkvQa6LZL0Dg4OGDFihAaBWnt4dD6bJJ70baAyyc6KFSuwdu1arFu3Du+99x4SEhIUWc9er9rkiUka6mMlxOGIEQKZJDfDFh7Y+x8qVUEfPhLImzhzLGk47svCOiLqL1mS0G1txeV1OJpZgl0HcjAqJoBC607AiYhPJ0c78kgYptbDQwcmOqDbzksBkyIQGRmJ6667Ti27du1Sf39+/PFH/O9//1Pt8GShtnh6Dtz3nDtzJKcKe46VqmSxXjRBZS02KtoPB2N8sP5AkcWT9Pe/n4SqmiZ8ft80BS8T9De8sB0v3DweM+MtP3LBWr4T0s+BQ0BC+SWU3/DbJ6H8hmjI9mBD4KP12ailiN9EK/Ci17Dnd8yEuECs35aKb7flC0mvAdOPayc7R713cXfNMtGseSJz2e4SkXZXn7HnuV2NoO9ru0yQryHN9fc2v62av2iSabTXjR0Ll3Oxd4GLZ/fRtpwjpbPEvVp7djQ5wnI6pjSWMQrzMF3EvjkIem28HRH0fI6/M+0Jeu0axt/WzSpIek1z3VZuVkdyN+xJ35WnvJD0pv92TJw4Ebxcc801iiz79ddf8cILL6iFdaOZrGc5ioGWw6G/cWKEQEZuGdzdyMNggAnDBeRNn5NfidSMYqzfnYXZE8JOuj8lFfU4kl6ElKxSpBFB7+XhjLrGZpIOKUdWTulJ5R2JsE8kSZHRUX4I9O0gLvCkK+SAtSOg/f1hvfrt27fjiy++wKpVq/DTTz/Bz89PT9bPnz9/QIa6ijRK2U5JjByQ9vvSaDBpwW7and2XKvrl2qam4yirbNC31dzM6phAYUWj/phsCALWjICE8peo2yeh/LpvsYTyW/P/Zul7dwjsJMeGqHBfq5AHNBzLeCLpt5EO9oa9BdhNHvUTovvHSaSwvB45JbUor20keUJXkSg0vCmDcLuOtN9v+vR6VJF3tWbLJl6AuTGWqWSg9VHWgoA5EBhCBLjurc8ctfexTiaeZ8+ejeeeew4zZszoY23Wc/mDDz6II0eO4Mknn9R707/11lt49913lVd3+5G888472Lx5M84//3wsXLiw/WnZNyEC7N3KchRff/01MjMzlfwEk/W8TJum83Y0YXNdVnXtSzuQlFKGFY/Nx9as5i7LDvaTrEf/9v92Km+PZfN6FxJnSoxScirw2Xe7MYS0iC47ewLCAnSyN8nZZdhGevUpROCzTaWkTN5E0O8kL/pCGgObnd0wBPm7I5zkcuqJuM8rrEJxaRXqyfuGbQQR9QumxcDX00ntW9rHO9/shbvTCbx352RL65rV96e4uBgrV65Uy8aNOh1jlsFZtmwZzjnnHLi795/U02mPrIenhysuP2OM1eGanluBD7/djX/eMhEz4k4OwbSUAV3+/DZkFdRg7VNzVZd2pJTi1pd24p4LRuKCmabzoDF2vI98fABHsqrwyb1Tjb1EygkC3SJQR3q2xoTyc0XHTxxvE8pf1VDdJqS+28Z6WYDbNQzlN0W7WzK26UP5X73gtV72rHeXnRTKT155bIdIP5iT3LHe7ivnv9RlKL/Wcleh/FqZnq45KZ+pQvm5bdZENpenIH8vOvIU5O9MR6H83B/G39ZD+RkHW7IKiopb9OBvWDInDokjg6xu6D9sSMaupGycPzcc954TZ5b+ZxbVYPORUmzl5WAx6hp07z1aY8/cMA5zRvtru7IeZAhwJPn5by2Hi6MbIkjy5IzRS8ESPpZkaSXpePTHvyDGPw6PLHrAkromfRlkCFi0Jz1r0rMOe2/lbr788ku89NJL8PX1RV5eHmnn6kLy/f39UVBQAJ6fqKio0BMbfL6hQee1xm3W1tbCyUlHhGlzGV5eXkhNTYUmNaAd174XVVVVcHNz0ydyffXVV6Fpm2tlult7e3sTUWcHTZOfy3eHQXee9t21KeeNQ0Dzbr3hhhvAMhTffPMN3n77bbUkJiZiwYIFahk1qncZto3rha4UJ/RhE7mbVj36CNJ5tASLCfHATPKo5/DQNbTMnRyF7fRwe+hYQZvubd2dqbz/fb3JQ2ScN2LCvBEW6AEn+5PDI/KKq5GUXIgd+7Pxfv5uTCCdyHmTItrUZyk7ppj65b+t/HeNl6amJvU3kNfaMe24tt9RGT5mKuP+cL6Q9gtHOGnHtG1e899wPq4d62ibz/XE+LfsiiuuUMu2bdsUWc9SXLzNvzWsX8+69trvU0/q7knZDfTyVF5Zj/Gj+p8o7kk/Oysb1JIrYtPhYrOR9OwB5u/ZqiXfWV+6Ot5EnvP2dq3fEd5nc3JoPdbV9aY+tympGMdNXanUZ/MISCi/hPK3/08gofztEZF9a0dg81Gdc054UP94oZsar4QRAYqkP5BZadKqm46fwGOfHcS63YVKCkir3JkiiMcN90aInzO83eyx9VAJ0gpqiaTXSnS9rqprpqjDOkQHGB99vPVoKcKovRBvy3SC6nrE1n92CIbAUBffEkfEiVDfufQdS+ya9GmQIWDRJL2GdXsiXDve3bqxsVHJkTCJw2Q/a7oz8c0LEya8Zm/o6OhoPSGuESl8rrCwEMHBwYpkYTKFj7ElJSUpGRQ+ZrjweU72yvVpJHt8fM+9evla7rMhgcMYGO63H7tGILU/Lvu9R4Ax5ckRjRjkbY0Q5G2WmeBksocOHVIk2Y4dO/DMM8+ohQl7bQkKClLX8eRLbGxs7zvU7sq4UFdsosSNQ4aKKL2mRx9pISQ936o5E8ORnV+hZG8+yG6VsPHzccXwCF/EkzZ2gLcLHAxIuHa3uM1uEMnc8DI2NgCb9mRi4440FJZU44JF5p8QatMRI3b6+o08evQobr31VhVRZERzVluE/6bz7wb/zedt7ffHjn+fWvb1x1rKGJbj7bi4OPX7VlRUhJdfflmR9ePGjVPRPuYCZk9amara28M6X2QcaRLMl/7vrdtXhLvPMs3f5IraJvzz22PYRBMYTKEXUYK4T/88A1GBvddVrCe5GxcnnYcrA97QqCPpXR1bj6kb0U8ftXVNCKIX2MFgK3fkYdH4gDaTIINhXDIGy0VAQvkt995IzwQBcyOw8VCpcsrx97LO39BQivB1oEjfw2nlqG1ohrODaZ5DUvKqsWpLroLfg+RKzzsllH6bAzE8qC25/ljNIbzy9VHkl9bh3uXde/Jf8vQWFJTW4unrx2FugnHe9x+szUQdOfe8eUuiub8OUr8gIAgIAl0iMKhJ+osvvrjLwVvqSY2k1yYFuJ9MDhvuG/adj3dH4huW7+02k2ZlZWWwt7fXE0pMIHF/DdfaNhNI2jntGK810whvXrcnwPmY5hnb/nxH12nXG17T2XVclpf2ZQ3La2W0vvZ0vXPnTvBiaGFhYWBpIlMS9Vy/eNK36tH7ePTNc9Xwfpliu6yiVl9NLMnUTJ8QgfAW6Rv9iR5uMLHPkj5M9H/zywF8/jMsjqjv67wR/x9ZunTpoCfp+e8MG//tMZVxnTU1NaaqrsN6dibrkjT7WClJz4MK8HXDQYpsqalvptDa1t+lDgfczcH0whpc9dy2Nl5gfMm7a9Pxl4t6P4nW1NiOpG/SfU9cyMNsIKyx+Tj8vSzrb2xvcfjHZ4dRQLlBrp4f2dsq5DpBoEcIONrZK61dSw7ld7JzgruzFwI9g3s0NiksCAgCXSOw/UgJQihS1lptGHkfBBBRn0X5v/YQUT/dRFKBcSFuCA90RWZ+Nb59ZFankYJTRnjhx805+GJdJk6bEIDx0V1HTs8c44sVv2fh/rf24bdn5sPeiJdlduDYm2zaSAFrvd/Sb0FAEBhYBAbmTa8HY9YI6B5cYvVFmdhmooVJbs26IuEZo/z8fOXhz57d5rQNGzaYs/pBXTffV1MT9AxY67dkUMPX6eBYj76B9NpjI307LTMQJ55+awOaiHz19nJBaVkNqusa+kzQG45jTIwfhiwaja9/tkCivq8sPQ2UE6ZyLhJtwkyLbOG14TafNzym7WtrY8pq1/OaCXNtX6uj/Vqr03Cij8vwvuFkH293tGhluR7tGq0cH+MoMMN6DNvh49XV1UqOjctppk2A8vVsHI1gjr83HEK8L1kXGeLt2Xsvca3fA7X28tD1vYgSs0Y49t6zjT3Krn1xhyLol50Shj+RVuuafQV46J39WEkvlH9cFgc3A2/4noy3ie6ls8G1TJKzGXrX96S+vpTl+87m6z44SHo7ehnfQwnwMDB5l/tyK+RaK0WA4ngllN9K7510WxDoCwK55P1dSAlQx1mpRKA29kCSCmSSfl96hclIeq7bw9VeNWFn13kc7umJQVhIHvZJGeUYHtx9/qX7z43H7acPx+HsSj1Bz88x65IKsfloCXYcLkUpyRLefUE8zp+hk250dhqqnuVYgsfOBO8xGm6yFgQEAUGgpwhYNEmvEfQa6dDTwVlreY2k5/FrVldXp4gjbb/9mrXyWV7FnMaaxwNtGqnFpJW2MGmlkVqdrbmsOY2jCzinAa9Z8mjdunX4+OOPVZMsR3HZZZfh6quvNksXDOZyzFK/pVeamqPz6rUUPXrG663/7VIEfeLYMCyZEYOv1x5G0pF8/LYzE3MTw00GaQJJ5hROilLSNz9vccKiadEmq7svFbX+5epLLcD06dP7VsEgunrPnj34/vvv1cK5VNjGjh2L0047DYsWLUJ/5MHgNndSsmo2Zyf7DnMnqJNW9MGapRF9kHB5/7dMVFU3YOGkIDxAL4VsHKr9vNcxlJDkTXJuZbceX53B1UCe9IbSNjX1OpLe1aFnj24HMiuwnvRcNx8qxhF6ufbzccIn90zr1GOto/4UUw4CNh8P3ct0R2VMcYwTx/2ytxA5xbUI8XXGGYRrQB+0/XnOyuBRSt9FByLpswpbI530Jww2+EV9K+kIbzhQohLZZZOn39zEQDx5eYJBKdkUBAQBQUAQEAQ6RyCzSPdbE+TXPbnceS0DfyaYPOnZ9mXonkFN3aMqkg30IsK+rLoRe9PLsD+jCnlltYjyd8W1CyIV2T6hxYO+nBLxfr8jFxfMDEMRRcXd8cZulFc14u0/TEK4n84Jw93ZDpNHeKtuPrviKD7/LaNNl53oWYodLTRzstdFVZbTM53mkGCKaEutflkLAoKAIGAsAj170zO2VhOWMySqTVitRVfFJD2TyoZjZwJY85Rs33n2uGeimq8b7MaYMBHOiyUZJwxeuXKlWtasWQMfHx9ceOGFegLNnH3lEERbNk2PPtRCwkg///kg8osqsWRuPBLjA9WtWTRjOHIKKrFheypGkQd8gAk1KbXksaxRP5JI+/AA634JsOXvcvuxs878Tz/9pBb+u8IWEhKiJvyYnJ81a1b7S8y+vz9DR9J7uFunHn17gIoqdMni2x83Zp8J4PdWpaqifz5PR9Br1y2bHkzn0vQeYtpxXu9IKcXqPYWoIxI+wNMBF88OVy+mhmV4u5404F0cW3/rqiliiM3VwLteHejkg4mBG/61A6UtBDsXs6cfjEZq13BydzOF4f+8uwB3nDH8pH7k00RDoJcTcsgTkM2dJmf6akx+1zUcPynCgLVpr3x2K7SIAW7n398l445z43DJ7DDVbDZ5I36xKRsllY3UVzsi8YPB4fLtjZrAf35Jw1srk0kSbgjmEbl+11kj9Ml8HSj0vZKIAEMz1NldtTsff/vgQJu+MHb1hJ2YICAICAKCgCBgLALNLRGOTibScTe2XVOXc2n5/W9uSWJv6vrP/Mt64jqG0vNBW8e66BB3XH1qJD7+PRMjgl0xLdYHn6zPwNs/pCrt+sc/PqT057k/T3xxGK/dPBGNlNPnBcoTdA1d5+Jkpyfo+Xng8WvG4JTR/noPe20cDtQ2W30TRbTS9X96bz827ivE6BgvvH37pA4n/LVrZS0ICAKCgCkRsHhWl0lZW/OkZzKeSXdj5W64HHuQWxpxbcovqqXWtW3bNkXM//DDD8jNzcWYMWPw0EMPYfny5fDz8+uXbts4R48MCr10d3MyKfHd2xv3zW9HcTS1EFNJe14j6Lku1pCeP204/rdqH75bewjXnjOxt010eB0T9bsP5GDL3iyEL+y9BnaHlcvBfkdAI+Z5XV5eDo6U4r8pp556qpr4c3IaHAR5vwPbQYN9Iek5WSwTyrPGBZCOc9vHqZsXx4CX9vb8t0fx6a8ZbQ6v3l2IN25LhI9bWwKc63ZzbtXLrybSns3VSE3698lrTCPoZ1Mf7142AqE+J0v7vPJ9Mo6QZ9yEGE+cNblVi3pnahluIZL/1TsScTSnSrX90858JBOZzp5m3m4O6poFY/2V11kWeb+/tzaDnl9OKLK/kiYV3Ejvf2lisHqxrib9/398dRi/bM0DkxZnUoj5wxeOVPXyx1NfHlZ4xoZ7YNn0EFQSvp+vz8I/6bgDhcH7Unv3vbVXX55ftr/ekIMXbxp/UrTCQx8mYTUlh2Xjtnh7AyUK/uDeKcrLzoE85tjrji2PJiJu+NdO9ZJ/1WlRuJVC5F+lyQFtsuCmM0fg/Jmh8Gh3j9XF8iEICAKCgCAgCHSBQFmtbnKXPbet2TTy3Jki0cxh/JvLS0SQG2Yl+OLUcf4YHeahl55556c0hFDk47Q/+BDBrns2+uMbe9Q1cREeSM2uwoFUnZd/eW0jviT9eg8XO/Usds2SaPz3x1T1PPDkp4dRf+5xsISOoTk76sZVRI4Nd765Bxl5uueeAxRB+smGLL2zgOE1si0ICAKCgDkQsPhfC1sj6Pkms0c8k/SGnvS8bbhv+GXQPOmFpDdExXzbrPXM2vw//vgjNm3apBpauHAhHnzwQZx11lnma7iTmofZsG5eAYVBsh79iAifTtDpv8PHsig083AufLxdMXvCyZI2IyO9ibwPx9bdmVi3KxNzJp5cpi+9TUwIxXry1N912AcTWzz4+1JfX641ldxNX/pgTdcyEb9+/Xq18N+W9PR0uLi44Mwzz8TcuXPV4u5uWRESDY2tIcLWhHX7vvbFM7qMiGq2YCOTqWoEfbC/Cx67fDTGRXjiD2/vxSby1HqSvL+euXqMvnstjndtCHkOu2YzNtEth4evI499JurX7y0AS7zcbeBNrjWWma9LNDwhqm0itq+35KoieeRF30BeZWz80soLe5WzMfn96jfJWPHwTCLZj2DbwSJ13PCDnNpx3vRQXPXCdiUNxOQ6e8ZxIj32jH/rl3Tl7bbnWKmq97VbJuonPa6aH4FtdDyXiHQm6JnkuO/CeCyhl+vPN2bhecLtHkoM9+Njs6H9FrKXvkbQcz9epgmQ3w8U4dM1Gbj/vSR8cPcUcIK4JhoTTzhcZeC9/+5PaVg8MQj3UmTEn6k9Jgz+Q5MYDZS099oFUTRZYB5ywhAv2RYEBAFBQBAYPAiUkBwLm2OLnIq1jqyhxcPdXHlx7qVJ+4XkUMCSNx0Zt5vVIh3EEjds/BsdRNJ4/75tEp7/7ii+/j0LJTQB79YShZicq3u+YaeJM8kJ4flvjmEDPQ89+n4STcan4LazYrB4QpDykre30xH///fqbqVNP56kci6YHapyDK3YmC0kfUc3RY4JAoKAWRCweJLeLKO28Eo1Tfr2nvSddZs979mT3hbkbjrDwNzHd+7cid9//12RaFu3blXNjRgxAtdcc40i0iZPnmzuLnRaP/EdNmFHM0ux4qck2JMXKYdcOtEDmI+3TncwOMBzwDHYTF7sbDNJc76zkNaFU6ORlVeJ9dtI9ibaF/6UVNZUNmNcGHYmZeNwWvGAk/SmGpOt1HPttddi+/bt6m84E/M84cfkvCV7zDdZOUlf36jzSo/0P9mz3Njvnb+Ho5JS+Z7I7MvnRSDYu/MIB9ZYZQ96b0q8+uEfpyqtefbE37pfR2qv28Me6tEqdJvb5xdPNnv71j/wdS1EuUZGqwJdfASRTM3Kv87GV1uy1cvor0So87JkWghuOz1Gr/XOZDVbuIE2P0vl/LglRx2fGe+HLzdnq203Vwe8dutEJTHDsjU3vLIT7GXG8jB/uWikSsjmRAR4VIArWHKmuLIOfP2NVI61+8fHeeOVGybAvoXsvp281ZjYDyTZH7boUDc9Qc/7PNbpcT44468beBdvkFf/yDDdhNVXG3X9q6B6PyUvt0tP0U18ZhbrXsq5POcKmEIv2rxspURxR0mfP40mJeypj4yxRtBffGqE6u9nROR/vS2HJjNiservc/DmTyn4fE2m8sD7+NdMXLEoEpfNCYezlcsWMDZigoAgIAgIAuZHoIq8utk6ezcwfw9M00J9i367E0XImcNmjvTtlKDn9nwoP01ecRk9V9SDk/GyOdM74dt3TlI5dqbHeiuSfhdJCi4gsp8dAva15FHismFE5j9/zVjw882rP6ao56G/0MT9e6sz8J87JqG6Tnefasn5axrJ4fzrhnF8GZ52PYK03Cp6ZjhxkkSOKiAfgoAgIAiYGAGLdwli73Fb86Znsp0J+uLiYv3t7s6TnjXsxZNeD5dJNn777Tc8/vjjWLJkiZKaeP7558EJfG+//XZ8+umnWL16NR599FEMJEHPAyVp3cFnLWMqLKtBUkoR1u7IICmXbEWqjI2jRIK+bmgmBmgvSbywFZZWI7+06ySA5gRp56E8ZGSVICLMB+OGB3TZ1EJKJEtxMSR7c7TLcj09aU9yEONHBSMlvQhlVToPk57WIeUHBoE77rgDr732Go4cOYIXX3wRixcvtmiCnlFiz2Jrttz8StV9LcFYb8bCHu3XLo1RHlfn/W0jfthJMi7MTHdgKQXV6ujiKYF6gv6213ep0Gt+kWR79GPWQNdd39Sy1iRZ+Hwz6aSyVdUZjz0HWrEX+yryNH/g0tFqkoDJ97MeXa+Ida7PtyUS4KsWUp614a/+53Y+payKZHZCWiYg7KnC4UGu6rgdbWvSO5mUhNWfXqBZLocT58YGuyGetOKZoGcrbNH+jwqka2m8LDHz+qoUved9AkUVsHWmdVtE5dn7ngl6nhx45qsjSM2pVC/hfN0b5BFXWK77u5dR2ErSp9JYtFsS5KubRCkjwqS2BUMm6u8kzfu7iJS/Ym4EV4XtROazcdJePs5k/VXkhddMZf9NXvWnP7IejJGYICAICAKCgCDQHQJ+LQnXswp08indlbfU83UtJL2LmSapC1p+wzsbf1DLc0h5dRNKWp4pOLJOS/I6tuU5YuOhElWFq4s9qmp0xDvn1eHJ/u+25yKUyHpOAL/ikVmYFO+D5OxKPPv1UeSX6aIj2RnhqStbE8TPT9S91+0yIPw766McFwQEAUHAFAiIJ70pUDRxHUzS19fXIyCglezjiYrO5G7Yk15IetPeBCbL/va3v4GlbRYsWIDLL78cU6dORVxcnGkbMkFtTMJYuzEflVtUhczccuQXV6GwpBpFtHQ0QbdldwY8PZwwItIfNbX1aKCHRibreZkyPkJ5aGrJVPsLl3TSxWcbb4TMTJi/G+ZNj8GazclYsz0d8ydHmqybEcFe2IR07D1aYHI5HZN1Uio6CYF58+addMxSD4yJ0EmiNLW8rFlqP7vrV3GJ7mU5xKdz7/fu6uDz1y+MogmLE3iXCGcOn/77RwdJT9UVhWX1qKaXwwDyTl/xwAxE++uI7U/Im34tSdAUltQpgp69tZ6kl8HLntuqdOEvp/ULN4wHe8GzHcjQTSbwtqZPn5RZrhKn8bHu7JFPDtDzwQncTklhl00JxtlEon9DL6n/+PggHnl3v6rnljNiwN5kT31yEG98n6LXsY8iop29xz4jWZlbiKT+B0nNsHTOuU9swvBQd/JKr1Q67jzJcD7py3dlN5DW+18/SFJebhyObmiclM2rRY8/n3DryAK8nRUpv+ih31VCN/Z08yOM3r17Ml6iPvPEwwVPbsbT143TJ7nlevjl+7ynNisPuq0tUjyh9KJf3EIGTBnlp/fAD6BJBpYi4mt4suSSZ7Zg4cQAXDkvEreSnu3VJL3zn19S8SHJ87Be7bePzOyoq3JMEBAEBAFBQBDQIxAVoIuazS6sQFiAm/64tW3kFuj03iPpd9KUFkYRjUlEgHPema6MI+I4GjCYntvuPS8O/9uU00ZXnh0Fxg33JrkbHdk+gcpntDhIsAMAT/b/7cMDePaLI4iLdIcj8ScpOboJ932p5Zg5Wied+sx1Y9vICt60KArfrc/GUXoemkre+qa04+RkyTaUuJ8Tx5tpOYGhrLff4rzRvq2a5GPI/+Jz1Ken4gRxRfYhoRjxtyfaFzNq/3hdLY498pByDI157O8Y6tCKf/qLz6OxqFW+0HX0GARfcqlR9UqhgUegp9+rge+x9KA9AkLSt0fEAvaZpD9+/HgbUr4rT3pN7kY86U1385iMZ8/W2NhY01Vqppp0vpVmqtzM1aZkl2HL/hxk55UpbXmtOUd7O4SHeiMqxAuhAR7aYbWuqK5DMWnRl5TXoLJCF+6oFdi2J0NtHkouxMKZwzEirK3GslbO1OvC4mq4ODtg7HB/o6qeMS4UWfkV2LQzXenphweYRm88MtiT5CGG4sCxgSPp+dlSbPAikEiEKhsn4ywlTVBvknyxNssgL3qOBAiiJKpuTn1/DGIC94IZIfj3L2nYebQUaZRklfFhndQLTwlT8HgTCf0nkoN57rPDFK5dqzzALyeC//alw9W72Lt/mIIrn9+qSPFHPzqAN0lHnT3H2VucPefdSIt11kg/MMn/064Co0n6WtJnX7crX2m0M7HuS/erighu7h/bgaxyLCENdtaY/xdptXLfOAHbNQsjMTfBH1e/uB15JfXqhfW/d03GAzQRwX3icmxc5wM0rqjArl/al5LszDAK+1qxOQd5pfUqCWs+TVQw6X8eYefvrns5ZKmcjuzJa8bg9ld2gaVt2GZSstrHLhmtpHEevWgUaggjlgz6w2u7cCPp7rM9QWHtH/6WqV7+c+hFnft61zmxyuPfl17mGxqP44krWr3l+Jo/nD1Cad9nkWROKenacqK591alYWS0p4oaKKCXfLYCitxinX7RqFdwyIcgIAgIAoJAJwhEtxDziuROCOmklOUfzqVJBrZJRISb0v50TryKZDOU3Ouo/nMpoTzL2LDc3BhKMM9Le3ud5Pg0ScCnrxpDz086EpzrfpekBp8nj3nOf7PniC5ijq/nZPV/Oj9O5Qm6gqQL/UiW0NDYU/+l2yfqnScMz/Vlu+rwIRy99TrY+wdhzCdf4sAVl6IhLwsj/vUG3BPGnFR1Ocntptx/l/74MGcXHCe5495aZVISqndsVpdXHzkM9zFj9VXV7N2DurRj+n3ZsAwE0p5/Fk2lJYh54GEMde5YrrOn3yvLGJn0oj0CfX87bV+j7PcZAY2k74kmPXvS83VipkPAGgh6Hi3N51ilfbPuCPaTTIwz6cu7kL48J4ANIm35mRPCMJxkY1i+pWNrfSj7eYszeVM6I9jPjYijMhxLL0FmTilKyqrx2cq9GBMfjFMmRcDbre0DV8f19u4oq36w1/+MxJ55xC+YFkMSOaX49tdDuPXiKb1rvN1VLPMcEuSpMKglT+eB0E2uZ01Hup9igxMBJotHR3vhQGoZeSCVYpJHkNUNtKDFiz4soOMH3N4MiD24HqBko5qx7I32oqgdY9kZ9mQvJi+vQE+nNo5Sni52WPHgTOwjL/lRobq/cS9cPw4HSEOdZVfY2IPrH3RseKDxnnhPkMTN+0ROv/dzmvIQZy9xNibll80KxYw4nRwNv/jy0t7ev6v1b1MMRQh8cu9UpQdbUN5AGvYuavKg/TWd7S+eEEgJ2gL1p+8gb/StB+uRSC/8geQVzwleNa16faGWDX4Z//XJuRSOXgc/mmiwN9B5Y4czTrqbQpr+3uSJx5rybJwj4G3Sma0k7X8er5ND6wTA+5Q8lhPxeji3fW6aN4bIf3qxD6bflU/vm4pXV7KXfq4i+lu6Ah/q680kcyQEvYaIrAUBQUAQEAQ6Q8CHJun5XSfXiuVuONq5nt7TAsi5IcIgf01nY+7JcX6uZIk8Y4yflboyw+cufjZwN/iNZ7k8doBgybxcchIgnyZ4k7QNSxdq1p6g145PijHtxATXO2Sorl092dri5TTUseMIz6yX/6m647VoKYIvvxJOYZSHpw8kgPu48fA+61zlSe82erQ2VLUe9da7al2xZzeS776tzTnZGTgEKjb+jubSIpqc+XOnJH1Pv1cDNxppuSsEuv5L19WV/XiuM5mXfuxCvzalkfSG4y6ikKPm5o51aLm8yN306y2yqMas0ZP+x00piqBfMCsWm3dlkGZgA2ZNjsIpEyPQE/memZQs1bXlASzI1xUzxuokFzLpQfj37WnYfzhXLafOHIHpY8zjvZLXQvi5kid9T8yHiKZ5pE+/iiYrvlt/DGfOHtGTyzstG07e9DxRUVZRC2eavOhvK6CoAn/P/olg6O+xSXs6BKbGeyuSPp0mxiaNtD6SPo0ieNgWGhDGupGZ7tPwRdGwViahNSkbw+O8zS+U41o0VXmfSeb2yWjnkXd7T4zbu3ZBpFo4eW0peaIzMc0Tlz35W2vYJnuVaRqwhsd7up1ZRAlciTzX9O45lL0r4/62x8OwPE8isA1tCRPX5q8NX9K18kzyd/ayr00kODk44hHy0ueFJwfYc97DxaHT67S6ZS0ICAKCgCAgCBgiwE4BRzMqcCi9FCMju/6tM7zOUrazC3UT/ONboiktpV+96Qfn0+nOa7839fb0miEt8jJD7HXvj0OddOT8EPuTHZ0ayyniPDtdNRF+6x2w82hxWOOZhhZrrq5GM0ngDHN0xDBX3fOQds5wrWR1KMcOW8Rt/6fWGrGrdnryQZMEx4mfUhI9Bn0xlFvh6gzbHELPfao9iuisLyiAY2Cr84Zh0xwl0JCfB3tvny7HY3hN+22Tt0t95rFpYzheW4vG8nLdGFqePdv34TjdkwYapwPJWA+le2NoxvavfcQE7+uPUbssl6RZT75X2jWytjwEWu+o5fVN9agjTWpju7p27Vo89thjqKmpga+vL2lX68Kk+XrWfHemMBEmwvlceno63N3bSk4w+d1I/wnaS82U039GLqt5unOZ9lIzhYWFSlOe6/j666+N7bIqV1FRgXpKUGpI0ldVVYGPd2SiSd8RKrZzzNpI+o17s7BzXxaWzo3Hmi0p8PJ0wZJZw5U3fE/vmkbQt78unEJLL106Bt/8dlSR9L9uPGY2kl5r28X55Icq7VxnayY4UzJLlZ5+fJQfYk0gz9PQqJvMK61sIEw7a1mOCwK9R0AjkvMKWvXSe19b/15ZRv8vUtJK4EsE+JKJHb8Y9G+P+q81L1d78GIpVlBUizHDTT+hx16LbMUkx2QqY09/MUFAEBAEBAFBoDcIzBjpo0j6XQdzrJOkJ4lOtgkxuiTvvcFArmmLgEastifnh3ZA0jfk5auLnYbHtxL0batD5huvofT7r9RRlsJxShiPkGuvh1v8yDYlj971f6jev7vNsXHf/NQrIrx8+3YlweO18HRE3/+Qvs7955yO5toajF/1myKQM156ESXffKnOu06ajsALL0bms0+hsTAPjhHRCLzmBvjOmavO82RDxr9eQNkvP+jrcx0zAZH3PwzHoJ45Bpm63erkZBy56SpwNMMQO3uU/qDj+Ow8vBDx4KPwnNwafdpUWYmM559B+brV+nF4zlmAiLvvhV0L52hM/1iGKOmCs/R18EbShWe32R/71Ur6Xuj+b/bke9WmEtmxKAQsnqRvT5D3BL28vDzMnz8f7IXuQTOOGunNxD8T7XyMjb3Q3dzc4O+v81DTyvFxJuKZBNeOcXmul8s60AwoH68jQt3FxUVt8z4vnHh01KhReiKfrzPW/Pz81IykNgnA1w0fPhwHDx7ssAqeIBC5mw6hsY2DVsTS7yWt+LWbUzB/xnD8vP4ooiN9ceacOJIhaA01NOVNO3tuLCVorEI+eYCs3JCMpTQZYGpzadG0dnXqmSe91o/TyYOeZW++/ikJ91w7Szvc63Uj6++QlVfV9roOuVAQ6AqBWaN8MYFkUnYfKUI+aXMHUlJPa7FDaUUU6txMXvShAyIHZS04maOfz31zFOXkzc+e6eXk0c+6+IkjTE/Sc1I5tiTyWmRdfTFBQBAQBAQBQWAgETh1bADe+ykNqRnFVudNz/lb0ikC0Yui6OaNEe8fU32PtEStQ1q8qzWyfphBAlflkU7PSk1EeLPZ+/i2elDTvqEHu1NEJDxmz0dzVSWqd29H9fZNOEpL/NsfwSUyUl3PHx6z51LC2TC1X/bTd/rjfdk4QZxZh0Z9Z3MbPxEn6P20dOUKNGZnIf+j9+E0PBYOkdGqn9lPPw7vmbMUoZ/61N9RufE3dZ371FmoPUTa+TSpkPLIAxj1+n9o0K3RA6pQFx/mardqy0Y0VZSB+8f9qdz8O1Lu+wMSPlkBhxY+Me2Jv6Fy6wbVOxeaMKlJ2qMI+zTiDUc8+bQ6bkz/Rn/yFbzPWE7hCJQLjPBj40mRIRTtqdmQYa2UrjHfK+06WVsuAq131HL72OueXXzxxb2+diAvZHKeJxIMJwa0Yx31iycRONEsr8VsDwFr4eirSctww/Z0zJwUhTWbkjFpbBgWk9yLue265RPx2mfbsTspmxLRepOeto9Jm3R00P0ZzS2qpESPLeGHPWjBjTzwz12cgI+/24MvVh/C+Qvaejz0oCpVtLHFk76pyVq+GT0doZS3BASumB+qSPrDqUVE0odbQpeM6sORtGJVbpEZpW6M6oiNFeIkrJ+tyVCjziIPek0uZ3iLRI0p4eBIj2HkLPHlhhzcskSXmNeU9UtdgoAgIAgIAoJATxAYRXrokaS7np5bBWvzpt95OA9V1fW48cyYk5Kq9gQDKdsWAfamDrzmJjiG6KRafZeeBbeJkzCMHEfZWM5kz5J5alv7qNy2sc2xqMf+Ae9Zs9XpoPMvAHghYxmV9H++oDzri1Z+i4hbblfH+SPoggv12/s2r1dks/6AmTZ8580HL0wyc3Jct6nTEHnn3aq1fcvPUH1oLCkm7/s6RdBzJMDoj75UUQOMw+Gbr0dd8mFUHkhqk+C2u+6aq10m6A0T/Cb/5SFUrF+Dwu+/RejV16ImLU1P0Me/9SFcoqJQm5qCQ9dfoY7XkIIHT5wY07/mmmpE3X2PGmrFpvVKkz6cZIr0kkftQOjue9WuuOxaKAJWQdIbktUWiqNJu8XjZZLe0JNeO9ZRQ1zO1jDqCAdbPUb5b6zCNu3JhJ096TH7uWNUbEC/EPQaMDMnROD7tYew4uf98L9wCvwpKaCpzEkj6Vv0GntTb3SIJ1g3/wgRnn21RtJNZnOmRExigoC5EJg90g+jYnyx60AOZlBuiM4TPZurBz2vl3NVZOVSsttRfhgb0fMJtZ63KFdoCHiTBM0fL4jHc58fbpOEdfII006acnuswz+NvP027itEHU1aDkQCbW3cshYEBAFBQBAQBBiBWQkkr0skvfKmJ4eBkVG+VgFM0pE8eFIem0tmR1hFf62lk+w5H0IJYDXzXbBQ21Rr1j33XnqO2mZt9uodmzHM2w8eM3SkPJ9wCDSQfyGHzYp9+1CXlYHjNbV0Tifp2ECEsKWZ3xmtci2Rf30SzdVVsHN1Q1WLaoTLxCloKC1VC/fdKW4k6tKOoT47u0ckfftxm6pdlrdxH9WabNedJh2YpK9PT1NN1rasnaJGKIKeDzpHx4D3eRy1aaltohvURfTRWf+088asu/teGVOHlBl4BCyepO/Kg3zg4TNPDxTh3gNP+vZe9+bpldRqqQhYA0efkV+JrbszFYQ/rjuMK86e0K9wjo8LQBJJ7aRlFuPb347g0tPHmExih/IPwsfbVUnq9GVQnNjWFMltNU96B3uL//PeF7jkWgtA4I/LRuD6F7ZguyLqdZ5AFtCtTruwcXeGSlR685KoTsvICfMhcOHMMEyK8caf392PjLwqXLYwEt5m0sh/7JLROHhKpRD05rudUrMgIAgIAoJADxCYN9oPH/2iI0zXbE1FWIAH3FwsJ09MR0PJpNxD+UVVuOmsWLg4ivNPRxiZ6xgnV436472q+oq9e5FMJL1LbLz+mGG7x0lC5fBtNykC2PA4bx9v7kSKpn3Bftx3Cg7Wt+Yxbpx+m73p2Vju5nCL5I3+JG00EZnfFzNVu44RUW1kd5yjolW3Got1znba2mlEbJvuOg3XkfSNpSVtjms7nfVPOy9r20HA4lkc1lq3NRkXnphoptlQQ096w+32X09bw6f9+G19/wQsn6bfsEsnc8D3akZiBHw9+z8J37RxoYqkz8svx6/b0kyqTx8e7Ik9RFQeSi8d8IRQZZU6LXpnJ8t+8Lf1/7eDYfxjI9wwbWwQ1lIC6JBAD0QGulvssLYezEUy6dGfMy8aWuJbi+3sIO4Yy9t89qdpyCquRbif6SKa2kPm7myHqbHe7Q/LviAgCAgCgoAgMCAIjI/2wuKpwVi1NRelZTXqXYRzZ1mybSepUHc3B1x7qnjRW/J9ynnnbUXQu8+cC//l58GRPOyrjx5Gxt8e7mO3yRONjSRnjDWW2uGksV3ZMFfXDk87Bui8/x2CwhB07Q0nlXEd2TdJWHO1W5+Vpfo6zFOXZ8neV5e7ofbIoTZjqKV7wsa5BTqyzvqnleWksJx5rqmKIg9acmtq52Q9uBAwPvPCAI67K4J6ALtltqY1aRtDCRveZt35jozxYW96MdtEwBpufQ4R42xR4b6YlhAyIDdqeKgXxsTrZu4PHstHowk128OCddIZrDM5kMYRC2XlOpI+KkiX5X0g+yNtD34Erjo1XP3+/G/VflRQMlBLtOLyOmykfBihQR646wzz58GwRAwsqU/0OGNWgt6Sxip9EQQEAUFAEBAENARuWhwNf2/dBPX+w7nYeShPO2Vx67U7MnDwaAGWzxyY9zaLA8SCO1R9YL/qXeDFl8IzcRKcQkPRWKTzSu9Lt+28dKRzXVoKVBLbdpU5Regmb6p3blM6+Hy6ZN26dqWM33WN1U1asW79UBcXsASQ4eIUGmZ8ZT0o2dN265KPkI5+ha4FlhnaulltO0VGqbXmWV+fkYrqY8fUseqjR8H7bM4t5dRODz40eaOyDet7cJXpi/J3oeinVeAIj/bWUFiI/K+/Aq/bW+X+feo6zjMg1jUCFu9Jb4tSLhpJbzg5wdvJycm47LLL0NzcTBOajeAog/LycpSXlaGMFjHbRMDSSfpsCpOsp6SxbAkj/Af0JkWHeYEfirk/e4monzTSQMuvDz2LDNI9xCidyXbe9KVV9fAmLcf+sN0tD/tjaVzWoBHeH5hIG+ZFYFKUB6aQxvu2g0X44Ns9uPXiKeZtsBe1r9uZhpraBtyxfCScLP6ppxcDlEsEAUFAEBAEBAFBwOIRCPVxxnWLo/DUJwdVX9dRZG84OdWYMleWKUDYn1KEjTvSsGBKCG47fbgpqpQ6zIiAY1QMapL2IPPZp5RmvdKwJ+KcNezrDu7HsQf/jLBbbyeivRn5H3+g7wknQGVLf/4ZDLG3hwOR4KFXXKU/z/IrnMS1sTAPh264Gs4j4lBHeush198Mz6lT4RgUBKfh8Sqp674Lz4UDecI3EBHN17A3PbcbceddKPjqSzST97dmqU/9XW2G3nAzHHxbvcr5ev9LrkLhx+8i7aF7kUn9d588lTLhkm5AQwNi/vKYVoVR64yXXzRLuzy2QzdeA9fxE1FDxDNPKrD5n7VMrTkpLEc1sGzPkZuu0mPEJ/k4n2cztn+qMH24TZ2B6v27kfv6iyhZ+Q2c40ehmTjA4Guuh2t8vFbM7OuSNb8i8x+6ezH2qx/aePVnPPc0OMFx+cYNiPvHs/q+8KTGsTtv1u/7nbZYvy0bJyNgFa+rhmT1yUMYfEd4vI6Ojm0855m4t6MQl+joaHXOiZKNcJmioiLw9ogRIwYfEDIioxCw9MSxeUTSszlQgtWxIwKMGpO5CoWRJIdmSeQdYiqS3svVAZPHhWP73kxspqWqpg6ZeeWUpLIClVV18PFyxXmnjTb7Q3jSYZ1HzpQEy9cH1+6DrK0fgUcujMe1/6pGYWkt3vpqF65bPtFiBrVuV6byBFs4IxLnThnYSUKLAUU6IggIAoKAICAICAIDgsDyaSHYlVKmZG/YgWD1llRcvLg1CeWAdMqg0XKKivzmlwOICnHHE5eOMjgjmwOFACeRVUYcUUcWet31OFFfh8oNv6Hw0/cVOR965x9R8NEHRKAXoXLz72i85DLydj+Bsp9XnlRF+dqf1THXMRMAA5J+qIMDQv/0ELKfflx5gWue4I0Vugh5vogJ+/THHkRzaREa6moQetd91O77aKYEqZzstpESwJb9+F0bCRytD8EGbWmdCrv2ejgQ+Z/3zluqTq2sOn/80TYL1b7wAABAAElEQVRa8No1na3N1S7jZB8UjLJfflBNcyLZsD8/TDJDOrkePhj95weR8aIzylb/qCYx+JjXgiU0aXE3byrrSf/4gqALL8LxhnqUfvO/NvfD69SF/UrSO7bkFLD3D1IRD7rR6D6dKEEuk/TOxFka2jA3V3B5nvBxDJHoHENsOtoeQp7qFq2TkpCQgP/+97+YSrN1tmK7d+9WHvP79++HJnnz/vvvKxx+/fVXW4FBxtkNAqv3FuCB/+7Dz0+dijUplpcURuv+t+uOYB95eI8bHYIzZw/8ZNKrn25TkjBhwV648qzWZDVaf3uyZhmN7KJK5BZUIaegAvkk63O8pQIXZwcEUVIoXy9njI8LRIC3S0+q7nHZ/SmF9FB9EFMnRGDh1KgeX2+qC554cx0SYrzw9h2TTFWl1GMFCOxMKcUtL+1UPfX3c8MN5yYOeK+3JuXilw1HMW18GP5+aRzcHVq0NQe8Z9IBQUAQEAQEAUFAELBVBLJLanHnm+T5nF+tIBgdG4hz5vefJ2xnuDNB/8qHOumOn/8+Fx4uVuHP2dlwbO84Sa80ECmueac3V1eDCf4hRLZzItpeG9VbX1Cg6rIj3XUm79sYny8qhIOfn2rHVO0er61FQ0mJas/e16dvY2jT4a53OmuXpWvYM55J+rgXXwEn7G2qrISDf+dOQCwN00QJce1bsOm6ZePOsu5/fV6+ijCwc3cjT/b+l7hlz3iWJGKd/PbG98zBx6f9YSWJ1FxTCzs3t5POyYG2CJyMatvzFrFna570DDrrz2sEPe8fOHAAxcV91xbjusQGBwLuzvZqIBY9y0Y9LCUim83HQ6fBqHYG8COEiHOdbrvxhF1lTSMKSmuQX1xFSyUKi2tQVNIatjeMIl0iInwRR5ECh44VwIvGehV5E7s69t+f2E27MxEV5jOgBP0A3lZpeoARSIzxxgOXjMITHx9EIUXPvPbZdtxy4eQB69UeipRhgn7i6GAh6AfsLkjDgoAgIAgIAoKAINAeAZa9eeH68Xjg/f04klGBA0fzYWc/bECdmfYmF+C71bpEl2/eOVkI+vY3zRr2ydNeI+i5u90lIjV6SKzyQN7tnRqfb0n6ymVM1e5QZ2elr99pu2Y6YWy7Q0nNwoGWroxJbJbxMaXxhIvTAHujd5W4tiOCnsfP/RaC3rhvQv8xSMb1p8NStkbSG5LzGiATJ07Epk2btF1ZCwJ6BE5ortv6I5a54dSPhHVXCDBJzw/DnVlhWQ0KSmpIuoMWIuULiIwvr9BNNGjXMAkfH+MPrmt4hA8CyFtes8+bT+BoaiE27ckkwrxtqJdWxtTrt1fsQW1tI86eP9LUVUt9goDRCCybGgL2Dnt3VRpK6f8RE/UXLRlLE3SORtdhioKb9mVjzaZkJMQH4JmrRokHvSlAlToEAUFAEBAEBAFBwGQIhPs545/XT8D97+3DnmOl2Hsgh/JJDcPi6f3z7qANpJrydK0hbXxu39vTEW/93yTwJIKYICAICAKCwMAgYPEkPSdJHTasD+E5A4Nrn1ttPzHhQGFFnChWTBBoj4Cle9Jr/XVy0Hn+a/sDtfbx1D14urm27c/nPx9ETn4Fqmvq23RNI+SDA9wR7OeOEH93ONp3rAnIF16waBQ+J2m/reTZHuDrinHDzafDX1pRj/e/3Y2q6npcsHQsAs0sqdMGGNkRBDpA4NYlwzEu0gt/fHO3Iuo//XEfzjl1JP3fMX9oY3VtE1heKyW9CEtIg/7hC0bAzviAmQ5GI4cEAUFAEBAEBAFBQBAwDwK+7vYtHvX7sDmpGDsor5UjedTPSYzA0H54fjmUVoy1RNCXlFZjyig/vHzjePMMVGoVBAYBAsMoH6RjRDTsQ8IGwWhkCJaMgMWT9AyerZH0HaUJsKeM2yyBIyYItEfAWr4VzhbiSV9BiVzZwoJb9ds4ua27myMac5oxIsoPYUEeRhHy7e+Fts9E/WufVauw0bq6ZkxNCNZOmWy9L7kQ364+qOo7deYIxIZ5m6xuqUgQ6AsCs0f54tU7EnEP6a2yR/17lEx24tgwTB8bCg9KsmwO27w/B79uPKYmxh66YizOSjTf5Jg5+i91CgKCgCAgCAgCgoDtIeDqOAzPXzseL353DJ+uycDGHWnIyCnBzIlRGBHmZRZAMvIrsS0pG4dJotPNxQGXL4zCHWcMN0tbUqkgMFgQcAoPx+j/fjBYhiPjsGAEhKS34Jtj2DX2pOeoAjFBoD0C1uJJn5lXjugQj/bd7/f9CkqKxBZKHvGaBZGX7xJeZsRoh/q8Zj1uTqLKuti1FEo6NzG8z3VyBayNvzUpR4Wl8n5EqA+mj5Es6YyFmOUgMIk06l+9fRL+/tkhHM2swHbyDjucko/EBCLrx4VhmAk8xPglc8/hXHrJLERDUzPmJwbjT8tj4ePWNkrGclCRnggCgoAgIAgIAoKAINAWgWHkNn/32bGYM8YP//05DdsPleCz3L2YPC4ccyZFwqmLCN62NXW+l19cjZScMhxNL0FWTilcnB1w5ilRuOnUUJLt7FpXu/Na5YwgIAgIAoKAqRGweJKevcdtzZO+o5vMnvRC0neEjBw7ThrolmxhQZ7Iyi1DFknJWIKVV9bC1cURof0gv/HAjXPwX9KL37A9FTxJMWtCRK8nKkqr6rGVvIX3kudLY7MufiKRvJNNObFgqvsTF9Y6AWKqOqUe60NgFH0PPrh7ClbvK8CPOwqwbk8+ftuSgmPpBQgP8lY5HSKCvODiZJykXUPTCeQUVqrlCIVo5+SVwc3NAXMnBuK0CX6YM9rf+kCSHgsCgoAgIAgIAoKAIEAITCYHh8k3eePj9Zn4YHWGcnDIJEI9OtwXsZG+CA/omXQgOzMcSi1COtVRSFHDbEzOL54RhavnhSEmoH9zBqkOyIcgIAgIAoJAlwhYPEnPvW+vz97liAbpSfGkH6Q31gTD2pFSBthbLikaQbIym3eB9N7LUVnTCHeXgfVyrahqQPzw/iPzrjlnPH7ekoptlEg2I6tEecVMHBkIfy+Xbu8+a2xnFpQjr6gau4icr61rVNe4uzlhzpQojI+1TEkPD2fjSNduAZACgwKBBWMDwMvhnCis3JGLjQdKsJlyNmgWHOiJqFAvOJAOq6PDMJU4jZOn8X51fQNy86uQT+R8XkEFmk+cAHucTUnwxzWnjsb8sf5wd7KKRxltuLIWBAQBQUAQEAQEAUGgUwQumR2OpRQd+PuBQqwjrfrfdqXTu1Q6vOndYWRMAJxIIsfRwQ7O9MzkQDm/Ghoa1TtWVU0DeKmubVDPTTW01iws2Atjor1wyZxQjAwUz3kNF1kLAoKAIGBpCFjFm62tedKzJv2QIUNw3333IT8/HzU1NcjJyaEf4NYfWkv7Ikl/Bg6B5uOW7UkfQZ709sOG0ve3CTsO5mLepIgBAys5uwzskZIQ238kPQ920bRoRNDD8aGUAuw+kKM8Y/zJkz820k8liGJi0oEetp1oqaLEtRm5FcgrrEBRSXUbrDgCYPyoYEymxW2AJzvadEx2BAEjEIgPcUN8SCzuOgsoqqzHQZLBOZxTjVU78rFpZ3qnNfhRGHZ0sBtOSQhHbIg7xkd6ItxPlwC604vkhCAgCAgCgoAgIAgIAlaKgKeLHc6cHKyW8gtGYtXuAnyzJQcHjuWhvEKXX6u7oY2M8UNivB+mxXojMcIF9LohJggIAoKAIGDhCFg8Sc8SL7ZG0jNBz8YSNyEhIfDw8MD8+fMxffp0C/86SfcGAoFGUj4ZOhANG9mmg90QhIV6IzWjWBHUk4hgHihv+t2H8lSv/b17Fi5q5FC7LBYf4Q1eSidHIYmSvh4luQ4m7A29XDqqgL2GfbxdEUsJbYWc7wghOWaNCPi5O+IUkqfh5XpKWNZEEk41DbRQ/oYaSrZc00gLreNC3eFFL6pigoAgIAgIAoKAICAI2CICTNhfODNELTz+OpL/S86vQ2pBNdILa1FZ1wRfysfj7+FIkboOCPV2QpSvSNnY4ndFxiwICALWj4DFv/myV7mtkfQ8ZrbHH3/c+r9hMgKzI8CS9JZM0jMAU8eGKpKeCentREzPnxxpdlzaN5BZUIXDyQUIC/HusaZj+7r6su/t5ojZ48PUwvWUUyLbQkoGW0eRBhxtUN/QjHoiKAN8XeHn6QxfCm01RZLNvvRZrhUEzI2AHUXbeDjzYvGPJeaGQuoXBAQBQUAQEAQEAUGgUwScyAEqIdRZLZ0WkhOCgCAgCAgCVomAxb8NM2FtZ2fx3TT5zde86U1esVQ46BDgHKIDq/LePaTDSW965IgAHDpWoGQtWEdx5rjQ7i80YYkdSVmqtpHRfiaste9Vebo6gBcxQUAQEAQEAUFAEBAEBAFBQBAQBAQBQUAQEAQEAdtEwNIdcNVdsUWS3ja/jjLq3iDQZOGa9NqYJie0kvJrNycjObtcO2X29ZYk0nA8WqB030dG+Zq9PWlAEBAEBAFBQBAQBAQBQUAQEAQEAUFAEBAEBAFBQBAwFgGLJ+mPHz9uc3I3xt48KScIMALNxy3+v7G6URGB7pg2oVXm5tPv9/TLDcwimZvft6SqtiaMphwP4rXeL7hLI4KAICAICAKCgCAgCAgCgoAgIAgIAoKAICAICALGIWDx7J4tyt1omvTG3UIpZesINLXkMLAGHBZMjURCfLC+q0+8uQ4VpMluTluzNRUNTZSAMsYfC6dGmbMpqVsQEAQEAUFAEBAEBAFBQBAQBAQBQUAQEAQEAUFAEOgxAkLS9xgyuUAQsCwEKMeoVdlZc2IRTslbNXv5w83gpK7msBVrDiMzp1RVvezUUeZoQuoUBAQBQUAQEAQEAUFAEBAEBAFBQBAQBAQBQUAQEAT6hIDFZ2QtLy/H4sWL1SCbm5vBXua88DYbrxsaGkhr2gEsjcPGSVe5jGHyVcPtYcOG6cs1NjbC0dERQ4cOVQuX4/O85mOsh89rbZ/P8T6bdryqqgqenp76NjvyhOe+aX03XPPx9guPWRuLakg+BIEuEGhqPtHFWcs7NXQIsGx+PD75YR+KSqpVB99fsRPLF43GKBMmdf1hQzLp0Oer+q9cngh7i5+StLx7JT0SBAQBQUAQEAQEAUFAEBAEBAFBQBAQBAQBQUAQMD8CFk/Sr127FseOHVPEORPmTJJrRLm2nZGRgaioqDakPEOnkeXa2pAo14jxgoIC+Pj4KLKfCX8+zmtempqa1KLtG57Xrud1bm4uAgIC9HeL29MmBXitbWukPq8NF208huMbM2aMvj7ZEAS6QqCxmVhvKzPWhb/y7AlYsz0Nu/Znq95/9fMBHBsZhFmkW+/j4djrETU0HceG3VnYlZQNP183XLp0HNycLf5PXa/Ha4kXxoe4W2K3pE+CgCAgCAgCgoAgIAgIAoKAICAICAKCgCAgCFgkAhbPXCUmJoIXMUFAEBhcCDg5DMPpM4cjOsQL64isZ6/6fYfykJxRgukTIjCGNOTdXOyNHnRZVT32Hi3ATiLn6+ubkDg2DEtmxBh9vRQ0HQLuzsbfN9O1KjUJAoKAICAICAKCgCAgCAgCgoAgIAgIAoKAIGCdCFg8SW+dsEqvBQFBwFgERkb5IoqI+u0HcnAwuRCFxVX4deMxrNmUjNAgT0SFeiMy1AtM6js52MHRntfDkFdcjdyiKhSV1aK4rAbpGcVopiiWGYlRGB8X2CdvfGP7LuXaIpCaU9H2gOwJAoKAICAICAKCgCAgCAgCgoAgIAgIAoKAICAIdIuAkPTdQiQFBAFBwNwIMOk+e0K4Wg6lFWP/sQIcSSlEVm6ZWtZv77oHzk72iB8RgPHxweSZ79F1YTkrCAgCgoAgIAgIAoKAICAICAKCgCAgCAgCgoAgIAhYEAJC0lvQzZCuCAKCAMCe9bzU1MeiuLwOJeQlX8gLyeE0NuqSQ2s4+Xk7Iy7KDzHkac8JacUEAUFAEBAEBAFBQBAQBAQBQUAQEAQEAUFAEBAEBAFrQ0BIemu7Y9JfQcBGEHBxtINLgBvCaRETBAQBQUAQEAQEAUFAEBAEBAFBQBAQBAQBQUAQEAQGKwJDB+vAZFyCgCAgCAgCA4OAh7PM/w4M8oOz1eS86sE5MBmVICAICAKCgCAgCAgCgoAgIAgIAoKAINCCgJD08lUQBAQBQUAQMAkC9Q2Nqp6RYe4mqU8qEQRe/TEZ972zT4AQBAQBQUAQEAQEAUFAEBAEBAFBQBAQBAY1AkLSD+rbK4MTBAQBQaD/EMgtquq/xqQlm0AgJbcGOQU1NjFWGaQgIAgIAoKAICAICAKCgCAgCAgCgoDtIiAkve3eexm5ICAICAImRaC5uQlRwZJDwKSg2nhlZdUNGCpZoW38WyDDFwQEAUFAEBAEBAFBQBAQBAQBQWDwI2DzJH1TUxM+//xzZGRkDP67LSMUBAQBQcCMCOQXVcNV9OjNiLBtVj3U5p9UbPO+y6gFAUFAEBAEBAFBQBAQBAQBQUAQsCUEbP7V9+uvv8aFF16IdevW2dJ9l7EOAgSmxnqrUZRV1g6C0cgQBgMCBSR3E+LjPBiGImOwIASGiSe9Bd0N6YogIAgIAoKAICAICAKCgCAgCAgCgoA5ELB5kr6oqEjhOnXqVHPgK3UKAmZFIDzQFfnF1WZtQyoXBIxBII++h7X1TZg/1s+Y4lJGEDAagSHDbP5RxWispKAgIAgIAoKAICAICAKCgCAgCAgCgoB1ImDzb75fffUVAgICEBsba513UHpt0wjEh3sgT5J12vR3wFIGfyyjZcIz1sdSuiT9GCQIDBsyZJCMRIYhCAgCgoAgIAgIAoKAICAICAKCgCAgCHSMgE2T9CUlJVi1ahUuv/xyDOknEiApKQmvvvoqLrroIgQGBmLOnDmoq6vr+O7IUUGgGwRGhrkhJ6+sm1JyWhAwPwJZueUqaay7aNKbH2wba2GYnZD0NnbLZbiCgCAgCAgCgoAgIAgIAoKAICAI2BwCNk3Sf/nll+qGL1u2rMsbzyT6s88+i9mzZysyn8n1e+65B0eOHOnwutraWtTX17c5x4lpx48fjzFjxuC2227DZ599ps5zuf6aIGjTIdkZFAjEh7ircew6nD8oxiODsE4ESqvqkZJVhokteRKscxTSa0tFQH4jLfXOSL8EAUFAEBAEBAFBQBAQBAQBQUAQEARMhYBNk/Svv/66krqZNWtWp3geOHAACQkJuPfee7FhwwZMnz4dYWFheO655xAfH48VK1bor01NTcVll10GFxcX+Pv7tzn39ttvY+/evarsrbfeiqysLOTn52PLli1wdHTU1yEbgkBPEODksT6eTti2L6snl0lZQcCkCGzYmabqu2JuuEnrlcoEgebjJ2DXT5FugrYgIAgIAoKAICAICAKCgCAgCAgCgoAgMFAI2CxJz4T5zp07cd1112HYsGGd4r9p0yakpKSo82+88QZ4f8eOHYpwHzduHJYvX459+/bh119/VZ7yH330EWJiYjBlyhRVduPGjXj88cdVO1yejeVumOQvKChQ+/IhCPQFgbOmB6GopBqpORV9qUauFQR6hUBtQzMOpxYjIcYLoT7OvapDLhIEOkOgofE4horcTWfwyHFBQBAQBAQBQUAQEAQEAUFAEBAEBIFBgoDNkvScMJaNSfauzNvbW52+/fbbceONN+qLjh07Fk888YTaX7t2LS655BJUVlbi5ZdfRnJyMlavXq3Os/b8ww8/jMbGRuzatUvJ3MTFxeGFF15QmvT333+/8qjXVywbgkAPEVg2NURd8fuOtB5eKcUFgb4jsIWiOOrrm3Dzkpi+VyY1CALtEKhrOC6e9O0wkV1BQBAQBAQBQUAQEAQEAUFAEBAEBIHBh4DNkvTbtm1Td1Mj4Tu7tXZ2duqUm5tbmyJMyL/yyivqGJPumlc8S+AcP35c6dXfcccdStaGz7N3/dChQ3HBBReAk8fyJAF71j/11FMICgrCDz/80KZ+2REEjEWAvZenjPJFVm4Z1mxPN/YyKScI9BmBvOJqbNyRrrzoWXpJTBAwNQLKk36oJI41Na5SnyAgCAgCgoAgIAgIAoKAICAICAKCgGUhoGOgLatP/dIbJtLZEhMTlTQNE+U1NTXIy8tThHtVVRXuvvtu3Hzzzaock+l8LDIyUnnEf/vtt3rP+cWLFyvN+meeeQaL/p+98wCPourC8Jfeew/pJKH3KiAWEERUQAXFhiIiNlT4UWwoKoqCooiCXUQUFQsoShEpIr1DgAQI6aT3Xv977u5sNmWTTUjb5Fyf3Zm5c8u57yxx98yZ79xwg2yv/fbUU0/Jw/nz58uI+rlz52LixImghLUbNmzA/fffj5tuugkZGRlwdHTU7sr7TEAvAs/f0QVT3zmIfUej4eVqi64BLnr140ZMoLEESOZm7cYTsLIwxev3dG/sMNyPCdRJoKi0HDZWHfarSp1s+CQTYAJMgAkwASbABJgAE2ACTIAJtB8CRhWitJ/l6L8S0pWfMmWKRm9eV8+YmBgZFf/ss89KDXul3S233ILZs2dj9OjRsoqc/itXrsSuXbuQkpICb29vKI781NRUuLi4gJz1y5cvl+3Hjx8vI+izs7Px008/ybo9e/agriS2yty8ZQK1Edh+MhkvfHUKFmamuOfWPvB0samtGdcxgSYh8On6IzIXwpsP9sKo3u5NMiYPwgSqE7hu/k54u1lj7dzB1U/xMRNgAkyACTABJsAEmAATYAJMgAkwgXZDoMM66ZUrSJHzmZmZKCsrg7W1NUjWhvbJsZ6Xl4chQ4YoTWUkfWFhIZydnaV0jeaEjh1fX1+Ym5tLjXpqUlxcjNWrV0uJGyUZLdW7u7tLBz7p0xsZ8WP9xIRL4wi8+UsENvwbKx31t43tiUBv+8YNxL2YgA4CFEG/ZuNx6aCfNNIX8yeF6mjJ1UzgygkMm/sPOvvYYc0zg658MB6BCTABJsAEmAATYAJMgAkwASbABJhAGyXQ4Z30zXVdyJlvZWWF6dOn44svvqgxDd0YSE9Ph4ODA0gXn/TquTCBpiAwb00Ydh9NlEONu6YL+nXxaIpheQwmANKgJ4mbopJSsIOePxDNTaC0vALDhZO+W6ADvp49sLmn4/GZABNgAkyACTABJsAEmAATYAJMgAm0GgEWem0m9FFRUXLkoUOH1joDac+z/nytaLjyCgksua8Hfg11xuJ1Z/DXrnCcikjC1QMCOKr+Crl25O4ZuUXYfTgKYeKzRGX+1G6YNNi7IyPhtbcAAUoaS8WUE8e2AG2eggkwASbABJgAE2ACTIAJMAEmwARakwA76ZuIflZWFkiuZvDgwXjggQekjj0NPXAgR/81EWIepgEEJg3xQrdOtli0/jwiojPw/R/H4ePlyM76BjDkppCR8wdOxWmc80E+jnjlzlB0FfIjXJhAcxMoLlOlzOEnzZqbNI/PBJgAE2ACTIAJMAEmwASYABNgAq1NgJ30TXQF9u7dKxPHUvLYy5cvY9OmTXLk0FDWa24ixDxMAwmQI3XN0/2xPyIDKzZdxPmYTOmst7O1hJ+3A/y9HUVyWVtOMNtAru21OUnZkN58YmoOohOyEJ+QKWVtaL3+nRzx0NhAjO3l3F6Xz+tqgwSKS8qkVSasBtcGrw6bxASYABNgAkyACTABJsAEmAATYAJNSYA16ZuIZkVFBebMmYP3339fMyJJ3ezbt09zzDtMoDUJnIvLwfbT6TgamYnYxBxk5RTVMMe/k5O6ThXBquQxpoTG4o+F6pxxBWR6YyPhOatQyVEoPjQjUVehrqvsq9WulfIiSyuF+dI+7VWrl1QB1TrolLJM7WaVZ7VrRVs6LK9cVIWyaFFddZyKasdVx2mNo8KiEiSl5umcOiTAFV39HXBtTyeMCHbQ2Y5PMIHmIhCbmo87Fu3DoG6uWDGzT3NNw+MyASbABJgAE2ACTIAJMAEmwASYABNodQIcSd9El4CcmMuWLcPNN9+MmTNnIjIyEkuWLGmi0XkYJnDlBCiyXiVT4i8Hi08vQHxaodwPT8hBTkGJXpMcPp+pbkcu6koHtaqy9rq8gjJEXc7Va3xupB8BKwtTBAlJI32Kt7MVvF0samnqJuvKyo0R6GkDGtPC1Agjuig3a2rpwlVMoIUIFCma9CbV/860kAE8DRNgAkyACTABJsAEmAATYAJMgAkwgRYiwE76JgY9atQonDp1Crm5uXB3d2/i0Xk4JtB0BDoJxy29qAwOaYBT9sams6GxI+UUlOKseDLAUIu9lSnruhvqxWO7W4xAcan6SR1OHNtizHkiJsAEmAATYAJMgAkwASbABJgAE2gdAgbtpD969CjWrVuHYcOGYeLEia1DsJZZra2tQa/mLtu3b8e3336LxYsXw8PDo7mn4/GZQJshYCec3A26sdBmLGdDmAAT0JeA4qQ34Uh6fZFxOybABJgAE2ACTIAJMAEmwASYABMwUAKKlLTBmb9161YMGDAAq1ev7rAR6xEREfj666/lTYqcHMONKja4Dx8bzASYABNgAs1OoLhUZn0A++ibHTVPwASYABNgAkyACTABJsAEmAATYAKtTMAgnfR5eXm47777pHP+2LFj0kndyhxbZXrSvp81a5bUv3/llVdaxQaelAkwASbABJhAcxDQRNJrJWRujnl4TCbABJgAE2ACTIAJMAEmwASYABNgAq1NwCDlbt59910kJyfjt99+g7e3d50MS0pKcOTIEakRTzrxKSkpyMrKQnFxMcrKytCzZ09MmjSpzjHa6kkTExO8/fbbWLt2rUxa+8ILL8DV1bWtmst2MQEmwASYABPQm0CJ+H80FVNTg4wn0Hud3JAJMAEmwASYABNgAkyACTABJsAEmIDBOenJsf7RRx+hd+/euPXWW2tcwdOnT2Pv3r2gCHvSrD948GCNNtUrzp49i65du1avNohje3t7LFy4EHPmzMGnn34KctRzYQJMgAkwASZg6AQUuRvOG2voV5LtZwJMgAkwASbABJgAE2ACTIAJMIH6CBick/6///6TUfTPPPMMjLQegSd9doqIP3PmTJU1BwUFSTkYqly+fDnc3Nzg7OwMikKn/uXl5fD396/Sx9AOHn74YZDcDSWQnTdvHszMzAxtCWwvE2ACTIAJMIEqBEpKy+Wxidb/66s04AMmwASYABNgAkyACTABJsAEmAATYALthIDBOekjIyMl+k6dOlW5BOSkJwe9nZ0dFixYgFGjRqFXr17iMXlTdO7cGba2tnjyySer9Kl+cPjwYezZswfZ2dno1q0bbr75ZlhZWVVvpvfx5cuXERsbix49esDGxqbWfiS7c/78eSlT4+HhUaVNaWkp9u/fj927d2PHjh04cOAAZsyYgffee69KO1rbY489JqVvdu3ahdGjR1c5zwdMgAkwASbABAyNQEmZKnGssamRoZnO9jIBJsAEmAATYAJMoFEELibmobNn7b6DRg3InZgAE2ACTMBgCBick56c8FSOHz8uk8cqpMmhTg72/v3713CskyOcoufrKr///nsN+ZzQ0FB89913GDBggOwaHx+PdevWITU1VY43YcIEUJvqJT8/X94Q+PLLLzWnfvnllxra96SpTzcO4uLiZLvrr78eP/30kxx78+bNmDZtmnxqQBnE3d0dhYWFymGV7fTp06WT/ueff2YnfRUyfKAQoCSMMSkF8HezgpmBaTzPXxOG5MzKz37fIAfMHh+sLE2z1bedpkMDdvKLyhCfXgA3ews42vDTKg1Ax02ZQKMIKIljzTiSvlH8uBMTYAJMgAkwASbQtgkkZRXh+KVMnLyUhRPidT42Wxo8oIszPp7Vr20bz9YxASbABJhAkxMwOCf94MGDJYTPPvsML730EpycnDRQhg8frtnX3qFEsRRRr6tQxDpFqFOZO3cuhg0bBorMX7ZsGQYOHIjo6GhcuHBBRucrY9DNglWrVmH9+vXo16/yf6BFRUW47bbbsGXLFhnVTw7+nTt3Soc73UhQpGjIgf/QQw/J4R54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AQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD6B/w+jDwOxnklQVQAAAABJRU5ErkJggg==" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simple example\n", - "\n", - "Let's consider a toy example: I have a system that accepts logs and perform two separate sub-tasks. First, it will summarize them. Second, it will summarize any failure modes captured in the logs. I want to perform these two operations in two different sub-graphs.\n", - "\n", - "The most important thing to recognize is the information transfer between the graphs. `Entry Graph` is the parent, and each of the two sub-graphs are defined as nodes in `Entry Graph`. Both subgraphs inherit state from the parent `Entry Graph`; I can access `docs` in each of the sub-graphs simply by specifying it in the sub-graph state (see diagram). Each subgraph can have its own private state. And any values that I want propagated back to the parent `Entry Graph` (for final reporting) simply need to be defined in my `Entry Graph` state (e.g., `summary report` and `failure report`).\n", - "\n", - "![Screenshot 2024-07-12 at 10.35.41 AM.png](attachment:9145adc1-ce9d-4a22-8183-e13796d4a388.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from operator import add\n", - "from typing import List, TypedDict, Optional, Annotated, Dict\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import StateGraph, START, END\n", - "\n", - "\n", - "# The structure of the logs\n", - "class Logs(TypedDict):\n", - " id: str\n", - " question: str\n", - " docs: Optional[List]\n", - " answer: str\n", - " grade: Optional[int]\n", - " grader: Optional[str]\n", - " feedback: Optional[str]\n", - "\n", - "\n", - "# Failure Analysis Sub-graph\n", - "class FailureAnalysisState(TypedDict):\n", - " docs: List[Logs]\n", - " failures: List[Logs]\n", - " fa_summary: str\n", - "\n", - "\n", - "def get_failures(state):\n", - " docs = state[\"docs\"]\n", - " failures = [doc for doc in docs if \"grade\" in doc]\n", - " return {\"failures\": failures}\n", - "\n", - "\n", - "def generate_summary(state):\n", - " failures = state[\"failures\"]\n", - " # Add fxn: fa_summary = summarize(failures)\n", - " fa_summary = \"Poor quality retrieval of Chroma documentation.\"\n", - " return {\"fa_summary\": fa_summary}\n", - "\n", - "\n", - "fa_builder = StateGraph(FailureAnalysisState)\n", - "fa_builder.add_node(\"get_failures\", get_failures)\n", - "fa_builder.add_node(\"generate_summary\", generate_summary)\n", - "fa_builder.add_edge(START, \"get_failures\")\n", - "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", - "fa_builder.add_edge(\"generate_summary\", END)\n", - "\n", - "\n", - "# Summarization subgraph\n", - "class QuestionSummarizationState(TypedDict):\n", - " docs: List[Logs]\n", - " qs_summary: str\n", - " report: str\n", - "\n", - "\n", - "def generate_summary(state):\n", - " docs = state[\"docs\"]\n", - " # Add fxn: summary = summarize(docs)\n", - " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", - " return {\"qs_summary\": summary}\n", - "\n", - "\n", - "def send_to_slack(state):\n", - " qs_summary = state[\"qs_summary\"]\n", - " # Add fxn: report = report_generation(qs_summary)\n", - " report = \"foo bar baz\"\n", - " return {\"report\": report}\n", - "\n", - "\n", - "def format_report_for_slack(state):\n", - " report = state[\"report\"]\n", - " # Add fxn: formatted_report = report_format(report)\n", - " formatted_report = \"foo bar\"\n", - " return {\"report\": formatted_report}\n", - "\n", - "\n", - "qs_builder = StateGraph(QuestionSummarizationState)\n", - "qs_builder.add_node(\"generate_summary\", generate_summary)\n", - "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", - "qs_builder.add_node(\"format_report_for_slack\", format_report_for_slack)\n", - "qs_builder.add_edge(START, \"generate_summary\")\n", - "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", - "qs_builder.add_edge(\"send_to_slack\", \"format_report_for_slack\")\n", - "qs_builder.add_edge(\"format_report_for_slack\", END)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that each sub-graph has its own state, `QuestionSummarizationState` and `FailureAnalysisState`.\n", - " \n", - "After defining each sub-graph, we put everything together." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Dummy logs\n", - "question_answer = Logs(\n", - " id=\"1\",\n", - " question=\"How can I import ChatOllama?\",\n", - " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", - ")\n", - "\n", - "question_answer_feedback = Logs(\n", - " id=\"2\",\n", - " question=\"How can I use Chroma vector store?\",\n", - " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", - " grade=0,\n", - " grader=\"Document Relevance Recall\",\n", - " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", - ")\n", - "\n", - "\n", - "# Entry Graph\n", - "class EntryGraphState(TypedDict):\n", - " raw_logs: Annotated[List[Dict], add]\n", - " docs: Annotated[List[Logs], add] # This will be used in sub-graphs\n", - " fa_summary: str # This will be generated in the FA sub-graph\n", - " report: str # This will be generated in the QS sub-graph\n", - "\n", - "\n", - "def convert_logs_to_docs(state):\n", - " # Get logs\n", - " raw_logs = state[\"raw_logs\"]\n", - " docs = [question_answer, question_answer_feedback]\n", - " return {\"docs\": docs}\n", - "\n", - "\n", - "entry_builder = StateGraph(EntryGraphState)\n", - "entry_builder.add_node(\"convert_logs_to_docs\", convert_logs_to_docs)\n", - "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", - "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", - "\n", - "entry_builder.add_edge(START, \"convert_logs_to_docs\")\n", - "entry_builder.add_edge(\"convert_logs_to_docs\", \"failure_analysis\")\n", - "entry_builder.add_edge(\"convert_logs_to_docs\", \"question_summarization\")\n", - "entry_builder.add_edge(\"failure_analysis\", END)\n", - "entry_builder.add_edge(\"question_summarization\", END)\n", - "\n", - "graph = entry_builder.compile()\n", - "\n", - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_logs': [{'foo': 'bar'}, {'foo': 'baz'}],\n", - " 'docs': [{'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", - " {'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", - " {'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],\n", - " 'fa_summary': 'Poor quality retrieval of Chroma documentation.',\n", - " 'report': 'foo bar'}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "raw_logs = [{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]\n", - "graph.invoke({\"raw_logs\": raw_logs}, debug=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Custom reducer functions to manage state\n", - "\n", - "Now, let's highlight a possible stumbling block when we use the same `State` across multiple sub-graphs.\n", - " \n", - "We will create two graphs: a parent graph with a few nodes and a child graph that is added as a node in the parent.\n", - "\n", - "We define a custom [reducer](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) function for our state." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " return left + right\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.add_edge(START, \"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.add_edge(\"child_end\", END)\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.add_edge(START, \"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.add_edge(\"fin\", END)\n", - "graph = builder.compile()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", - "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n", - "\u001b[0m{'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", - "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n", - "\u001b[0m{'name': 'test', 'path': ['grandparent']}\n", - "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test', 'path': ['grandparent']}\n", - "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", - "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n", - "\u001b[0m{'name': 'test', 'path': ['grandparent', 'parent']}\n", - "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", - "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", - "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling']\n", - "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling']}\n", - "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling']}\n", - "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n", - "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling',\n", - " 'fin']}\n" - ] - }, - { - "data": { - "text/plain": [ - "{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling',\n", - " 'fin']}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice here that the `[\"grandparent\", \"parent\"]` sequence is duplicated! \n", - "\n", - "This is because our child state has received the full parent state and returns the full parent state once it terminates. \n", - "\n", - "To avoid duplication or conflicts in state, you typically would do one or more of the following:\n", - "\n", - "1. Handle duplicates in your `reducer` function.\n", - "2. Call the child graph from within a python function. In that function, handle the state as needed. \n", - "3. Update the child graph keys to avoid conflicts. You would still need to ensure the output can be interpreted by the parent, however.\n", - "\n", - "Let's re-implement the graph using technique (1) and add unique IDs for every value in the list. This is what is done in [`MessageGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph)." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "import uuid\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " left_, right_ = [], []\n", - " for orig, new in [(left, left_), (right, right_)]:\n", - " for val in orig:\n", - " if not isinstance(val, dict):\n", - " val = {\"val\": val}\n", - " if \"id\" not in val:\n", - " val[\"id\"] = str(uuid.uuid4())\n", - " new.append(val)\n", - " # Merge the two lists\n", - " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", - " merged = left_.copy()\n", - " for val in right_:\n", - " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", - " merged[existing_idx] = val\n", - " else:\n", - " merged.append(val)\n", - " return merged\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.add_edge(START, \"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.add_edge(\"child_end\", END)\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.add_edge(START, \"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.add_edge(\"fin\", END)\n", - "graph = builder.compile()" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAKwATIDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAUGBAcIAwkCAf/EAFgQAAEDAwEDBAoNCQcDAgUFAAEAAgMEBQYRBxIhCBMWMRQiQVFVdHWUstEVFzI0NTY3OFZhk7GzI0JUcXOBo9LhM1J2kZKhtAliclOiGCQlQ2NGlsHU8P/EABsBAQADAQEBAQAAAAAAAAAAAAACAwUEAQYH/8QAPBEBAAECAgYFCgUEAgMAAAAAAAECAwQREhMhMVGRFEFScaEVMjM0U2GxwdHwBSKBktIjYuHxQnJjorL/2gAMAwEAAhEDEQA/APqmiIgIiICIiAiIgIiICIiAiIgIiICxq25UltY19XVQ0rHHRrp5AwE94alZK15tSpYay8YrHPDHPGZ6glkjQ4f2J7hXuyImqrdETPKJlbao1lcUcVv6VWXwxQecs9adKrL4YoPOWeta76PWvwbR/YM9SdHrX4No/sGepZHlXD9irnDU8nf3eDYnSqy+GKDzlnrTpVZfDFB5yz1rXfR61+DaP7BnqTo9a/BtH9gz1J5Vw/Yq5weTv7vBsTpVZfDFB5yz1p0qsvhig85Z61rvo9a/BtH9gz1J0etfg2j+wZ6k8q4fsVc4PJ393g2J0qsvhig85Z606VWXwxQecs9a130etfg2j+wZ6k6PWvwbR/YM9SeVcP2KucHk7+7wbE6VWXwxQecs9adKbL4XoPOWeta76PWvwbR/YM9Sgs8sdthwfIZI7fSse231DmubC0EHm3aEHRXWfxHD3rlNvRmM5iN8dZP4flGel4N6oiLQYwiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKg7Svh3FP29R+CVflQdpXw7in7eo/BK8q9Hc/61f/MunDemp72KiIvgH1SMybJrXh1hrb1eq2O32ujZzk9TLrusGunc4kkkAAcSSAFrPL+UtjdhsFju9ujrbrS3G9w2eT/6dVxyU+9uukcYzDvlwa5pazQF+92uuhVw2t2y13nZ1eqK82i4322yxtE1DaWF9W/t2kOiAIO8wgPGh17Xhr1LRc5zm84DS11fbMhvtuxvMqC4W91xt/M3mrt0RaZHPpwGl72uc4A7oc8N1IXXZt0VRnVxc12uqmcqeDdGTbb8Nw+jtlVeLlU0Udxpuy4GuttU54h0Gr5GNiLogNRrzgbp3dF6X/bThmNS2qOtvQdJdqM19vZSU01U6rhG7q6IRMdv8HtOg1JGp00BI1ptFvl7ynK7c+e25zFhlXZi+korDTTUtRLcDK9rmVbm7r4W82Iy0Pc1h3nFx4aKJ2F4leqC/bJH3OxXCjNow6vt9S+rpHsFNUtqYGBhcRoC5rHlvHtm8RqOKlFmiKdKfj3o6yvSyj73NkYtt6tWT7U77hjKOvgloW03Y1S631QbO6SN8j98mINhDQ0AF7hvnXTXqW0Fp60T12HcoTL31lju9Rb8ngtnYNyoqJ89Mx0LJY5GzSN4RaFzT22moK3CqLsUxMaMbMo+C63MzE6XGRQG0D4h5H5OqPw3KfUBtA+IeR+Tqj8NyuwXrNr/ALR8YTq3S3OiIvtHxwiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKg7Svh3FP29R+CVflBZRh9HlnYZqpqqnkpHufFJSS824Et3Tx07xXuUVRVTM5ZxMc4mF1muLdyKp6musowuwZvRxUmQ2WgvdLFJzscNfTtmYx+hG8A4EA6EjX61Wv8A4f8AZnpp0AxvTvexcP8AKto+1VQ+GL357/RPaqofDF789/osOPwqumMovfFrzjbM7ZpULG9lGF4dchcLFilms9eGGMVNDQxxSbp6xvNAOh0VrUl7VVD4Yvfnv9E9qqh8MXvz3+ijP4TNW2bscpSjHWo2REo1FrTlLUtbsvxrE62xXu6Rz3LKLdaagz1HOAwTPcJABpwOg4HuLbvtVUPhi9+e/wBF55H/APLHKXvlC1wlDVtFT3KiqKSrhjqaWojdFLDK0OZIxw0c1wPAggkEKjjYBszaQRgOOAjiCLXCCP8A2raPtVUPhi9+e/0T2qqHwxe/Pf6KUfhVVO69EfpKM46zVvpaxg2C7N6WeOaHA8dimjcHsey2QhzXA6gg7vAqd2gfEPI/J1R+G5XH2qqHwxe/Pf6LyrNj1ruFJNS1N0vM1PMx0ckbq3g5pGhB4d0K+z+GzReouV3c9GYndPVLzptmImIhe0RFqsIREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQc78tr4k7P/8AHVm/EcuiFzvy2viTs/8A8dWb8Ry6IQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREHO/La+JOz//AB1ZvxHLohc78tr4k7P/APHVm/EcuiEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARRl+yKgxqkbUV8/Nh7tyKJjS+SZ+mu6xg4uOgJ0A4AEngCVT5toN+q3E0VjpqOHhum41ZMpH1sja4D/AFn12026qoz3R79n+11Fqu55sNhota9M8u/RrJ/qmTpnl36NZP8AVMpaqO1HNb0S9wbKRa16Z5d+jWT/AFTJ0zy79Gsn+qZNVHajmdEvcHym5bWwo7DNuN0pqKnEOOXnW52vcboyON7jvwjuDm37zQP7u4T1ru//AKb2xObZlsWkyW4Rvhu+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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", - "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n", - "\u001b[0m{'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", - "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n", - "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n", - "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", - "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'}], ['sibling']\n", - "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n", - "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n", - "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n", - "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'},\n", - " {'id': 'a4328c5f-845a-43de-b3d7-53a39208e316', 'val': 'fin'}]}\n" - ] - }, - { - "data": { - "text/plain": [ - "{'name': 'test',\n", - " 'path': [{'val': 'grandparent', 'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49'},\n", - " {'val': 'parent', 'id': '2a6f0263-3949-4e47-a210-57f817e6097d'},\n", - " {'val': 'child_start', 'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088'},\n", - " {'val': 'child_middle', 'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783'},\n", - " {'val': 'child_end', 'id': '669dd810-360f-4694-a9f3-49597f23376a'},\n", - " {'val': 'sibling', 'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718'},\n", - " {'val': 'fin', 'id': 'a4328c5f-845a-43de-b3d7-53a39208e316'}]}" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" + "cells": [ + { + "attachments": { + "71516aef-9c00-4730-a676-a54e90cb6472.png": { + "image/png": "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" } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to create subgraphs\n", + "\n", + "For more complex systems, subgraphs are a useful design principle. Subgraphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", + "\n", + "![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "attachments": { + "9145adc1-ce9d-4a22-8183-e13796d4a388.png": { + "image/png": "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gAACCCCAAAII1LMAQfp6BqZ6BBBAAIHGK6CBzaefftoO4Ne//nW5gVx99dU2QN+mTZty2/XD2rVr5bvvvhNNSRMeHi5Tp06VoKCgcsdt3brVBlg14K+B4/vuu8+mqXnhhRckICCg3LFVfcjJybGHxMfXfDFBDfLecsstkpJSuhiiVqQL4mrffVwWQHT6MGvWLHnggQckMzPTbho2bJhon9u2bescYuv685//bIP+ulHT7+gxNb2ZsWfPHvn0009l3759tv7zzjuvxkF1XVD3F7/4hXzxxRdl/dOUQDpGZ7Fd3aGGr776qlx66aXSvXv3smOdfXp8ZR76HXG2640ODfrr4r0Vi+tx+j45OfkUj4KCAjteTavUsmVLGTBggKhvxaL79Tulixc7Rcf0t7/9TZYtW2Zvrriul3DkyBEZO3as5Ofn2+/lmW6sOPXxigACVQuk52SImH+fYyIbdm2SyUMny5uzXpcFJlA/YcC4qjvqpUc4f/Z66fAYFgIIIIAAAgg0MQHS3TSxC85wEUAAAQSqL6ALsGogVNPdtGrVqtyJN9xwg00tUzEwrovLXnzxxfLEE0/Ic889J/fff79dxPXAgQPlztf9Wm9qaqrcfPPNsmXLFvn666/l448/LndcdT5oznwtsbE1T7fw3nvvlQXodUw6k37lypXyySefnHKzQJ8quO2222yA/sorr5S+ffva2em6gK1TNN3OFVdcYQP048aNk8mTJ9tUQepQk6Kz3s855xz561//Ks8//7w8++yzctNNN1nzmtSj43EC9PrkgS6w66QE6tq1a1lVOl5tZ/r06WXb9I0+NTB69GhxxqhPF+gNDb25oYFvHaMG+7VevYlw2WWX2fP1vCeffFLy8vJk7ty59smLP/zhD3afjunCCy+Uxx9/3H7WX1qfWukaB5pGSdv70Y9+ZK9D2UEn3/zjH/+w30udPa/fsUceecQ+raBPeujCxnpDoX379mWnqYHeVNGnIlxvppQdwBsEEKiVQGauCdKb0j70h3/falVRDU9q16adjOg/RpK3r5XktE01PNubDuevst50NRkLAggggAACTV2A/7Np6t8Axo8AAgggcFoBDbBqiYysXhoDDdBrwHTIkCE2aKsznnXBWU1L8tRTT5Vrx5ktf9VVV4neDNDjtKxatarccdX5oEH6083grur86667Tnr06GEPe/31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wASYABPQm0CJ+H80FVNTg4wn0Hud3JAJMAEmwASYABNgAkyACTABJsAEmIDBOenJsf7RRx+hd+/euPXWW2tcwdOnT2Pv3r2gCHvSrD948GCNNtUrzp49i65du1avNohje3t7LFy4EHPmzMGnn34KctRzYQJMgAkwASZg6AQUuRvOG2voV5LtZwJMgAkwASbABJgAE2ACTIAJMIH6CBick/6///6TUfTPPPMMjLQegSd9doqIP3PmTJU1BwUFSTkYqly+fDnc3Nzg7OwMikKn/uXl5fD396/Sx9AOHn74YZDcDSWQnTdvHszMzAxtCWwvE2ACTIAJMIEqBEpKy+Wxidb/66s04AMmwASYABNgAkyACTABJsAEmAATYALthIDBOekjIyMl+k6dOlW5BOSkJwe9nZ0dFixYgFGjRqFXr17iMXlTdO7cGba2tnjyySer9Kl+cPjwYezZswfZ2dno1q0bbr75ZlhZWVVvpvfx5cuXERsbix49esDGxqbWfiS7c/78eSlT4+HhUaVNaWkp9u/fj927d2PHjh04cOAAZsyYgffee69KO1rbY489JqVvdu3ahdGjR1c5zwdMgAkwASbABAyNQEmZKnGssamRoZnO9jIBJsAEmAATYAJMoFEELibmobNn7b6DRg3InZgAE2ACTMBgCBick56c8FSOHz8uk8cqpMmhTg72/v3713CskyOcoufrKr///nsN+ZzQ0FB89913GDBggOwaHx+PdevWITU1VY43YcIEUJvqJT8/X94Q+PLLLzWnfvnllxra96SpTzcO4uLiZLvrr78eP/30kxx78+bNmDZtmnxqQBnE3d0dhYWFymGV7fTp06WT/ueff2YnfRUyfKAQoCSMMSkF8HezgpmBaTzPXxOG5MzKz37fIAfMHh+sLE2z1bedpkMDdvKLyhCfXgA3ews42vDTKg1Ax02ZQKMIKIljzTiSvlH8uBMTYAJMgAkwASbQtgkkZRXh+KVMnLyUhRPidT42Wxo8oIszPp7Vr20bz9YxASbABJhAkxMwOCf94MGDJYTPPvsML730EpycnDRQhg8frtnX3qFEsRRRr6tQxDpFqFOZO3cuhg0bBorMX7ZsGQYOHIjo6GhcuHBBRucrY9DNglWrVmH9+vXo16/yf6BFRUW47bbbsGXLFhnVTw7+nTt3Soc73UhQpGjIgf/QQw/J4R54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AQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD6B/w+jDwOxnklQVQAAAABJRU5ErkJggg==" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simple example\n", + "\n", + "Let's consider a toy example: a system that accepts logs and perform two separate sub-tasks. First, it will summarize them. Second, it will summarize any failure modes captured in the logs. These two operations will be performed by two different subgraphs.\n", + "\n", + "The most important thing to recognize is the information transfer between the graphs. `Entry Graph` is the parent, and each of the two subgraphs are defined as nodes in `Entry Graph`. Both subgraphs inherit state from the parent `Entry Graph`; I can access `docs` in each of the subgraphs simply by specifying it in the subgraph state (see diagram). Each subgraph can have its own private state. And any values that I want propagated back to the parent `Entry Graph` (for final reporting) simply need to be defined in my `Entry Graph` state (e.g., `summary report` and `failure report`).\n", + "\n", + "![Screenshot 2024-07-12 at 10.35.41 AM.png](attachment:9145adc1-ce9d-4a22-8183-e13796d4a388.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define subgraphs" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Optional, Annotated\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "# The structure of the logs\n", + "class Logs(TypedDict):\n", + " id: str\n", + " question: str\n", + " answer: str\n", + " grade: Optional[int]\n", + " feedback: Optional[str]\n", + "\n", + "\n", + "# Define custom reducer (see more on this in the \"Custom reducer\" section below)\n", + "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", + " if not left:\n", + " left = []\n", + " \n", + " if not right:\n", + " right = []\n", + "\n", + " logs = left.copy()\n", + " left_id_to_idx = {log[\"id\"]: idx for idx, log in enumerate(logs)}\n", + " # update if the new logs are already in the state, otherwise append\n", + " for log in right:\n", + " idx = left_id_to_idx.get(log[\"id\"])\n", + " if idx is not None:\n", + " logs[idx] = log\n", + " else:\n", + " logs.append(log)\n", + " return logs\n", + "\n", + "\n", + "# Failure Analysis Subgraph\n", + "class FailureAnalysisState(TypedDict):\n", + " # keys shared with the parent graph (EntryGraphState)\n", + " logs: Annotated[list[Logs], add_logs]\n", + " failure_report: str\n", + " # subgraph key\n", + " failures: list[Logs]\n", + "\n", + "\n", + "def get_failures(state: FailureAnalysisState):\n", + " failures = [log for log in state[\"logs\"] if log[\"grade\"] == 0]\n", + " return {\"failures\": failures}\n", + "\n", + "\n", + "def generate_summary(state: FailureAnalysisState):\n", + " failures = state[\"failures\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " failure_ids = [log[\"id\"] for log in failures]\n", + " fa_summary = f\"Poor quality of retrieval for document IDs: {', '.join(failure_ids)}\"\n", + " return {\"failure_report\": fa_summary}\n", + "\n", + "\n", + "fa_builder = StateGraph(FailureAnalysisState)\n", + "fa_builder.add_node(\"get_failures\", get_failures)\n", + "fa_builder.add_node(\"generate_summary\", generate_summary)\n", + "fa_builder.add_edge(START, \"get_failures\")\n", + "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", + "fa_builder.add_edge(\"generate_summary\", END)\n", + "\n", + "\n", + "# Summarization subgraph\n", + "class QuestionSummarizationState(TypedDict):\n", + " # keys that are shared with the parent graph (EntryGraphState)\n", + " summary_report: str\n", + " logs: Annotated[list[Logs], add_logs]\n", + " # subgraph keys\n", + " summary: str\n", + "\n", + "\n", + "def generate_summary(state: QuestionSummarizationState):\n", + " docs = state[\"logs\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", + " return {\"summary\": summary}\n", + "\n", + "\n", + "def send_to_slack(state: QuestionSummarizationState):\n", + " summary = state[\"summary\"]\n", + " # NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack\n", + " return {\"summary_report\": summary}\n", + "\n", + "\n", + "qs_builder = StateGraph(QuestionSummarizationState)\n", + "qs_builder.add_node(\"generate_summary\", generate_summary)\n", + "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", + "qs_builder.add_edge(START, \"generate_summary\")\n", + "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", + "qs_builder.add_edge(\"send_to_slack\", END)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that each subgraph has its own state, `QuestionSummarizationState` and `FailureAnalysisState`.\n", + " \n", + "After defining each subgraph, we put everything together." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define parent graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Dummy logs\n", + "dummy_logs = [\n", + " Logs(\n", + " id=\"1\",\n", + " question=\"How can I import ChatOllama?\",\n", + " grade=1,\n", + " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", + " ),\n", + " Logs(\n", + " id=\"2\",\n", + " question=\"How can I use Chroma vector store?\",\n", + " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", + " grade=0,\n", + " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", + " ),\n", + " Logs(\n", + " id=\"3\",\n", + " question=\"How do I create react agent in langgraph?\",\n", + " answer=\"from langgraph.prebuilt import create_react_agent\",\n", + " )\n", + "]\n", + "\n", + "\n", + "# Entry Graph\n", + "class EntryGraphState(TypedDict):\n", + " raw_logs: Annotated[list[Logs], add_logs]\n", + " logs: Annotated[list[Logs], add_logs] # This will be used in subgraphs\n", + " failure_report: str # This will be generated in the FA subgraph\n", + " summary_report: str # This will be generated in the QS subgraph\n", + "\n", + "\n", + "def select_logs(state):\n", + " return {\"logs\": [log for log in state[\"raw_logs\"] if \"grade\" in log]}\n", + "\n", + "\n", + "entry_builder = StateGraph(EntryGraphState)\n", + "entry_builder.add_node(\"select_logs\", select_logs)\n", + "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", + "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", + "\n", + "entry_builder.add_edge(START, \"select_logs\")\n", + "entry_builder.add_edge(\"select_logs\", \"failure_analysis\")\n", + "entry_builder.add_edge(\"select_logs\", \"question_summarization\")\n", + "entry_builder.add_edge(\"failure_analysis\", END)\n", + "entry_builder.add_edge(\"question_summarization\", END)\n", + "\n", + "graph = entry_builder.compile()\n", + "\n", + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_logs': [{'id': '1',\n", + " 'question': 'How can I import ChatOllama?',\n", + " 'grade': 1,\n", + " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", + " {'id': '2',\n", + " 'question': 'How can I use Chroma vector store?',\n", + " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", + " 'grade': 0,\n", + " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", + " {'id': '3',\n", + " 'question': 'How do I create react agent in langgraph?',\n", + " 'answer': 'from langgraph.prebuilt import create_react_agent'}],\n", + " 'logs': [{'id': '1',\n", + " 'question': 'How can I import ChatOllama?',\n", + " 'grade': 1,\n", + " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", + " {'id': '2',\n", + " 'question': 'How can I use Chroma vector store?',\n", + " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", + " 'grade': 0,\n", + " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],\n", + " 'failure_report': 'Poor quality of retrieval for document IDs: 2',\n", + " 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.invoke({\"raw_logs\": dummy_logs}, debug=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom reducer functions to manage state\n", + "\n", + "You might have noticed that we defined a custom [reducer]([reducer](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) function (`add_logs`) or the `logs` key in `EntryGraphState`. It is necessary to provide a reducer when using shared state keys across multiple subgraphs.\n", + "\n", + "Let's take a look at implementing a custom reducer. We will create two graphs: a parent graph with a few nodes and a child graph that is added as a node in the parent. We'll also define a custom reducer function (`reduce_list`) for our state. This is functionally equivalent to simply using `operator.add`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "# define a simple reducer\n", + "def reduce_list(left: list, right: list) -> list:\n", + " if not left:\n", + " left = []\n", + " if not right:\n", + " right = []\n", + " return left + right\n", + "\n", + "# define parent and child state\n", + "class ChildState(TypedDict):\n", + " name: str\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "class ParentState(TypedDict):\n", + " name: str\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "# define a helper to build the graph\n", + "def make_graph(parent_schema, child_schema):\n", + " child_builder = StateGraph(child_schema)\n", + " \n", + " child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", + " child_builder.add_edge(START, \"child_start\")\n", + " child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", + " child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", + " child_builder.add_edge(\"child_start\", \"child_middle\")\n", + " child_builder.add_edge(\"child_middle\", \"child_end\")\n", + " child_builder.add_edge(\"child_end\", END)\n", + " \n", + " builder = StateGraph(parent_schema)\n", + " \n", + " builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", + " builder.add_edge(START, \"grandparent\")\n", + " builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", + " builder.add_node(\"child\", child_builder.compile())\n", + " builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", + " builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", + " \n", + " # Add connections\n", + " builder.add_edge(\"grandparent\", \"parent\")\n", + " builder.add_edge(\"parent\", \"child\")\n", + " builder.add_edge(\"parent\", \"sibling\")\n", + " builder.add_edge(\"child\", \"fin\")\n", + " builder.add_edge(\"sibling\", \"fin\")\n", + " builder.add_edge(\"fin\", END)\n", + " graph = builder.compile()\n", + " return graph\n", + "\n", + "\n", + "graph = make_graph(ParentState, ChildState)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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qGMrKWrlllkcWO5vd3f00jd1zSN06HU8V0Wz291ptVHRPrKm4Op4WxGrrHB0026NN95AALj0kgDj4FrO3Om8WX77Eq/wl6blL6w81QWW8VFS7gxk9vmpY9fjdJK1oA+M8T8QJ4JwNcarGbLa7Le8eg/4k/8APkUrWpxSyOx3HqG3vkE0sLP0kjRoHPJLnED4t4lbZefj1RXi11RyTM+aTyiIi4EEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBV35WnfTsG/vAofMkViFXflad9Owb+8Ch8yRBYhERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBV35WnfTsG/vAofMkViFXflad9Owb+8Ch8yRBYhERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEXh80cZ0e9rT8ROi89lQ/7aP+IK2kfVflrtx/pAKzM8qw+C4bNn2GvwrJo7rUUkl6510skG+x0B/0du4dSe67rTToK/UXsqH/AG0f8QX5nf0hXJprZ9tePZJilKKlubVMdvmij9zHcODQXEcGiRmjtfjjkcUtIuJyUuUhcuUzi94yCfDDilrpKltJSym5dl9lv3S6XQc1HuhmsfHjqXEcN0ruShOx/Z7aNj2zTH8PtcsRprXStifKNGmeU91LKR8b3lzv3qY9lQ/7aP8AiCWkfVF8uyof9tH/ABBfVLAiIoCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAtBnV1qLNjNRPSSCGpkkhpo5SAebdLMyIPAIIJG/qNQRqOI0W/UT2od6Q/tG3emwLsZPEVY1ETyXjzWOWGpGA445v6eyUNbKTq6esgbPLI7hq5z3guc46DUkklPa+xb5tWfyCL1VsL/AH2hxexXK83Ofsa226mkq6qfcc/m4o2l73brQSdGgnQAk+AKJ4ftxwnO7zBarNeXS3CopzVU0NTR1FL2TCNCXwmWNolaAQSWE6L0OHxI/nO8vO1vPa+xb5tWfyCL1U9r7Fvm1Z/IIvVWhxXbtg2a5L2v2i+c/dnNkfFBNSTwCcM92YnyMa2Xd8O4XaDilg264PlFbV01svRqTR9kdlzmjqGU9KYHObKJZnRiOMt3SdHOBI0cNQQTOHxOnO8vO1vva+xb5tWf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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'path': []}\n", + "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", + "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': ['grandparent']}\n", + "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test', 'path': ['grandparent']}\n", + "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': ['grandparent', 'parent']}\n", + "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", + "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", + "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling']}\n", + "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling']}\n", + "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n" + ] + }, + { + "data": { + "text/plain": [ + "{'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling',\n", + " 'fin']}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.invoke({\"name\": \"test\"}, debug=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice here that the `[\"grandparent\", \"parent\"]` sequence is duplicated! \n", + "\n", + "This is because our child state has received the full parent state and returns the full parent state once it terminates. \n", + "\n", + "To avoid duplication or conflicts in state, you typically would do one or more of the following:\n", + "\n", + "1. Handle duplicates in your `reducer` function.\n", + "2. Call the child graph from within a python function. In that function, handle the state as needed. \n", + "3. Update the child graph keys to avoid conflicts. You would still need to ensure the output can be interpreted by the parent, however.\n", + "\n", + "Let's re-implement the graph using technique (1) and add unique IDs for every value in the list. This is what is done in [`MessageGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import uuid\n", + "\n", + "def reduce_list(left: list | None, right: list | None) -> list:\n", + " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", + " if not left:\n", + " left = []\n", + " if not right:\n", + " right = []\n", + " left_, right_ = [], []\n", + " for orig, new in [(left, left_), (right, right_)]:\n", + " for val in orig:\n", + " if not isinstance(val, dict):\n", + " val = {\"val\": val}\n", + " if \"id\" not in val:\n", + " val[\"id\"] = str(uuid.uuid4())\n", + " new.append(val)\n", + " # Merge the two lists\n", + " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", + " merged = left_.copy()\n", + " for val in right_:\n", + " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", + " merged[existing_idx] = val\n", + " else:\n", + " merged.append(val)\n", + " return merged\n", + "\n", + "\n", + "class ChildState(TypedDict):\n", + " name: str\n", + " # note the updated reducer here\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "class ParentState(TypedDict):\n", + " name: str\n", + " # note the updated reducer here\n", + " path: Annotated[list[str], reduce_list]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since our graph topology hasn't changed, we can just reuse the same `make_graph` helper function we defined previously and pass new schema for the parent and child graphs." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'path': []}\n", + "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", + "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]}\n", + "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]}\n", + "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'}], ['sibling']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'},\n", + " {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]}\n", + "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'},\n", + " {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]}\n", + "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n" + ] + }, + { + "data": { + "text/plain": [ + "{'name': 'test',\n", + " 'path': [{'val': 'grandparent', 'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7'},\n", + " {'val': 'parent', 'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e'},\n", + " {'val': 'child_start', 'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf'},\n", + " {'val': 'child_middle', 'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3'},\n", + " {'val': 'child_end', 'id': '052b5578-6939-4dc0-8e24-0a13548a937e'},\n", + " {'val': 'sibling', 'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29'},\n", + " {'val': 'fin', 'id': '82dc42d5-799b-4fad-8fbd-b12f32c179d2'}]}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph = make_graph(ParentState, ChildState)\n", + "graph.invoke({\"name\": \"test\"}, debug=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see that that now the path values are no longer duplicated thanks to the updated reducer we introduced above." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/examples/subgraphs-manage-state.ipynb b/examples/subgraphs-manage-state.ipynb new file mode 100644 index 000000000..a018e6321 --- /dev/null +++ b/examples/subgraphs-manage-state.ipynb @@ -0,0 +1,868 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to manage state in subgraphs\n", + "\n", + "For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", + "\n", + "In this how-to guide we will cover how to manage the persisted state in subgraphs. This will enable a lot of the human-in-the-loop interaction patterns.\n", + "\n", + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define SubGraph\n", + "\n", + "First, let's set up our subgraph. For this, we will create a simple graph that can get the weather for a specific city. We will compile this graph with a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/) before the `weather_node`:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END, START, MessagesState\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "@tool\n", + "def get_weather(city: str):\n", + " \"\"\"Get the weather for a specific city\"\"\"\n", + " return f\"It's sunny in {city}!\"\n", + "\n", + "raw_model = ChatOpenAI()\n", + "model = raw_model.with_structured_output(get_weather)\n", + "\n", + "\n", + "class SubGraphState(MessagesState):\n", + " city: str\n", + "\n", + "\n", + "def model_node(state: SubGraphState):\n", + " result = model.invoke(state['messages'])\n", + " return {\"city\": result[\"city\"]}\n", + "\n", + "def weather_node(state: SubGraphState):\n", + " result = get_weather.invoke({\"city\": state['city']})\n", + " return {\"messages\": [{\"role\": \"assistant\", \"content\": result}]}\n", + "\n", + "\n", + "subgraph = StateGraph(SubGraphState)\n", + "subgraph.add_node(model_node)\n", + "subgraph.add_node(weather_node)\n", + "subgraph.add_edge(START, \"model_node\")\n", + "subgraph.add_edge(\"model_node\", \"weather_node\")\n", + "subgraph.add_edge(\"weather_node\", END)\n", + "subgraph = subgraph.compile(interrupt_before=[\"weather_node\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define Parent Graph\n", + "\n", + "We can now setup the overall graph. This graph will first route to the subgraph if it needs to get the weather, otherwise it will route to a normal LLM." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Literal\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "class RouterState(MessagesState):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "\n", + "class Router(TypedDict):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "router_model = raw_model.with_structured_output(Router)\n", + " \n", + "def router_node(state: RouterState):\n", + " system_message = \"Classify the incoming query as either about weather or not.\"\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " route = router_model.invoke(messages)\n", + " return {\"route\": route['route']}\n", + "\n", + "\n", + "def normal_llm_node(state: RouterState):\n", + " response = raw_model.invoke(state['messages'])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state['route'] == \"weather\":\n", + " return \"weather_graph\"\n", + " else:\n", + " return \"normal_llm_node\"\n", + "\n", + "\n", + "graph = StateGraph(RouterState)\n", + "graph.add_node(router_node)\n", + "graph.add_node(normal_llm_node)\n", + "graph.add_node(\"weather_graph\", subgraph)\n", + "graph.add_edge(START, \"router_node\")\n", + "graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "graph.add_edge(\"normal_llm_node\", END)\n", + "graph.add_edge(\"weather_graph\", END)\n", + "graph = graph.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test this out with a normal query to make sure it works as intended!" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'other'}}\n", + "{'normal_llm_node': {'messages': [AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 9, 'total_tokens': 18}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-9730e690-8cbd-4ba0-a962-3f8a4e848ef9-0', usage_metadata={'input_tokens': 9, 'output_tokens': 9, 'total_tokens': 18})]}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"hi!\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! We didn't ask about the weather, so we got a normal response from the LLM.\n", + "\n", + "## Resuming from breakpoints\n", + "\n", + "Let's now look at what happens with breakpoints. Let's invoke it with a query that should get routed to the weather subgraph where we have the interrupt node." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'weather'}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the graph stream doesn't include subgraph events. If we want to stream subgraph events, we can pass `subgraphs=True` and get back subgraph events like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')]})\n", + "((), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'route': 'weather'})\n", + "(('weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3',), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')]})\n", + "(('weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3',), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'city': 'San Francisco'})\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"values\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we get the state now, we can see that it's paused on `weather_graph`" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('weather_graph',)" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config)\n", + "state.next" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we look at the pending tasks for our current state, we can see that we have one task named `weather_graph`, which corresponds to the subgraph task." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(PregelTask(id='99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', name='weather_graph', error=None, interrupts=(), state={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3'}}),)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state.tasks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However since we got the state using the config of the parent graph, we don't have access to the subgraph state. If you look at the `state` value of the `PregelTask` above you will note that it is simply the configuration of the parent graph. If we want to actually populate the subgraph state, we can pass in `subgraphs=True` to `get_state` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PregelTask(id='99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', name='weather_graph', error=None, interrupts=(), state=StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'city': 'San Francisco'}, next=('weather_node',), config={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', 'checkpoint_id': '1ef6a48a-018f-638c-8001-a7af39dcd6ee', 'checkpoint_map': {'': '1ef6a489-fddc-6208-8001-5e02ff54dfba', 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3': '1ef6a48a-018f-638c-8001-a7af39dcd6ee'}}}, metadata={'source': 'loop', 'writes': {'model_node': {'city': 'San Francisco'}}, 'step': 1, 'parents': {'': '1ef6a489-fddc-6208-8001-5e02ff54dfba'}}, created_at='2024-09-03T23:02:42.795391+00:00', parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', 'checkpoint_id': '1ef6a489-fded-6936-8000-c96152586915'}}, tasks=(PregelTask(id='c153ac13-b9a5-543a-8044-3b3c852fd0bc', name='weather_node', error=None, interrupts=(), state=None),)))" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config, subgraphs=True)\n", + "state.tasks[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have access to the subgraph state! If you look at the `state` value of the `PregelTask` you can see that it has all the information we need, like the next node (`weather_node`) and the current state values (e.g. `city`).\n", + "\n", + "To resume execution, we can just invoke the outer graph as normal:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd'), AIMessage(content=\"It's sunny in San Francisco!\", id='07b513fa-30af-4ee4-83e4-2af8f6d133bd')]}}\n" + ] + } + ], + "source": [ + "for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Modifying state\n", + "\n", + "### Update the state of a subgraph\n", + "\n", + "What if we want to modify the state of a subgraph? We can do this similarly to how we [update the state of normal graphs](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/), just being careful to pass in the config of the subgraph to `update_state`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'weather'}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"4\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"what's the weather in sf\", id='35e331c6-eb47-483c-a63c-585877b12f5d')]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config, subgraphs=True)\n", + "state.values['messages']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to update the state of the **inner** graph, we need to pass the config for the **inner** graph, which we can get by accessing calling `state.tasks[0].state.config` - since we interrupted inside the subgraph, the state of the task is just the state of the subgraph." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '4',\n", + " 'checkpoint_ns': 'weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2',\n", + " 'checkpoint_id': '1ef6a424-2bb2-6ee0-8002-6a6ca5dbc91f',\n", + " 'checkpoint_map': {'': '1ef6a40d-0fca-671c-8001-3064b486db01',\n", + " 'weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2': '1ef6a424-2bb2-6ee0-8002-6a6ca5dbc91f'}}}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.update_state(state.tasks[0].state.config, {\"city\": \"la\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now resume streaming the outer graph (which will resume the subgraph!) and check that we updated our search to use LA instead of SF." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(('weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2',), {'weather_node': {'messages': [{'role': 'assistant', 'content': \"It's sunny in la!\"}]}})\n", + "((), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='35e331c6-eb47-483c-a63c-585877b12f5d'), AIMessage(content=\"It's sunny in la!\", id='c3d6b224-9642-4b21-94d5-eef8dc3f2cc9')]}})\n" + ] + } + ], + "source": [ + "for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fantastic! The AI responded with \"It's sunny in LA!\" as we expected.\n", + "\n", + "### Acting as a subgraph node\n", + "\n", + "Another way we could update the state is by acting as the `weather_node` ourselves instead of editing the state before `weather_node` is ran as we did above. We can do this by passing the subgraph config and also the `as_node` argument, which allows us to update the state as if we are the node we specify. Thus by setting an interrupt before the `weather_node` and then using the update state function as the `weather_node`, the graph itself never calls `weather_node` directly but instead we decide what the output of `weather_node` should be." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'route': 'weather'}})\n", + "(('weather_graph:bdb185a9-ff74-58dd-ae72-34e8665a33d7',), {'model_node': {'city': 'San Francisco'}})\n", + "interrupted!\n", + "((), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='5d721f30-278e-460f-a83c-fdb101731f3e'), AIMessage(content='rainy', id='43b30e0d-6ea0-4e9c-92de-3e411e6fa21d')]}})\n", + "[HumanMessage(content=\"what's the weather in sf\", id='5d721f30-278e-460f-a83c-fdb101731f3e'), AIMessage(content='rainy', id='43b30e0d-6ea0-4e9c-92de-3e411e6fa21d')]\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"14\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "# Graph execution should stop before the weather node\n", + "print(\"interrupted!\")\n", + "state = graph.get_state(config, subgraphs=True)\n", + "# We update the state by passing in the message we want returned from the weather node, and make sure to use as_node\n", + "graph.update_state(state.tasks[0].state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", + "for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "print(graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect! The AI responded with the message we passed in ourselves.\n", + "\n", + "### Acting as the entire subgraph\n", + "\n", + "Lastly, we could also update the graph just acting as the **entire** subgraph. This is similar to the case above but instead of acting as just the `weather_node` we are acting as the entire subgraph. This is done by passing in the normal graph config as well as the `as_node` argument, where we specify the we are acting as the entire subgraph node." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'route': 'weather'}})\n", + "(('weather_graph:53ab3fb1-23e8-5de0-acc6-9fb904fd4dc4',), {'model_node': {'city': 'San Francisco'}})\n", + "interrupted!\n", + "[HumanMessage(content=\"what's the weather in sf\", id='64b1b683-778b-4623-b783-4a8f81322ec8'), AIMessage(content='rainy', id='c1d1a2f3-c117-41e9-8c1f-8fb0a02a3b70')]\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"8\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "# Graph execution should stop before the weather node\n", + "print(\"interrupted!\")\n", + "# We update the state by passing in the message we want returned from the weather graph, making sure to use as_node\n", + "# Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n", + "graph.update_state(config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_graph\")\n", + "for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n", + " print(update)\n", + "print(graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, the AI responded with \"rainy\" as we expected.\n", + "\n", + "## Double nested subgraphs\n", + "\n", + "This same functionality continues to work no matter the level of nesting. Here is an example of doing the same things with a double nested subgraph (although any level of nesting will work). We add another router on top of our already defined graphs." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Literal\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "class RouterState(MessagesState):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "\n", + "class Router(TypedDict):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "router_model = raw_model.with_structured_output(Router)\n", + " \n", + "def router_node(state: RouterState):\n", + " system_message = \"Classify the incoming query as either about weather or not.\"\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " route = router_model.invoke(messages)\n", + " return {\"route\": route['route']}\n", + "\n", + "\n", + "def normal_llm_node(state: RouterState):\n", + " response = raw_model.invoke(state['messages'])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state['route'] == \"weather\":\n", + " return \"weather_graph\"\n", + " else:\n", + " return \"normal_llm_node\"\n", + "\n", + "\n", + "graph = StateGraph(RouterState)\n", + "graph.add_node(router_node)\n", + "graph.add_node(normal_llm_node)\n", + "graph.add_node(\"weather_graph\", subgraph)\n", + "graph.add_edge(START, \"router_node\")\n", + "graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "graph.add_edge(\"normal_llm_node\", END)\n", + "graph.add_edge(\"weather_graph\", END)\n", + "graph = graph.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "class GrandfatherState(MessagesState):\n", + " to_continue: bool\n", + " \n", + "def router_node(state: GrandfatherState):\n", + " # Dummy logic that will always continue\n", + " return {\"to_continue\": True}\n", + "\n", + "def route_after_prediction(state: GrandfatherState) -> Literal[\"graph\", \"__end__\"]:\n", + " if state['to_continue']:\n", + " return \"graph\"\n", + " else:\n", + " return \"__end__\"\n", + "\n", + "grandparent_graph = StateGraph(GrandfatherState)\n", + "grandparent_graph.add_node(router_node)\n", + "grandparent_graph.add_node(\"graph\", graph)\n", + "grandparent_graph.add_edge(START, \"router_node\")\n", + "grandparent_graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "grandparent_graph.add_edge(\"graph\", END)\n", + "grandparent_graph = grandparent_graph.compile(checkpointer=MemorySaver())" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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fEjMjweDLiRCeABzqrqr/FJfeAACGAgobukuH7Rj2RCdEFDd0lw/aMeyIeS3+JZcX7alRslOiFvHtA79sx7ZAmhC3j2gd+2Y9sgZlndrXhn0KdsL6AAOCVLtLfbf+9uv/It09pSbu7DG21dkfKNrjj+0xjPeF0GstHkVCUel7SjVx91vTMtVuxxyMK4ozREGmnMmySXe2icrP6hs0BHXF3PyfmrvqGK/A9o47zZv1SFguLufk/NXfUMV+B7Rx3mzfqkMse8Tw+6tzOAAH1EgAAAAAAAAAAAAAIy5+5qW80e9QxJiMufualvNHvUMYNS9RV/uu2D5o7+IdG7RpLqKv912wfNHfxDo3aMp/DAAACgAAAAAAAAAAAAABBWZ2ld8+rfxTonRXKW3ZSPS61STSGqdb7ryULoyUado4pZlnW48VGPFa59NrTaU0zVERMart804zGCo2XLGAgutc749a6CXvB1rnfHrXQS94V29p8qr9P8AIujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0Yp0BBda53x610EveDrXO+PWugl7wdvafKq/T/IujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0Yp0BBda53x610EveDrXO+PWugl7wdvafKq/T/IujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0YtOdSh2703/4gyPqtDoAc1dS1QSzs1pp2Es2wab9kEuGdIStdWq3lX63D6hvjrXO+PWugl7wyLauNUWUz+X+TLvqnQEF1rnfHrXQS94Otc749a6CXvDe3tPlVfp/k26MU6Agutc749a6CXvB1rnfHrXQS94O3tPlVfp/kXRinQEF1rnfHrXQS94Otc749a6CXvB29p8qr9P8i6MU6Agutc749a6CXvB1rnfHrXQS94O3tPlVfp/kXRinQEF1rnfHrXQS94Otc749a6CXvB29p8qr9P8AIujFOiChu6S4ftGPZEHWud8etdBL3hkQkM/Gv11RU1nLH6paVKUTRNkWqkkkRFk/mHKZtLW0s/8AHMRE3683wzG6ZxNUROtKiFvHtA79sx7ZAmhC3j2gd+2Y9sgdMs7ta8M+hTthfQABwS1lo8lYSs0vaUaSPtR6GlqR2OKRmnEmSJc1U5m2aT7+zTlB/WNmil2lvtv/AHt1/wCRbp7Sk3d2GNtq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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(grandparent_graph.get_graph(xray=2).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we run until the interrupt, we can now see that there are snapshots of the state of all three graphs" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'to_continue': True}})\n", + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8',), {'router_node': {'route': 'weather'}})\n", + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8', 'weather_graph:12bd3069-de24-5bc6-b4f1-f39527605781'), {'model_node': {'city': 'San Francisco'}})\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in grandparent_graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Grandparent State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'to_continue': True}\n", + "---------------\n", + "Parent Graph State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'route': 'weather'}\n", + "---------------\n", + "Subgraph State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'city': 'San Francisco'}\n" + ] + } + ], + "source": [ + "state = grandparent_graph.get_state(config, subgraphs=True)\n", + "print(\"Grandparent State:\")\n", + "print(state.values)\n", + "print(\"---------------\")\n", + "print(\"Parent Graph State:\")\n", + "print(state.tasks[0].state.values)\n", + "print(\"---------------\")\n", + "print(\"Subgraph State:\")\n", + "print(state.tasks[0].state.tasks[0].state.values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now continue, acting as the node three levels down" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8',), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]}})\n", + "((), {'graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]}})\n", + "[HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]\n" + ] + } + ], + "source": [ + "grandparent_graph_state = state\n", + "parent_graph_state = grandparent_graph_state.tasks[0].state\n", + "subgraph_state = parent_graph_state.tasks[0].state\n", + "grandparent_graph.update_state(subgraph_state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", + "for update in grandparent_graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "print(grandparent_graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As in the cases above, we can see that the AI responds with \"rainy\" as we expect.\n", + "\n", + "We can explore the state history to see how the state of the grandparent graph was updated at each step." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720'), AIMessage(content='rainy', id='7c80f847-248d-4b8f-8238-633ed757b353')], 'to_continue': True}, next=(), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f40-7a2c-6f9e-8002-a37a61b26709'}}, metadata={'source': 'loop', 'writes': {'graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720'), AIMessage(content='rainy', id='7c80f847-248d-4b8f-8238-633ed757b353')]}}, 'step': 2, 'parents': {}}, created_at='2024-08-30T17:19:35.793847+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f312-6338-8001-766acddc781e'}}, tasks=())\n", + "-----\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720')], 'to_continue': True}, next=('graph',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f312-6338-8001-766acddc781e'}}, metadata={'source': 'loop', 'writes': {'router_node': {'to_continue': True}}, 'step': 1, 'parents': {}}, created_at='2024-08-30T17:19:21.627097+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f303-61d0-8000-1945c8a74e9e'}}, tasks=(PregelTask(id='b59fe96f-fdce-5afe-aa58-bd2876a0d592', name='graph', error=None, interrupts=(), state={'configurable': {'thread_id': '2', 'checkpoint_ns': 'graph:b59fe96f-fdce-5afe-aa58-bd2876a0d592'}}),))\n", + "-----\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720')]}, next=('router_node',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f303-61d0-8000-1945c8a74e9e'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-08-30T17:19:21.620923+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f2f9-6d6a-bfff-c8b76e5b2462'}}, tasks=(PregelTask(id='e3d4a97a-f4ca-5260-801e-e65b02907825', name='router_node', error=None, interrupts=(), state=None),))\n", + "-----\n", + "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f2f9-6d6a-bfff-c8b76e5b2462'}}, metadata={'source': 'input', 'writes': {'messages': [{'role': 'user', 'content': \"what's the weather in sf\"}]}, 'step': -1, 'parents': {}}, created_at='2024-08-30T17:19:21.617127+00:00', parent_config=None, tasks=(PregelTask(id='f0538638-b794-58fc-a406-980d2fea28a1', name='__start__', error=None, interrupts=(), state=None),))\n", + "-----\n" + ] + } + ], + "source": [ + "for state in grandparent_graph.get_state_history(config):\n", + " print(state)\n", + " print(\"-----\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/time-travel.ipynb b/examples/time-travel.ipynb index dcdd035c0..2dd46e42b 100644 --- a/examples/time-travel.ipynb +++ b/examples/time-travel.ipynb @@ -46,7 +46,10 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_openai" + ] }, { "cell_type": "markdown", @@ -62,7 +65,18 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -78,7 +92,10 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -96,7 +113,22 @@ "id": "f5319e01", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "# `add_messages`` essentially does this\n", + "# (with more robust handling)\n", + "# def add_messages(left: list, right: list):\n", + "# return left + right\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] }, { "cell_type": "markdown", @@ -116,7 +148,19 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"] + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " return [\"The weather is cloudy with a chance of meatballs.\"]\n", + "\n", + "\n", + "tools = [search]" + ] }, { "cell_type": "markdown", @@ -133,7 +177,11 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] + "source": [ + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tool_node = ToolNode(tools)" + ] }, { "cell_type": "markdown", @@ -157,7 +205,11 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(temperature=0)" + ] }, { "cell_type": "markdown", @@ -175,7 +227,9 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": ["model = model.bind_tools(tools)"] + "source": [ + "model = model.bind_tools(tools)" + ] }, { "cell_type": "markdown", @@ -210,7 +264,20 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] + "source": [ + "from typing import Literal\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n", + " last_message = state[\"messages\"][-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"" + ] }, { "cell_type": "markdown", @@ -228,7 +295,50 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(State)\n", + "\n", + "\n", + "# Define the two nodes we will cycle between\n", + "def call_model(state: State) -> State:\n", + " return {\"messages\": model.invoke(state[\"messages\"])}\n", + "\n", + "\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")" + ] }, { "cell_type": "markdown", @@ -246,7 +356,11 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"] + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()" + ] }, { "cell_type": "code", @@ -254,7 +368,12 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"] + "source": [ + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] }, { "cell_type": "markdown", @@ -281,7 +400,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(app.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -312,7 +439,14 @@ ] } ], - "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"hi! I'm bob\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -350,7 +484,9 @@ "output_type": "execute_result" } ], - "source": ["app.get_state(config).values"] + "source": [ + "app.get_state(config).values" + ] }, { "cell_type": "markdown", @@ -379,7 +515,9 @@ "output_type": "execute_result" } ], - "source": ["app.get_state(config).next"] + "source": [ + "app.get_state(config).next" + ] }, { "cell_type": "markdown", @@ -426,7 +564,12 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -466,7 +609,9 @@ "id": "5a68afc0-606f-4294-a872-b2b563be0d69", "metadata": {}, "outputs": [], - "source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] + "source": [ + "app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" + ] }, { "cell_type": "code", @@ -490,7 +635,14 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"4\"}}\n", + "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", + "for event in app_w_interrupt.stream(\n", + " {\"messages\": [input_message]}, config, stream_mode=\"values\"\n", + "):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -526,7 +678,10 @@ "output_type": "execute_result" } ], - "source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"] + "source": [ + "current_values = app_w_interrupt.get_state(config)\n", + "current_values.next" + ] }, { "cell_type": "markdown", @@ -555,7 +710,9 @@ "output_type": "execute_result" } ], - "source": ["current_values.values[\"messages\"][-1].tool_calls"] + "source": [ + "current_values.values[\"messages\"][-1].tool_calls" + ] }, { "cell_type": "markdown", @@ -571,7 +728,11 @@ "id": "060e2e33-1f6a-40ef-850e-161b308986fb", "metadata": {}, "outputs": [], - "source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""] + "source": [ + "current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n", + " \"query\"\n", + "] = \"weather in San Francisco today\"" + ] }, { "cell_type": "code", @@ -591,7 +752,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.update_state(config, current_values.values)"] + "source": [ + "app_w_interrupt.update_state(config, current_values.values)" + ] }, { "cell_type": "markdown", @@ -629,7 +792,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.get_state(config).values"] + "source": [ + "app_w_interrupt.get_state(config).values" + ] }, { "cell_type": "code", @@ -648,7 +813,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.get_state(config).next"] + "source": [ + "app_w_interrupt.get_state(config).next" + ] }, { "cell_type": "markdown", @@ -679,7 +846,11 @@ ] } ], - "source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"] + "source": [ + "for event in app_w_interrupt.stream(None, config):\n", + " for v in event.values():\n", + " print(v)" + ] }, { "cell_type": "markdown", @@ -726,7 +897,13 @@ ] } ], - "source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"] + "source": [ + "for state in app_w_interrupt.get_state_history(config):\n", + " print(state)\n", + " print(\"--\")\n", + " if len(state.values[\"messages\"]) == 2:\n", + " to_replay = state" + ] }, { "cell_type": "markdown", @@ -754,7 +931,9 @@ "output_type": "execute_result" } ], - "source": ["to_replay.values"] + "source": [ + "to_replay.values" + ] }, { "cell_type": "code", @@ -773,7 +952,9 @@ "output_type": "execute_result" } ], - "source": ["to_replay.next"] + "source": [ + "to_replay.next" + ] }, { "cell_type": "markdown", @@ -806,7 +987,11 @@ ] } ], - "source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"] + "source": [ + "for event in app_w_interrupt.stream(None, to_replay.config):\n", + " for v in event.values():\n", + " print(v)" + ] }, { "cell_type": "markdown", @@ -834,7 +1019,18 @@ "id": "b084f141-5800-487b-b115-d2e58421b963", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"] + "source": [ + "from langchain_core.messages import AIMessage\n", + "\n", + "branch_config = app_w_interrupt.update_state(\n", + " to_replay.config,\n", + " {\n", + " \"messages\": [\n", + " AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n", + " ]\n", + " },\n", + ")" + ] }, { "cell_type": "code", @@ -842,7 +1038,9 @@ "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", "metadata": {}, "outputs": [], - "source": ["branch_state = app_w_interrupt.get_state(branch_config)"] + "source": [ + "branch_state = app_w_interrupt.get_state(branch_config)" + ] }, { "cell_type": "code", @@ -862,7 +1060,9 @@ "output_type": "execute_result" } ], - "source": ["branch_state.values"] + "source": [ + "branch_state.values" + ] }, { "cell_type": "code", @@ -881,7 +1081,9 @@ "output_type": "execute_result" } ], - "source": ["branch_state.next"] + "source": [ + "branch_state.next" + ] }, { "cell_type": "markdown", diff --git a/examples/tutorials/rag-agent-testing-local.ipynb b/examples/tutorials/rag-agent-testing-local.ipynb index f9e89e56d..3147d8677 100644 --- a/examples/tutorials/rag-agent-testing-local.ipynb +++ b/examples/tutorials/rag-agent-testing-local.ipynb @@ -169,9 +169,7 @@ "from langchain_core.output_parsers import JsonOutputParser\n", "\n", "# JSON\n", - "llm = ChatOllama(model=\"llama3.1\", \n", - " format=\"json\", \n", - " temperature=0)\n", + "llm = ChatOllama(model=\"llama3.1\", format=\"json\", temperature=0)\n", "\n", "\n", "prompt = PromptTemplate(\n", @@ -210,6 +208,7 @@ "from IPython.display import Image, display\n", "from langgraph.graph import START, END, StateGraph\n", "\n", + "\n", "class GraphState(TypedDict):\n", " \"\"\"\n", " Represents the state of our graph.\n", @@ -356,7 +355,7 @@ "workflow.add_node(\"web_search\", web_search) # web search\n", "\n", "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", + "workflow.add_edge(START, retrieve)\n", "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", "workflow.add_conditional_edges(\n", " \"grade_documents\",\n", @@ -381,21 +380,22 @@ "metadata": {}, "outputs": [], "source": [ - "import uuid \n", + "import uuid\n", + "\n", "\n", "def predict_custom_agent_answer(example: dict):\n", - " \n", " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", - " \n", + "\n", " state_dict = custom_graph.invoke(\n", " {\"question\": example[\"input\"], \"steps\": []}, config\n", " )\n", - " \n", + "\n", " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n", "\n", + "\n", "example = {\"input\": \"What are the types of agent memory?\"}\n", - "#response = predict_custom_agent_answer(example)\n", - "#response" + "# response = predict_custom_agent_answer(example)\n", + "# response" ] }, { @@ -544,6 +544,7 @@ " \"generate_answer\",\n", "]\n", "\n", + "\n", "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n", " \"\"\"\n", " Check if all expected tools are called in exact order and without any additional tool calls.\n", @@ -603,14 +604,6 @@ "\n", "![Screenshot 2024-07-23 at 12.39.27 PM.png](attachment:5e0fb308-080e-4621-ba20-55a655bb981e.png)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8eb46884-08aa-4ab4-8bb9-f72277c3b35b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/tutorials/rag-agent-testing.ipynb b/examples/tutorials/rag-agent-testing.ipynb index 5d83ae068..84c876241 100644 --- a/examples/tutorials/rag-agent-testing.ipynb +++ b/examples/tutorials/rag-agent-testing.ipynb @@ -1266,14 +1266,6 @@ "\n", "![Screenshot 2024-06-23 at 1.32.31 PM.png](attachment:953411a8-f352-4c8f-a923-d3ff171c6080.png)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bfa533b4-007f-4f86-b146-02c56a4e667a", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/tutorials/sql-agent.ipynb b/examples/tutorials/sql-agent.ipynb index fb1354ef4..3a7f077fb 100644 --- a/examples/tutorials/sql-agent.ipynb +++ b/examples/tutorials/sql-agent.ipynb @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 1, "id": "64b0bf1b14c2e902", "metadata": { "ExecuteTime": { @@ -170,7 +170,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "id": "1f1e1f4f86ed54", "metadata": { "ExecuteTime": { @@ -197,7 +197,7 @@ "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" ] }, - "execution_count": 20, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -228,7 +228,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 3, "id": "deae8460e4cf72b1", "metadata": { "ExecuteTime": { @@ -295,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 4, "id": "452d049a3d2a4406", "metadata": { "ExecuteTime": { @@ -360,7 +360,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 5, "id": "f7eb708ecb4c7cfc", "metadata": { "ExecuteTime": { @@ -416,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 6, "id": "293017e8f05ac2b3", "metadata": { "ExecuteTime": { @@ -432,10 +432,10 @@ { "data": { "text/plain": [ - "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5zdRt3uWwY23FSYmKZT7crGF', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 222, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_319be4768e', 'finish_reason': 'stop', 'logprobs': None}, id='run-a062c91e-084e-4a91-bba8-fdbb957e2a5c-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_5zdRt3uWwY23FSYmKZT7crGF'}], usage_metadata={'input_tokens': 222, 'output_tokens': 20, 'total_tokens': 242})" + "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 221, 'total_tokens': 241}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-dd7873ef-d2f7-4769-a5c0-e6776ec2c515-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 221, 'output_tokens': 20, 'total_tokens': 241})" ] }, - "execution_count": 24, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -485,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 7, "id": "90d04ceea7b6b010", "metadata": { "ExecuteTime": { @@ -675,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 14, "id": "4f200d1813897000", "metadata": { "ExecuteTime": { @@ -690,7 +690,7 @@ "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -727,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 15, "id": "85958809-03c5-4e52-97cc-e7c0ae986f60", "metadata": {}, "outputs": [ @@ -737,29 +737,42 @@ "'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'" ] }, - "execution_count": 53, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import json\n", - "\n", "messages = app.invoke(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", ")\n", - "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - "]\n", - "json.loads(json_str)[\"final_answer\"]" + "json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"][\"final_answer\"]\n", + "json_str" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "3bf7709f-500c-4f28-bb85-dda317286c63", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'first_tool_call': {'messages': [AIMessage(content='', tool_calls=[{'name': 'sql_db_list_tables', 'args': {}, 'id': 'tool_abcd123', 'type': 'tool_call'}])]}}\n", + "{'list_tables_tool': {'messages': [ToolMessage(content='Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track', name='sql_db_list_tables', tool_call_id='tool_abcd123')]}}\n", + "{'model_get_schema': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'function': {'arguments': '{\"table_names\":\"Employee, Invoice\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 177, 'total_tokens': 195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c91a5aad-fc05-4881-87f9-0662d703c3c8-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Employee, Invoice'}, 'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'type': 'tool_call'}], usage_metadata={'input_tokens': 177, 'output_tokens': 18, 'total_tokens': 195})]}}\n", + "{'get_schema_tool': {'messages': [ToolMessage(content='\\nCREATE TABLE \"Employee\" (\\n\\t\"EmployeeId\" INTEGER NOT NULL, \\n\\t\"LastName\" NVARCHAR(20) NOT NULL, \\n\\t\"FirstName\" NVARCHAR(20) NOT NULL, \\n\\t\"Title\" NVARCHAR(30), \\n\\t\"ReportsTo\" INTEGER, \\n\\t\"BirthDate\" DATETIME, \\n\\t\"HireDate\" DATETIME, \\n\\t\"Address\" NVARCHAR(70), \\n\\t\"City\" NVARCHAR(40), \\n\\t\"State\" NVARCHAR(40), \\n\\t\"Country\" NVARCHAR(40), \\n\\t\"PostalCode\" NVARCHAR(10), \\n\\t\"Phone\" NVARCHAR(24), \\n\\t\"Fax\" NVARCHAR(24), \\n\\t\"Email\" NVARCHAR(60), \\n\\tPRIMARY KEY (\"EmployeeId\"), \\n\\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\\n)\\n\\n/*\\n3 rows from Employee table:\\nEmployeeId\\tLastName\\tFirstName\\tTitle\\tReportsTo\\tBirthDate\\tHireDate\\tAddress\\tCity\\tState\\tCountry\\tPostalCode\\tPhone\\tFax\\tEmail\\n1\\tAdams\\tAndrew\\tGeneral Manager\\tNone\\t1962-02-18 00:00:00\\t2002-08-14 00:00:00\\t11120 Jasper Ave NW\\tEdmonton\\tAB\\tCanada\\tT5K 2N1\\t+1 (780) 428-9482\\t+1 (780) 428-3457\\tandrew@chinookcorp.com\\n2\\tEdwards\\tNancy\\tSales Manager\\t1\\t1958-12-08 00:00:00\\t2002-05-01 00:00:00\\t825 8 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 2T3\\t+1 (403) 262-3443\\t+1 (403) 262-3322\\tnancy@chinookcorp.com\\n3\\tPeacock\\tJane\\tSales Support Agent\\t2\\t1973-08-29 00:00:00\\t2002-04-01 00:00:00\\t1111 6 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 5M5\\t+1 (403) 262-3443\\t+1 (403) 262-6712\\tjane@chinookcorp.com\\n*/\\n\\n\\nCREATE TABLE \"Invoice\" (\\n\\t\"InvoiceId\" INTEGER NOT NULL, \\n\\t\"CustomerId\" INTEGER NOT NULL, \\n\\t\"InvoiceDate\" DATETIME NOT NULL, \\n\\t\"BillingAddress\" NVARCHAR(70), \\n\\t\"BillingCity\" NVARCHAR(40), \\n\\t\"BillingState\" NVARCHAR(40), \\n\\t\"BillingCountry\" NVARCHAR(40), \\n\\t\"BillingPostalCode\" NVARCHAR(10), \\n\\t\"Total\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\"InvoiceId\"), \\n\\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\\n)\\n\\n/*\\n3 rows from Invoice table:\\nInvoiceId\\tCustomerId\\tInvoiceDate\\tBillingAddress\\tBillingCity\\tBillingState\\tBillingCountry\\tBillingPostalCode\\tTotal\\n1\\t2\\t2009-01-01 00:00:00\\tTheodor-Heuss-Straße 34\\tStuttgart\\tNone\\tGermany\\t70174\\t1.98\\n2\\t4\\t2009-01-02 00:00:00\\tUllevålsveien 14\\tOslo\\tNone\\tNorway\\t0171\\t3.96\\n3\\t8\\t2009-01-03 00:00:00\\tGrétrystraat 63\\tBrussels\\tNone\\tBelgium\\t1000\\t5.94\\n*/', name='sql_db_schema', tool_call_id='call_z1tyC7cEAawi5oIQn731Uknp')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'function': {'arguments': '{\"table_names\":\"Customer\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 1179, 'total_tokens': 1195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-19e02169-5e1e-40d9-90a2-384336ca5069-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Customer'}, 'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1179, 'output_tokens': 16, 'total_tokens': 1195}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='de5d25f5-b891-4e47-8282-d04dc9b93e9e', tool_call_id='call_ErWLktUfxKsHGNGr74m72yYD')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'function': {'arguments': '{\"table_names\":[\"Customer\"]}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 1245, 'total_tokens': 1262}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2c5f800f-43dc-4224-847b-49b5079efd2a-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': ['Customer']}, 'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1245, 'output_tokens': 17, 'total_tokens': 1262}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='6c962a35-fc24-4f27-86f0-6ec05256d478', tool_call_id='call_TFaA52SbhgEqm3ElEAd4HCsn')]}}\n", + "{'query_gen': {'messages': [AIMessage(content=\"To determine which sales agent made the most in sales in 2009, we need to join the `Invoice`, `Customer`, and `Employee` tables. Here is the query to find the top sales agent:\\n\\n```sql\\nSELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\\n```\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 125, 'prompt_tokens': 1312, 'total_tokens': 1437}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3aa7262c27', 'finish_reason': 'stop', 'logprobs': None}, id='run-6cacd10d-d3aa-49ae-b9d7-8cc209fc4ccc-0', usage_metadata={'input_tokens': 1312, 'output_tokens': 125, 'total_tokens': 1437})]}}\n", + "{'correct_query': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'function': {'arguments': '{\"query\":\"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\\\nFROM Invoice i\\\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\\\nWHERE strftime(\\'%Y\\', i.InvoiceDate) = \\'2009\\'\\\\nGROUP BY e.EmployeeId\\\\nORDER BY TotalSales DESC\\\\nLIMIT 1;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 90, 'prompt_tokens': 337, 'total_tokens': 427}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-71067e75-80f6-4356-8239-518e466b3526-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': \"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\"}, 'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'type': 'tool_call'}], usage_metadata={'input_tokens': 337, 'output_tokens': 90, 'total_tokens': 427})]}}\n", + "{'execute_query': {'messages': [ToolMessage(content=\"[('Steve', 'Johnson', 164.34)]\", name='db_query_tool', tool_call_id='call_FwCE2c7WORU7lKHdSWqMv0ON')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'function': {'arguments': '{\"final_answer\":\"The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.\"}', 'name': 'SubmitFinalAnswer'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 41, 'prompt_tokens': 1553, 'total_tokens': 1594}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_cb7cc8e106', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2ec7bf3a-2a16-47bd-aa9c-b7d6dc531c1b-0', tool_calls=[{'name': 'SubmitFinalAnswer', 'args': {'final_answer': 'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'}, 'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1553, 'output_tokens': 41, 'total_tokens': 1594})]}}\n" + ] + } + ], "source": [ "for event in app.stream(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", @@ -806,10 +819,8 @@ " \"\"\"Use this for answer evaluation\"\"\"\n", " msg = {\"messages\": (\"user\", example[\"input\"])}\n", " messages = app.invoke(msg)\n", - " json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - " ]\n", - " response = json.loads(json_str)[\"final_answer\"]\n", + " json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"]\n", + " response = json_str[\"final_answer\"]\n", " return {\"response\": response}" ] }, @@ -1053,14 +1064,6 @@ "\n", "We will explore ways to resolve this using LangGraph in future cookbooks!" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0681b6e0-196e-440c-ab16-1a530411719e", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1079,7 +1082,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/tutorials/tool-calling-agent-local.ipynb b/examples/tutorials/tool-calling-agent-local.ipynb index 5c60336e8..f7d87a8a9 100644 --- a/examples/tutorials/tool-calling-agent-local.ipynb +++ b/examples/tutorials/tool-calling-agent-local.ipynb @@ -134,6 +134,7 @@ " for d in web_results\n", " ]\n", "\n", + "\n", "# Tool list\n", "tools = [retrieve_documents, web_search]" ] @@ -152,9 +153,11 @@ "from langgraph.graph.message import AnyMessage, add_messages\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list[AnyMessage], add_messages]\n", "\n", + "\n", "class Assistant:\n", " def __init__(self, runnable: Runnable):\n", " \"\"\"\n", @@ -291,6 +294,7 @@ "source": [ "import uuid\n", "\n", + "\n", "def predict_react_agent_answer(example: dict):\n", " \"\"\"Use this for answer evaluation\"\"\"\n", "\n", @@ -333,14 +337,6 @@ "\n", "https://smith.langchain.com/public/7a4938e3-f94f-4e04-a162-bf592fba4643/r" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74b813cb-18ed-42d8-b313-6ee56ded4bcc", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/usaco/usaco.ipynb b/examples/usaco/usaco.ipynb index 0a4d4e445..16f698fce 100644 --- a/examples/usaco/usaco.ipynb +++ b/examples/usaco/usaco.ipynb @@ -43,7 +43,10 @@ "id": "c686827a-8078-4fd4-af7a-638ca1362796", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U langgraph langsmith langchain_anthropic datasets langchain langchainhub"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph langsmith langchain_anthropic datasets langchain langchainhub" + ] }, { "cell_type": "code", @@ -51,7 +54,21 @@ "id": "e2e542bb-a99e-44d3-8ebb-6a952dcbf2bf", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _get_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_get_env(\"ANTHROPIC_API_KEY\")\n# Recommended\n_get_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _get_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_get_env(\"ANTHROPIC_API_KEY\")\n", + "# Recommended\n", + "_get_env(\"LANGCHAIN_API_KEY\")\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] }, { "cell_type": "markdown", @@ -69,7 +86,28 @@ "id": "f7a0c7bd-512d-4e5b-ab43-1bc3b8c97fd4", "metadata": {}, "outputs": [], - "source": ["import os\nimport zipfile\n\nimport datasets\nimport requests\n\nusaco_url = \"https://storage.googleapis.com/benchmarks-artifacts/usaco/usaco_sampled_with_tests.zip\"\nzip_path = \"usaco.zip\"\nextract_path = \"usaco_datasets\"\n\nresponse = requests.get(usaco_url)\nwith open(zip_path, \"wb\") as file:\n file.write(response.content)\n\nwith zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n zip_ref.extractall(extract_path)\n\nos.remove(zip_path)\n\nds = datasets.load_from_disk(os.path.join(extract_path, \"usaco_v3_sampled_with_tests\"))"] + "source": [ + "import os\n", + "import zipfile\n", + "\n", + "import datasets\n", + "import requests\n", + "\n", + "usaco_url = \"https://storage.googleapis.com/benchmarks-artifacts/usaco/usaco_sampled_with_tests.zip\"\n", + "zip_path = \"usaco.zip\"\n", + "extract_path = \"usaco_datasets\"\n", + "\n", + "response = requests.get(usaco_url)\n", + "with open(zip_path, \"wb\") as file:\n", + " file.write(response.content)\n", + "\n", + "with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n", + " zip_ref.extractall(extract_path)\n", + "\n", + "os.remove(zip_path)\n", + "\n", + "ds = datasets.load_from_disk(os.path.join(extract_path, \"usaco_v3_sampled_with_tests\"))" + ] }, { "cell_type": "markdown", @@ -88,7 +126,72 @@ "id": "54f9d037-121e-412f-857a-3e0ccc73892e", "metadata": {}, "outputs": [], - "source": ["import multiprocessing\nimport queue\nimport subprocess\nimport sys\nimport time\nimport traceback\n\nmultiprocessing.set_start_method(\"fork\", force=True)\n# WARNING\n# This program exists to execute untrusted model-generated code. Although\n# it is highly unlikely that model-generated code will do something overtly\n# malicious in response to this test suite, model-generated code may act\n# destructively due to a lack of model capability or alignment.\n# Users are strongly encouraged to sandbox this evaluation suite so that it\n# does not perform destructive actions on their host or network.\n# Proceed at your own risk:\n\n\ndef exec_program(q, program, input_data, expected_output, timeout):\n try:\n start_time = time.time()\n process = subprocess.Popen(\n [sys.executable, \"-c\", program],\n stdin=subprocess.PIPE,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n )\n stdout, stderr = process.communicate(input=input_data, timeout=timeout)\n if time.time() - start_time > timeout:\n raise TimeoutError(\"Execution timed out.\")\n if process.returncode != 0:\n q.put(f\"failed: {stderr}\")\n else:\n if stdout.strip() == expected_output.strip():\n q.put(\"passed\")\n else:\n q.put(f\"wrong answer. Expected '{expected_output}', got '{stdout}'\")\n except subprocess.TimeoutExpired:\n process.kill()\n q.put(\"timed out\")\n except Exception:\n q.put(f\"failed: {traceback.format_exc()}\")\n\n\ndef check_correctness(\n program: str, input_data: str, expected_output: str, timeout: float\n) -> str:\n q = multiprocessing.Queue()\n process = multiprocessing.Process(\n target=exec_program, args=(q, program, input_data, expected_output, timeout)\n )\n process.start()\n process.join(timeout=timeout + 1)\n if process.is_alive():\n process.terminate()\n process.join()\n result = \"timed out\"\n else:\n try:\n result = q.get_nowait()\n except queue.Empty:\n result = \"no result returned\"\n return result"] + "source": [ + "import multiprocessing\n", + "import queue\n", + "import subprocess\n", + "import sys\n", + "import time\n", + "import traceback\n", + "\n", + "multiprocessing.set_start_method(\"fork\", force=True)\n", + "# WARNING\n", + "# This program exists to execute untrusted model-generated code. Although\n", + "# it is highly unlikely that model-generated code will do something overtly\n", + "# malicious in response to this test suite, model-generated code may act\n", + "# destructively due to a lack of model capability or alignment.\n", + "# Users are strongly encouraged to sandbox this evaluation suite so that it\n", + "# does not perform destructive actions on their host or network.\n", + "# Proceed at your own risk:\n", + "\n", + "\n", + "def exec_program(q, program, input_data, expected_output, timeout):\n", + " try:\n", + " start_time = time.time()\n", + " process = subprocess.Popen(\n", + " [sys.executable, \"-c\", program],\n", + " stdin=subprocess.PIPE,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.PIPE,\n", + " text=True,\n", + " )\n", + " stdout, stderr = process.communicate(input=input_data, timeout=timeout)\n", + " if time.time() - start_time > timeout:\n", + " raise TimeoutError(\"Execution timed out.\")\n", + " if process.returncode != 0:\n", + " q.put(f\"failed: {stderr}\")\n", + " else:\n", + " if stdout.strip() == expected_output.strip():\n", + " q.put(\"passed\")\n", + " else:\n", + " q.put(f\"wrong answer. Expected '{expected_output}', got '{stdout}'\")\n", + " except subprocess.TimeoutExpired:\n", + " process.kill()\n", + " q.put(\"timed out\")\n", + " except Exception:\n", + " q.put(f\"failed: {traceback.format_exc()}\")\n", + "\n", + "\n", + "def check_correctness(\n", + " program: str, input_data: str, expected_output: str, timeout: float\n", + ") -> str:\n", + " q = multiprocessing.Queue()\n", + " process = multiprocessing.Process(\n", + " target=exec_program, args=(q, program, input_data, expected_output, timeout)\n", + " )\n", + " process.start()\n", + " process.join(timeout=timeout + 1)\n", + " if process.is_alive():\n", + " process.terminate()\n", + " process.join()\n", + " result = \"timed out\"\n", + " else:\n", + " try:\n", + " result = q.get_nowait()\n", + " except queue.Empty:\n", + " result = \"no result returned\"\n", + " return result" + ] }, { "cell_type": "markdown", @@ -114,7 +217,17 @@ ] } ], - "source": ["program_code = \"print('hello, world!')\"\ninput_data = \"\"\nexpected_output = \"hello, world!\"\ntimeout = 2\n\ntest_result = check_correctness(program_code, input_data, expected_output, timeout)\nprint(\"Example 1: \", test_result)\ntest_result = check_correctness(\"print('goodbye')\", input_data, \"hi there\", timeout)\nprint(\"Example 2: \", test_result)"] + "source": [ + "program_code = \"print('hello, world!')\"\n", + "input_data = \"\"\n", + "expected_output = \"hello, world!\"\n", + "timeout = 2\n", + "\n", + "test_result = check_correctness(program_code, input_data, expected_output, timeout)\n", + "print(\"Example 1: \", test_result)\n", + "test_result = check_correctness(\"print('goodbye')\", input_data, \"hi there\", timeout)\n", + "print(\"Example 2: \", test_result)" + ] }, { "cell_type": "markdown", @@ -152,7 +265,27 @@ "id": "f43d68d9-10be-4544-879a-88a33db18bea", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # Append-only chat memory so the agent can try to recover from initial mistakes.\n messages: Annotated[list[AnyMessage], add_messages]\n # From the dataset. These are used for testing.\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import AnyMessage, add_messages\n", + "\n", + "\n", + "class TestCase(TypedDict):\n", + " inputs: str\n", + " outputs: str\n", + "\n", + "\n", + "class State(TypedDict):\n", + " # Append-only chat memory so the agent can try to recover from initial mistakes.\n", + " messages: Annotated[list[AnyMessage], add_messages]\n", + " # From the dataset. These are used for testing.\n", + " test_cases: list[TestCase]\n", + " runtime_limit: int\n", + " status: str" + ] }, { "cell_type": "markdown", @@ -168,7 +301,18 @@ "id": "6d56776f-993b-4ca7-89ef-21dec01dc9d3", "metadata": {}, "outputs": [], - "source": ["input_states = [\n {\n \"messages\": [(\"user\", row[\"description\"])],\n \"test_cases\": row[\"test_cases\"],\n \"runtime_limit\": row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n \"problem_level\": row[\"problem_level\"],\n }\n for row in ds\n]"] + "source": [ + "input_states = [\n", + " {\n", + " \"messages\": [(\"user\", row[\"description\"])],\n", + " \"test_cases\": row[\"test_cases\"],\n", + " \"runtime_limit\": row[\"runtime_limit\"],\n", + " \"status\": \"in_progress\",\n", + " \"problem_level\": row[\"problem_level\"],\n", + " }\n", + " for row in ds\n", + "]" + ] }, { "cell_type": "markdown", @@ -186,7 +330,28 @@ "id": "7b9e7742-16a3-4ad2-bc63-5f9cd4fd734b", "metadata": {}, "outputs": [], - "source": ["from langchain_core.language_models import BaseChatModel\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass writePython(BaseModel):\n \"\"\"Write python code that resolves the problem.\"\"\"\n\n reasoning: str = Field(..., description=\"Conceptual solution.\")\n pseudocode: str = Field(..., description=\"Detailed English pseudocode.\")\n code: str = Field(..., description=\"Valid Python 3 solution to the problem\")\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n return {\"messages\": [self.runnable.invoke({\"messages\": state[\"messages\"]})]}"] + "source": [ + "from langchain_core.language_models import BaseChatModel\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "\n", + "class writePython(BaseModel):\n", + " \"\"\"Write python code that resolves the problem.\"\"\"\n", + "\n", + " reasoning: str = Field(..., description=\"Conceptual solution.\")\n", + " pseudocode: str = Field(..., description=\"Detailed English pseudocode.\")\n", + " code: str = Field(..., description=\"Valid Python 3 solution to the problem\")\n", + "\n", + "\n", + "class Solver:\n", + " def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n", + " self.runnable = prompt | llm.bind_tools([writePython])\n", + "\n", + " def __call__(self, state: State) -> dict:\n", + " # Our agent only can see the \"messages\" and will ignore the test info\n", + " return {\"messages\": [self.runnable.invoke({\"messages\": state[\"messages\"]})]}" + ] }, { "cell_type": "markdown", @@ -231,7 +396,22 @@ ] } ], - "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n# For this section, we are testing zero-shot performance and won't have\n# any examples. Partial them out to pre-fill the template.\nprompt = hub.pull(\"wfh/usaco-draft-solver\").partial(examples=\"\")\nprint(\"*\" * 35 + \"Prompt\" + \"*\" * 35)\nprompt.pretty_print()\n\n# Use Haiku if you want to save $$ while (almost) never correctly answering the question\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\nsolver = Solver(llm, prompt)"] + "source": [ + "from langchain import hub\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "# For this section, we are testing zero-shot performance and won't have\n", + "# any examples. Partial them out to pre-fill the template.\n", + "prompt = hub.pull(\"wfh/usaco-draft-solver\").partial(examples=\"\")\n", + "print(\"*\" * 35 + \"Prompt\" + \"*\" * 35)\n", + "prompt.pretty_print()\n", + "\n", + "# Use Haiku if you want to save $$ while (almost) never correctly answering the question\n", + "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", + "llm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", + "\n", + "solver = Solver(llm, prompt)" + ] }, { "cell_type": "code", @@ -250,7 +430,25 @@ ] } ], - "source": ["print(\"*\" * 34 + \" Example \" + \"*\" * 34)\nresult = solver(\n {\n \"messages\": [\n (\n \"user\",\n \"How do I get a perfectly random sample from an infinite stream\",\n )\n ]\n }\n)\nresult[\"messages\"][0].pretty_print()\n# Could expand to include (1)\n# 1. Restate the problem in plain English\n# 2. Closely following the explanation, restate and explain the solution in plain English\n# 3. Write a pseudocode solution\n# 4. Output the final Python solution with your solution steps in comments."] + "source": [ + "print(\"*\" * 34 + \" Example \" + \"*\" * 34)\n", + "result = solver(\n", + " {\n", + " \"messages\": [\n", + " (\n", + " \"user\",\n", + " \"How do I get a perfectly random sample from an infinite stream\",\n", + " )\n", + " ]\n", + " }\n", + ")\n", + "result[\"messages\"][0].pretty_print()\n", + "# Could expand to include (1)\n", + "# 1. Restate the problem in plain English\n", + "# 2. Closely following the explanation, restate and explain the solution in plain English\n", + "# 3. Write a pseudocode solution\n", + "# 4. Output the final Python solution with your solution steps in comments." + ] }, { "cell_type": "markdown", @@ -269,7 +467,57 @@ "id": "1785015b-24f8-415f-b950-e229b5137887", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n\n\n# This is the node we will add to the graph.\n# Most tool-calling APIs require that the `ToolMessage` contain the ID\n# of the\ndef format_tool_message(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response + \"\\nMake all fixes using the writePython tool.\",\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef evaluate(state: State):\n test_cases = state[\"test_cases\"]\n ai_message: AIMessage = state[\"messages\"][-1]\n if not ai_message.tool_calls:\n return {\n \"messages\": [\n HumanMessage(\n content=\"No code submitted. Please try again using the correct python code.\"\n )\n ]\n }\n try:\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n except Exception as e:\n return {\"messages\": [format_tool_message(repr(e), ai_message)]}\n num_test_cases = len(test_cases)\n succeeded = 0\n test_results = []\n # TODO: Multiprocess\n for test_case in test_cases:\n input_data = test_case[\"inputs\"]\n expected_output = test_case[\"outputs\"]\n test_result = check_correctness(code, input_data, expected_output, timeout)\n test_results.append(test_result)\n if test_result == \"passed\":\n succeeded += 1\n pass_rate = succeeded / num_test_cases if num_test_cases else \"N/A\"\n if pass_rate == 1:\n return {\"status\": \"success\"}\n\n responses = \"\\n\".join(\n [f\"\\n{r}\\n\" for i, r in enumerate(test_results)]\n )\n response = f\"Incorrect submission. Please respond with updated code.\\nPass rate: {succeeded}/{num_test_cases}\\nResults:\\n{responses}\"\n formatted_message = format_tool_message(response, ai_message)\n return {\"messages\": [formatted_message]}"] + "source": [ + "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", + "\n", + "\n", + "# This is the node we will add to the graph.\n", + "# Most tool-calling APIs require that the `ToolMessage` contain the ID\n", + "# of the\n", + "def format_tool_message(response: str, ai_message: AIMessage):\n", + " return ToolMessage(\n", + " content=response + \"\\nMake all fixes using the writePython tool.\",\n", + " tool_call_id=ai_message.tool_calls[0][\"id\"],\n", + " )\n", + "\n", + "\n", + "def evaluate(state: State):\n", + " test_cases = state[\"test_cases\"]\n", + " ai_message: AIMessage = state[\"messages\"][-1]\n", + " if not ai_message.tool_calls:\n", + " return {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"No code submitted. Please try again using the correct python code.\"\n", + " )\n", + " ]\n", + " }\n", + " try:\n", + " code = ai_message.tool_calls[0][\"args\"][\"code\"]\n", + " except Exception as e:\n", + " return {\"messages\": [format_tool_message(repr(e), ai_message)]}\n", + " num_test_cases = len(test_cases)\n", + " succeeded = 0\n", + " test_results = []\n", + " # TODO: Multiprocess\n", + " for test_case in test_cases:\n", + " input_data = test_case[\"inputs\"]\n", + " expected_output = test_case[\"outputs\"]\n", + " test_result = check_correctness(code, input_data, expected_output, timeout)\n", + " test_results.append(test_result)\n", + " if test_result == \"passed\":\n", + " succeeded += 1\n", + " pass_rate = succeeded / num_test_cases if num_test_cases else \"N/A\"\n", + " if pass_rate == 1:\n", + " return {\"status\": \"success\"}\n", + "\n", + " responses = \"\\n\".join(\n", + " [f\"\\n{r}\\n\" for i, r in enumerate(test_results)]\n", + " )\n", + " response = f\"Incorrect submission. Please respond with updated code.\\nPass rate: {succeeded}/{num_test_cases}\\nResults:\\n{responses}\"\n", + " formatted_message = format_tool_message(response, ai_message)\n", + " return {\"messages\": [formatted_message]}" + ] }, { "cell_type": "markdown", @@ -295,7 +543,25 @@ "id": "caf1560e-1517-4229-8a43-186816da6a3a", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"solver\", solver)\nbuilder.add_edge(START, \"solver\")\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"solver\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solver\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solver\": \"solver\"})\ngraph = builder.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"solver\", solver)\n", + "builder.add_edge(START, \"solver\")\n", + "builder.add_node(\"evaluate\", evaluate)\n", + "builder.add_edge(\"solver\", \"evaluate\")\n", + "\n", + "\n", + "def control_edge(state: State):\n", + " if state.get(\"status\") == \"success\":\n", + " return END\n", + " return \"solver\"\n", + "\n", + "\n", + "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solver\": \"solver\"})\n", + "graph = builder.compile()" + ] }, { "cell_type": "code", @@ -314,7 +580,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -393,7 +667,12 @@ ] } ], - "source": ["input_state = input_states[0].copy()\n# We will reduce the test cases to speed this notebook up\ninput_state[\"test_cases\"] = input_state[\"test_cases\"][:3]\nprint(input_state[\"messages\"][0][1])"] + "source": [ + "input_state = input_states[0].copy()\n", + "# We will reduce the test cases to speed this notebook up\n", + "input_state[\"test_cases\"] = input_state[\"test_cases\"][:3]\n", + "print(input_state[\"messages\"][0][1])" + ] }, { "cell_type": "markdown", @@ -455,7 +734,33 @@ ] } ], - "source": ["from langchain_core.tracers.context import tracing_v2_enabled\nfrom langsmith import Client\n\n\n# We don't need to include all the test cases in our traces.\ndef _hide_test_cases(inputs):\n copied = inputs.copy()\n # These are tens of MB in size. No need to send them up\n copied[\"test_cases\"] = \"...\"\n return copied\n\n\nclient = Client(hide_inputs=_hide_test_cases, hide_outputs=_hide_test_cases)\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )"] + "source": [ + "from langchain_core.tracers.context import tracing_v2_enabled\n", + "from langsmith import Client\n", + "\n", + "\n", + "# We don't need to include all the test cases in our traces.\n", + "def _hide_test_cases(inputs):\n", + " copied = inputs.copy()\n", + " # These are tens of MB in size. No need to send them up\n", + " copied[\"test_cases\"] = \"...\"\n", + " return copied\n", + "\n", + "\n", + "client = Client(hide_inputs=_hide_test_cases, hide_outputs=_hide_test_cases)\n", + "with tracing_v2_enabled(client=client):\n", + " events = graph.stream(input_state)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )" + ] }, { "cell_type": "markdown", @@ -501,7 +806,10 @@ "id": "d612dd8d-31af-426c-944b-203acd55ace0", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --upgrade --quiet rank_bm25"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --upgrade --quiet rank_bm25" + ] }, { "cell_type": "markdown", @@ -519,7 +827,29 @@ "id": "16937fef-58b9-4ab2-bbfc-5237aad235ec", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # NEW! Candidate for retrieval + formatted fetched examples as \"memory\"\n candidate: AIMessage\n examples: str\n # Repeated from Part 1\n messages: Annotated[list[AnyMessage], add_messages]\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import AnyMessage, add_messages\n", + "\n", + "\n", + "class TestCase(TypedDict):\n", + " inputs: str\n", + " outputs: str\n", + "\n", + "\n", + "class State(TypedDict):\n", + " # NEW! Candidate for retrieval + formatted fetched examples as \"memory\"\n", + " candidate: AIMessage\n", + " examples: str\n", + " # Repeated from Part 1\n", + " messages: Annotated[list[AnyMessage], add_messages]\n", + " test_cases: list[TestCase]\n", + " runtime_limit: int\n", + " status: str" + ] }, { "cell_type": "markdown", @@ -537,7 +867,40 @@ "id": "25f947a7-15bb-4119-a47e-b5c33ca0a249", "metadata": {}, "outputs": [], - "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n inputs = {\"messages\": state[\"messages\"]}\n has_examples = bool(state.get(\"examples\"))\n output_key = \"candidate\" # Used in the draft node\n if has_examples:\n output_key = \"messages\"\n # Used in the solve node\n inputs[\"examples\"] = state[\"examples\"]\n response = self.runnable.invoke(inputs)\n if not response.content:\n return {\n output_key: AIMessage(\n content=\"I'll need to think about this step by step.\"\n )\n }\n return {output_key: response}\n\n\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nsolver = Solver(llm, prompt)"] + "source": [ + "from langchain import hub\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "\n", + "class Solver:\n", + " def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n", + " self.runnable = prompt | llm.bind_tools([writePython])\n", + "\n", + " def __call__(self, state: State) -> dict:\n", + " # Our agent only can see the \"messages\" and will ignore the test info\n", + " inputs = {\"messages\": state[\"messages\"]}\n", + " has_examples = bool(state.get(\"examples\"))\n", + " output_key = \"candidate\" # Used in the draft node\n", + " if has_examples:\n", + " output_key = \"messages\"\n", + " # Used in the solve node\n", + " inputs[\"examples\"] = state[\"examples\"]\n", + " response = self.runnable.invoke(inputs)\n", + " if not response.content:\n", + " return {\n", + " output_key: AIMessage(\n", + " content=\"I'll need to think about this step by step.\"\n", + " )\n", + " }\n", + " return {output_key: response}\n", + "\n", + "\n", + "prompt = hub.pull(\"wfh/usaco-draft-solver\")\n", + "llm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", + "\n", + "draft_solver = Solver(llm, prompt.partial(examples=\"\"))\n", + "solver = Solver(llm, prompt)" + ] }, { "cell_type": "markdown", @@ -555,7 +918,13 @@ "id": "e5e0aa40-79a4-4071-9ad2-9aa2f36599ce", "metadata": {}, "outputs": [], - "source": ["# We will test our agent on index 0 (the same as above).\n# Later, we will test on index 2 (the first 'silver difficulty' question)\ntest_indices = [0, 2]\ntrain_ds = [row for i, row in enumerate(ds) if i not in test_indices]\ntest_ds = [row for i, row in enumerate(ds) if i in test_indices]"] + "source": [ + "# We will test our agent on index 0 (the same as above).\n", + "# Later, we will test on index 2 (the first 'silver difficulty' question)\n", + "test_indices = [0, 2]\n", + "train_ds = [row for i, row in enumerate(ds) if i not in test_indices]\n", + "test_ds = [row for i, row in enumerate(ds) if i in test_indices]" + ] }, { "cell_type": "code", @@ -563,7 +932,25 @@ "id": "96a1ff96-7556-4959-9f54-1ade3bd1c01a", "metadata": {}, "outputs": [], - "source": ["from langchain_community.retrievers import BM25Retriever\n\n\ndef format_example(row):\n question = row[\"description\"]\n answer = row[\"solution\"]\n return f\"\"\"\n{question}\n\n\n{answer}\n\"\"\"\n\n\n# Skip our 'test examples' to avoid cheating\n# This is \"simulating\" having seen other in-context examples\nretriever = BM25Retriever.from_texts([format_example(row) for row in train_ds])"] + "source": [ + "from langchain_community.retrievers import BM25Retriever\n", + "\n", + "\n", + "def format_example(row):\n", + " question = row[\"description\"]\n", + " answer = row[\"solution\"]\n", + " return f\"\"\"\n", + "{question}\n", + "\n", + "\n", + "{answer}\n", + "\"\"\"\n", + "\n", + "\n", + "# Skip our 'test examples' to avoid cheating\n", + "# This is \"simulating\" having seen other in-context examples\n", + "retriever = BM25Retriever.from_texts([format_example(row) for row in train_ds])" + ] }, { "cell_type": "markdown", @@ -580,7 +967,28 @@ "id": "af42962d-c06e-4b6e-96df-72ad48f17617", "metadata": {}, "outputs": [], - "source": ["from langchain_core.runnables import RunnableConfig\n\n\ndef retrieve_examples(state: State, config: RunnableConfig):\n top_k = config[\"configurable\"].get(\"k\") or 2\n ai_message: AIMessage = state[\"candidate\"]\n if not ai_message.tool_calls:\n # We err here. To make more robust, you could loop back\n raise ValueError(\"Draft agent did not produce a valid code block\")\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n examples_str = \"\\n\".join(\n [doc.page_content for doc in retriever.invoke(code)[:top_k]]\n )\n examples_str = f\"\"\"\nYou previously solved the following problems in this competition:\n\n{examples_str}\n\nApproach this new question with similar sophistication.\"\"\"\n return {\"examples\": examples_str}"] + "source": [ + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "\n", + "def retrieve_examples(state: State, config: RunnableConfig):\n", + " top_k = config[\"configurable\"].get(\"k\") or 2\n", + " ai_message: AIMessage = state[\"candidate\"]\n", + " if not ai_message.tool_calls:\n", + " # We err here. To make more robust, you could loop back\n", + " raise ValueError(\"Draft agent did not produce a valid code block\")\n", + " code = ai_message.tool_calls[0][\"args\"][\"code\"]\n", + " examples_str = \"\\n\".join(\n", + " [doc.page_content for doc in retriever.invoke(code)[:top_k]]\n", + " )\n", + " examples_str = f\"\"\"\n", + "You previously solved the following problems in this competition:\n", + "\n", + "{examples_str}\n", + "\n", + "Approach this new question with similar sophistication.\"\"\"\n", + " return {\"examples\": examples_str}" + ] }, { "cell_type": "markdown", @@ -598,7 +1006,34 @@ "id": "e6e73e85-1232-4848-beba-3139ac7d0a64", "metadata": {}, "outputs": [], - "source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = MemorySaver()\ngraph = builder.compile(checkpointer=checkpointer)"] + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"draft\", draft_solver)\n", + "builder.add_edge(START, \"draft\")\n", + "builder.add_node(\"retrieve\", retrieve_examples)\n", + "builder.add_node(\"solve\", solver)\n", + "builder.add_node(\"evaluate\", evaluate)\n", + "# Add connectivity\n", + "builder.add_edge(\"draft\", \"retrieve\")\n", + "builder.add_edge(\"retrieve\", \"solve\")\n", + "builder.add_edge(\"solve\", \"evaluate\")\n", + "\n", + "\n", + "def control_edge(state: State):\n", + " if state.get(\"status\") == \"success\":\n", + " return END\n", + " return \"solve\"\n", + "\n", + "\n", + "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n", + "\n", + "\n", + "checkpointer = MemorySaver()\n", + "graph = builder.compile(checkpointer=checkpointer)" + ] }, { "cell_type": "code", @@ -617,7 +1052,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -650,7 +1093,25 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"question-recall\", \"k\": 3}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"question-recall\", \"k\": 3}}\n", + "with tracing_v2_enabled(client=client):\n", + " events = graph.stream(input_state, config)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )\n", + " elif value.get(\"examples\"):\n", + " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", + " elif value.get(\"candidate\"):\n", + " print(str(value[\"candidate\"].content)[:200])" + ] }, { "cell_type": "markdown", @@ -677,7 +1138,10 @@ "output_type": "execute_result" } ], - "source": ["checkpoint = graph.get_state(config)\ncheckpoint.values[\"status\"]"] + "source": [ + "checkpoint = graph.get_state(config)\n", + "checkpoint.values[\"status\"]" + ] }, { "cell_type": "markdown", @@ -706,7 +1170,10 @@ "output_type": "execute_result" } ], - "source": ["silver_row = test_ds[1]\nsilver_row[\"problem_level\"]"] + "source": [ + "silver_row = test_ds[1]\n", + "silver_row[\"problem_level\"]" + ] }, { "cell_type": "code", @@ -764,7 +1231,33 @@ ] } ], - "source": ["silver_input = {\n \"messages\": [(\"user\", silver_row[\"description\"])],\n \"test_cases\": silver_row[\"test_cases\"],\n \"runtime_limit\": silver_row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n}\n\n\nconfig = {\"configurable\": {\"thread_id\": \"silver-question-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] + "source": [ + "silver_input = {\n", + " \"messages\": [(\"user\", silver_row[\"description\"])],\n", + " \"test_cases\": silver_row[\"test_cases\"],\n", + " \"runtime_limit\": silver_row[\"runtime_limit\"],\n", + " \"status\": \"in_progress\",\n", + "}\n", + "\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"silver-question-1\", \"k\": 2}}\n", + "with tracing_v2_enabled(client=client):\n", + " events = graph.stream(silver_input, config)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )\n", + " elif value.get(\"examples\"):\n", + " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", + " elif value.get(\"candidate\"):\n", + " print(str(value[\"candidate\"].content)[:200])" + ] }, { "cell_type": "markdown", @@ -809,7 +1302,36 @@ "id": "3c6456ba-363c-4133-8631-6dabb042b6ce", "metadata": {}, "outputs": [], - "source": ["# This is all the same as before\nfrom langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = MemorySaver()"] + "source": [ + "# This is all the same as before\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "builder = StateGraph(State)\n", + "prompt = hub.pull(\"wfh/usaco-draft-solver\")\n", + "llm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n", + "\n", + "draft_solver = Solver(llm, prompt.partial(examples=\"\"))\n", + "builder.add_node(\"draft\", draft_solver)\n", + "builder.add_edge(START, \"draft\")\n", + "builder.add_node(\"retrieve\", retrieve_examples)\n", + "solver = Solver(llm, prompt)\n", + "builder.add_node(\"solve\", solver)\n", + "builder.add_node(\"evaluate\", evaluate)\n", + "builder.add_edge(\"draft\", \"retrieve\")\n", + "builder.add_edge(\"retrieve\", \"solve\")\n", + "builder.add_edge(\"solve\", \"evaluate\")\n", + "\n", + "\n", + "def control_edge(state: State):\n", + " if state.get(\"status\") == \"success\":\n", + " return END\n", + " return \"solve\"\n", + "\n", + "\n", + "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n", + "checkpointer = MemorySaver()" + ] }, { "cell_type": "markdown", @@ -825,7 +1347,13 @@ "id": "461c13ba-01cc-44e1-b837-6a64d03069d9", "metadata": {}, "outputs": [], - "source": ["graph = builder.compile(\n checkpointer=checkpointer,\n # New: this tells the graph to break any time it goes to the \"human\" node\n interrupt_after=[\"evaluate\"],\n)"] + "source": [ + "graph = builder.compile(\n", + " checkpointer=checkpointer,\n", + " # New: this tells the graph to break any time it goes to the \"human\" node\n", + " interrupt_after=[\"evaluate\"],\n", + ")" + ] }, { "cell_type": "code", @@ -844,7 +1372,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -879,7 +1415,25 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"silver-hl-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"silver-hl-1\", \"k\": 2}}\n", + "with tracing_v2_enabled(client=client):\n", + " events = graph.stream(silver_input, config)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )\n", + " elif value.get(\"examples\"):\n", + " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", + " elif value.get(\"candidate\"):\n", + " print(str(value[\"candidate\"].content)[:200])" + ] }, { "cell_type": "markdown", @@ -962,7 +1516,10 @@ ] } ], - "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][0].content)"] + "source": [ + "snapshot = graph.get_state(config)\n", + "print(snapshot.values[\"messages\"][0].content)" + ] }, { "cell_type": "markdown", @@ -1042,7 +1599,12 @@ ] } ], - "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][-2].content[0][\"text\"])\nprint(\"\\n\\nCode:\\n\\n\")\nprint(snapshot.values[\"messages\"][-2].tool_calls[0][\"args\"][\"code\"])"] + "source": [ + "snapshot = graph.get_state(config)\n", + "print(snapshot.values[\"messages\"][-2].content[0][\"text\"])\n", + "print(\"\\n\\nCode:\\n\\n\")\n", + "print(snapshot.values[\"messages\"][-2].tool_calls[0][\"args\"][\"code\"])" + ] }, { "cell_type": "code", @@ -1071,7 +1633,9 @@ ] } ], - "source": ["print(snapshot.values[\"messages\"][-1].content[:200])"] + "source": [ + "print(snapshot.values[\"messages\"][-1].content[:200])" + ] }, { "cell_type": "markdown", @@ -1091,7 +1655,51 @@ "id": "b10fcbc9-6dd4-41ad-98f7-1cf1685035e6", "metadata": {}, "outputs": [], - "source": ["updated_config = graph.update_state(\n config,\n values={\n \"messages\": [\n (\n \"user\",\n \"\"\"Consider breaking down the algorithm into separate parts: reading inputs, detecting cycles using the tortoise and hare algorithm, and determining Bessie's final position by skipping ahead K steps.\n\nRead the inputs into three arrays:\n- Two arrays L and R for the ports (adjust for 0-based indexing)\n- A third array S for the direction sequence\n\nOptimize by multiplying K by M before the main loop to convert the number of repetitions into the total number of steps.\n\nUse the tortoise and hare algorithm to detect the cycle:\n- Define a helper function get_next(v) that returns the next position and direction index\n- Initialize two pointers s0 and s1 to (0, 0)\n- In each iteration:\n - Move s0 by 1 step and s1 by 2 steps using get_next()\n - If s0 equals s1, decrement K by 1 and break out of the loop\n - Otherwise, decrement K by 1\n- After the loop, if K is not 0, there is a cycle\n\nTo find the cycle length:\n- Initialize a counter variable rho to 1\n- Move s0 by 1 step using get_next()\n- Enter a loop:\n - Move s0 by 1 step using get_next()\n - Increment rho\n - If s0 equals s1, break out of the loop\n\nSkip ahead by reducing K modulo rho.\n\nSimulate the remaining steps:\n- While K > 0, move s0 to the next position using get_next() and decrement K\n\nPrint the final position (converted to 1-based indexing).\n\nPay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\"\"\",\n )\n ]\n },\n)"] + "source": [ + "updated_config = graph.update_state(\n", + " config,\n", + " values={\n", + " \"messages\": [\n", + " (\n", + " \"user\",\n", + " \"\"\"Consider breaking down the algorithm into separate parts: reading inputs, detecting cycles using the tortoise and hare algorithm, and determining Bessie's final position by skipping ahead K steps.\n", + "\n", + "Read the inputs into three arrays:\n", + "- Two arrays L and R for the ports (adjust for 0-based indexing)\n", + "- A third array S for the direction sequence\n", + "\n", + "Optimize by multiplying K by M before the main loop to convert the number of repetitions into the total number of steps.\n", + "\n", + "Use the tortoise and hare algorithm to detect the cycle:\n", + "- Define a helper function get_next(v) that returns the next position and direction index\n", + "- Initialize two pointers s0 and s1 to (0, 0)\n", + "- In each iteration:\n", + " - Move s0 by 1 step and s1 by 2 steps using get_next()\n", + " - If s0 equals s1, decrement K by 1 and break out of the loop\n", + " - Otherwise, decrement K by 1\n", + "- After the loop, if K is not 0, there is a cycle\n", + "\n", + "To find the cycle length:\n", + "- Initialize a counter variable rho to 1\n", + "- Move s0 by 1 step using get_next()\n", + "- Enter a loop:\n", + " - Move s0 by 1 step using get_next()\n", + " - Increment rho\n", + " - If s0 equals s1, break out of the loop\n", + "\n", + "Skip ahead by reducing K modulo rho.\n", + "\n", + "Simulate the remaining steps:\n", + "- While K > 0, move s0 to the next position using get_next() and decrement K\n", + "\n", + "Print the final position (converted to 1-based indexing).\n", + "\n", + "Pay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\"\"\",\n", + " )\n", + " ]\n", + " },\n", + ")" + ] }, { "cell_type": "markdown", @@ -1118,7 +1726,9 @@ "output_type": "execute_result" } ], - "source": ["graph.get_state(config).values[\"messages\"][-1]"] + "source": [ + "graph.get_state(config).values[\"messages\"][-1]" + ] }, { "cell_type": "markdown", @@ -1145,7 +1755,29 @@ ] } ], - "source": ["num_trials = 1\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] + "source": [ + "num_trials = 1\n", + "with tracing_v2_enabled(client=client):\n", + " for _ in range(num_trials):\n", + " events = graph.stream(None, updated_config)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )\n", + " elif value.get(\"examples\"):\n", + " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", + " elif value.get(\"candidate\"):\n", + " print(str(value[\"candidate\"].content)[:200])\n", + " if graph.get_state(config).values[\"status\"] == \"success\":\n", + " break\n", + " print(\"Continuing...\")" + ] }, { "cell_type": "code", @@ -1153,7 +1785,9 @@ "id": "20ee7535-1bc8-4105-87c4-0e7a89a011ff", "metadata": {}, "outputs": [], - "source": ["most_recent_state = list(graph.get_state_history(config))[0]"] + "source": [ + "most_recent_state = list(graph.get_state_history(config))[0]" + ] }, { "cell_type": "markdown", @@ -1231,7 +1865,14 @@ ] } ], - "source": ["snapshot = graph.get_state(most_recent_state.config)\nai_message = snapshot.values[\"messages\"][-2]\nif ai_message.content:\n print(ai_message.content)\nprint(\"\\n\\nCode:\\n\\n\")\nprint(ai_message.tool_calls[0][\"args\"][\"code\"] if ai_message.tool_calls else \"N/A\")"] + "source": [ + "snapshot = graph.get_state(most_recent_state.config)\n", + "ai_message = snapshot.values[\"messages\"][-2]\n", + "if ai_message.content:\n", + " print(ai_message.content)\n", + "print(\"\\n\\nCode:\\n\\n\")\n", + "print(ai_message.tool_calls[0][\"args\"][\"code\"] if ai_message.tool_calls else \"N/A\")" + ] }, { "cell_type": "code", @@ -1262,7 +1903,9 @@ ] } ], - "source": ["print(snapshot.values[\"messages\"][-1].content[:200])"] + "source": [ + "print(snapshot.values[\"messages\"][-1].content[:200])" + ] }, { "cell_type": "markdown", @@ -1280,7 +1923,24 @@ "id": "6eb46517-cdd1-4716-8a9e-df72cdd9ba67", "metadata": {}, "outputs": [], - "source": ["updated_config = graph.update_state(\n updated_config,\n values={\n \"messages\": [\n (\n \"user\",\n \"\"\"That's better, but you're still getting some errors. Let's double check some things:\n \n1. When calculating the cycle length, make sure the initialization and movement of the pointers is correct. Double-check the logic there and see if you can spot any discrepancies.\n2. Check the condition for whether there's a cycle after the main loop to ensure it covers all cases, like if K becomes 0 in the last iteration.\n\nThink step by step through youur implementation and update using the writePython tool.\"\"\",\n )\n ]\n },\n)"] + "source": [ + "updated_config = graph.update_state(\n", + " updated_config,\n", + " values={\n", + " \"messages\": [\n", + " (\n", + " \"user\",\n", + " \"\"\"That's better, but you're still getting some errors. Let's double check some things:\n", + " \n", + "1. When calculating the cycle length, make sure the initialization and movement of the pointers is correct. Double-check the logic there and see if you can spot any discrepancies.\n", + "2. Check the condition for whether there's a cycle after the main loop to ensure it covers all cases, like if K becomes 0 in the last iteration.\n", + "\n", + "Think step by step through youur implementation and update using the writePython tool.\"\"\",\n", + " )\n", + " ]\n", + " },\n", + ")" + ] }, { "cell_type": "markdown", @@ -1304,7 +1964,29 @@ ] } ], - "source": ["num_trials = 2\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] + "source": [ + "num_trials = 2\n", + "with tracing_v2_enabled(client=client):\n", + " for _ in range(num_trials):\n", + " events = graph.stream(None, updated_config)\n", + " for event in events:\n", + " for value in event.values():\n", + " messages = value.get(\"messages\")\n", + " if messages:\n", + " if isinstance(messages, list):\n", + " messages = value[\"messages\"][-1]\n", + " print(\n", + " \"Assistant:\",\n", + " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", + " )\n", + " elif value.get(\"examples\"):\n", + " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", + " elif value.get(\"candidate\"):\n", + " print(str(value[\"candidate\"].content)[:200])\n", + " if graph.get_state(config).values[\"status\"] == \"success\":\n", + " break\n", + " print(\"Continuing...\")" + ] }, { "cell_type": "markdown", @@ -1328,7 +2010,10 @@ ] } ], - "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"status\"])"] + "source": [ + "snapshot = graph.get_state(config)\n", + "print(snapshot.values[\"status\"])" + ] }, { "cell_type": "markdown", @@ -1354,14 +2039,6 @@ "\n", "LLMs are not capable of solving all these problems autonomously, but through better prompting and clever engineering, you can create a system that is able to more reliably arrive at the proper solution." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c71b4ba7-96ba-4643-b2de-eb2acdf4daba", - "metadata": {}, - "outputs": [], - "source": [""] } ], "metadata": { diff --git a/examples/web-navigation/web_voyager.ipynb b/examples/web-navigation/web_voyager.ipynb index 728003605..dae64975a 100644 --- a/examples/web-navigation/web_voyager.ipynb +++ b/examples/web-navigation/web_voyager.ipynb @@ -457,13 +457,13 @@ "source": [ "from langchain_core.runnables import RunnableLambda\n", "\n", - "from langgraph.graph import END, StateGraph\n", + "from langgraph.graph import END, START, StateGraph\n", "\n", "graph_builder = StateGraph(AgentState)\n", "\n", "\n", "graph_builder.add_node(\"agent\", agent)\n", - "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_edge(START, \"agent\")\n", "\n", "graph_builder.add_node(\"update_scratchpad\", update_scratchpad)\n", "graph_builder.add_edge(\"update_scratchpad\", \"agent\")\n", @@ -751,14 +751,6 @@ ")\n", "print(f\"Final response: {res}\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dcb656d9-fff5-42b0-9b16-60716a51ca0b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/libs/checkpoint-postgres/Makefile b/libs/checkpoint-postgres/Makefile index 043fef898..7057f1a97 100644 --- a/libs/checkpoint-postgres/Makefile +++ b/libs/checkpoint-postgres/Makefile @@ -38,11 +38,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint-postgres/README.md b/libs/checkpoint-postgres/README.md index 24a77673a..cf6beabac 100644 --- a/libs/checkpoint-postgres/README.md +++ b/libs/checkpoint-postgres/README.md @@ -2,6 +2,10 @@ Implementation of LangGraph CheckpointSaver that uses Postgres. +## Dependencies + +By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`). + ## Usage > [!IMPORTANT] @@ -44,7 +48,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -87,7 +90,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 02d5880a0..1c064415a 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -1,12 +1,13 @@ import threading from contextlib import contextmanager -from typing import Any, Iterator, List, Optional +from typing import Any, Iterator, List, Optional, Union from langchain_core.runnables import RunnableConfig from psycopg import Connection, Cursor, Pipeline from psycopg.errors import UndefinedTable from psycopg.rows import dict_row from psycopg.types.json import Jsonb +from psycopg_pool import ConnectionPool from langgraph.checkpoint.base import ( ChannelVersions, @@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import ( from langgraph.checkpoint.serde.base import SerializerProtocol +@contextmanager +def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]: + if isinstance(conn, Connection): + yield conn + elif isinstance(conn, ConnectionPool): + with conn.connection() as conn: + yield conn + else: + raise TypeError(f"Invalid connection type: {type(conn)}") + + class PostgresSaver(BasePostgresSaver): lock: threading.Lock def __init__( self, - conn: Connection, + conn: Union[Connection, ConnectionPool], pipe: Optional[Pipeline] = None, serde: Optional[SerializerProtocol] = None, ) -> None: super().__init__(serde=serde) + if isinstance(conn, ConnectionPool) and pipe is not None: + raise ValueError( + "Pipeline should be used only with a single Connection, not ConnectionPool." + ) + self.conn = conn self.pipe = pipe self.lock = threading.Lock() @@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver): already exist and runs database migrations. It MUST be called directly by the user the first time checkpointer is used. """ - with self.lock: - with self.conn.cursor(binary=True) as cur: - try: - version = cur.execute( - "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" - ).fetchone()["v"] - except UndefinedTable: - version = -1 - for v, migration in zip( - range(version + 1, len(self.MIGRATIONS)), - self.MIGRATIONS[version + 1 :], - ): - cur.execute(migration) - cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") - if self.pipe: - self.pipe.sync() + with self._cursor() as cur: + try: + version = cur.execute( + "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" + ).fetchone()["v"] + except UndefinedTable: + version = -1 + for v, migration in zip( + range(version + 1, len(self.MIGRATIONS)), + self.MIGRATIONS[version + 1 :], + ): + cur.execute(migration) + cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") + if self.pipe: + self.pipe.sync() def list( self, @@ -127,30 +143,34 @@ class PostgresSaver(BasePostgresSaver): if limit: query += f" LIMIT {limit}" # if we change this to use .stream() we need to make sure to close the cursor - for value in self.conn.execute(query, args, binary=True): - yield CheckpointTuple( - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["checkpoint_id"], + with self._cursor() as cur: + cur.execute(query, args, binary=True) + for value in cur: + yield CheckpointTuple( + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["checkpoint_id"], + } + }, + self._load_checkpoint( + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), + self._load_metadata(value["metadata"]), + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["parent_checkpoint_id"], + } } - }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": self._load_blobs(value["channel_values"]), - }, - self._load_metadata(value["metadata"]), - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["parent_checkpoint_id"], - } - } - if value["parent_checkpoint_id"] - else None, - ) + if value["parent_checkpoint_id"] + else None, + self._load_writes(value["pending_writes"]), + ) def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. @@ -198,7 +218,7 @@ class PostgresSaver(BasePostgresSaver): where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1" with self._cursor() as cur: - cur = self.conn.execute( + cur.execute( self.SELECT_SQL + where, args, binary=True, @@ -213,10 +233,11 @@ class PostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": self._load_blobs(value["channel_values"]), - }, + self._load_checkpoint( + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { @@ -317,16 +338,6 @@ class PostgresSaver(BasePostgresSaver): task_id (str): Identifier for the task creating the writes. """ with self._cursor(pipeline=True) as cur: - cur.execute( - self.DELETE_WRITES_SQL, - ( - config["configurable"]["thread_id"], - config["configurable"]["checkpoint_ns"], - config["configurable"]["checkpoint_id"], - task_id, - len(writes), - ), - ) cur.executemany( self.UPSERT_CHECKPOINT_WRITES_SQL, self._dump_writes( @@ -340,21 +351,24 @@ class PostgresSaver(BasePostgresSaver): @contextmanager def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]: - if self.pipe: - # a connection in pipeline mode can be used concurrently - # in multiple threads/coroutines, but only one cursor can be - # used at a time - try: - with self.conn.cursor(binary=True) as cur: + with _get_connection(self.conn) as conn: + if self.pipe: + # a connection in pipeline mode can be used concurrently + # in multiple threads/coroutines, but only one cursor can be + # used at a time + try: + with conn.cursor(binary=True, row_factory=dict_row) as cur: + yield cur + finally: + if pipeline: + self.pipe.sync() + elif pipeline: + # a connection not in pipeline mode can only be used by one + # thread/coroutine at a time, so we acquire a lock + with self.lock, conn.pipeline(), conn.cursor( + binary=True, row_factory=dict_row + ) as cur: + yield cur + else: + with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur: yield cur - finally: - if pipeline: - self.pipe.sync() - elif pipeline: - # a connection not in pipeline mode can only be used by one - # thread/coroutine at a time, so we acquire a lock - with self.lock, self.conn.pipeline(), self.conn.cursor(binary=True) as cur: - yield cur - else: - with self.lock, self.conn.cursor(binary=True) as cur: - yield cur diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 7ddb81237..6140af756 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -1,12 +1,13 @@ import asyncio from contextlib import asynccontextmanager -from typing import Any, AsyncIterator, Optional +from typing import Any, AsyncIterator, Iterator, List, Optional, Union from langchain_core.runnables import RunnableConfig from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline from psycopg.errors import UndefinedTable from psycopg.rows import dict_row from psycopg.types.json import Jsonb +from psycopg_pool import AsyncConnectionPool from langgraph.checkpoint.base import ( ChannelVersions, @@ -19,19 +20,38 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver from langgraph.checkpoint.serde.base import SerializerProtocol +@asynccontextmanager +async def _get_connection( + conn: Union[AsyncConnection, AsyncConnectionPool], +) -> AsyncIterator[AsyncConnection]: + if isinstance(conn, AsyncConnection): + yield conn + elif isinstance(conn, AsyncConnectionPool): + async with conn.connection() as conn: + yield conn + else: + raise TypeError(f"Invalid connection type: {type(conn)}") + + class AsyncPostgresSaver(BasePostgresSaver): lock: asyncio.Lock def __init__( self, - conn: AsyncConnection, + conn: Union[AsyncConnection, AsyncConnectionPool], pipe: Optional[AsyncPipeline] = None, serde: Optional[SerializerProtocol] = None, ) -> None: super().__init__(serde=serde) + if isinstance(conn, AsyncConnectionPool) and pipe is not None: + raise ValueError( + "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool." + ) + self.conn = conn self.pipe = pipe self.lock = asyncio.Lock() + self.loop = asyncio.get_running_loop() @classmethod @asynccontextmanager @@ -45,7 +65,7 @@ class AsyncPostgresSaver(BasePostgresSaver): pipeline (bool): whether to use AsyncPipeline Returns: - PostgresSaver: A new PostgresSaver instance. + AsyncPostgresSaver: A new AsyncPostgresSaver instance. """ async with await AsyncConnection.connect( conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row @@ -63,25 +83,22 @@ class AsyncPostgresSaver(BasePostgresSaver): already exist and runs database migrations. It MUST be called directly by the user the first time checkpointer is used. """ - async with self.lock: - async with self.conn.cursor(binary=True) as cur: - try: - results = await cur.execute( - "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" - ) - version = (await results.fetchone())["v"] - except UndefinedTable: - version = -1 - for v, migration in zip( - range(version + 1, len(self.MIGRATIONS)), - self.MIGRATIONS[version + 1 :], - ): - await cur.execute(migration) - await cur.execute( - f"INSERT INTO checkpoint_migrations (v) VALUES ({v})" - ) - if self.pipe: - await self.pipe.sync() + async with self._cursor() as cur: + try: + results = await cur.execute( + "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" + ) + version = (await results.fetchone())["v"] + except UndefinedTable: + version = -1 + for v, migration in zip( + range(version + 1, len(self.MIGRATIONS)), + self.MIGRATIONS[version + 1 :], + ): + await cur.execute(migration) + await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") + if self.pipe: + await self.pipe.sync() async def alist( self, @@ -110,32 +127,35 @@ class AsyncPostgresSaver(BasePostgresSaver): if limit: query += f" LIMIT {limit}" # if we change this to use .stream() we need to make sure to close the cursor - async for value in await self.conn.execute(query, args, binary=True): - yield CheckpointTuple( - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["checkpoint_id"], - } - }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": await asyncio.to_thread( - self._load_blobs, value["channel_values"] + async with self._cursor() as cur: + await cur.execute(query, args, binary=True) + async for value in cur: + yield CheckpointTuple( + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["checkpoint_id"], + } + }, + await asyncio.to_thread( + self._load_checkpoint, + value["checkpoint"], + value["channel_values"], + value["pending_sends"], ), - }, - self._load_metadata(value["metadata"]), - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["parent_checkpoint_id"], + self._load_metadata(value["metadata"]), + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["parent_checkpoint_id"], + } } - } - if value["parent_checkpoint_id"] - else None, - ) + if value["parent_checkpoint_id"] + else None, + await asyncio.to_thread(self._load_writes, value["pending_writes"]), + ) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database asynchronously. @@ -162,7 +182,7 @@ class AsyncPostgresSaver(BasePostgresSaver): where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1" async with self._cursor() as cur: - cur = await self.conn.execute( + await cur.execute( self.SELECT_SQL + where, args, binary=True, @@ -177,12 +197,12 @@ class AsyncPostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": await asyncio.to_thread( - self._load_blobs, value["channel_values"] - ), - }, + await asyncio.to_thread( + self._load_checkpoint, + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { @@ -273,16 +293,6 @@ class AsyncPostgresSaver(BasePostgresSaver): task_id (str): Identifier for the task creating the writes. """ async with self._cursor(pipeline=True) as cur: - await cur.execute( - self.DELETE_WRITES_SQL, - ( - config["configurable"]["thread_id"], - config["configurable"]["checkpoint_ns"], - config["configurable"]["checkpoint_id"], - task_id, - len(writes), - ), - ) await cur.executemany( self.UPSERT_CHECKPOINT_WRITES_SQL, await asyncio.to_thread( @@ -297,23 +307,119 @@ class AsyncPostgresSaver(BasePostgresSaver): @asynccontextmanager async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]: - if self.pipe: - # a connection in pipeline mode can be used concurrently - # in multiple threads/coroutines, but only one cursor can be - # used at a time - try: - async with self.conn.cursor(binary=True) as cur: + async with _get_connection(self.conn) as conn: + if self.pipe: + # a connection in pipeline mode can be used concurrently + # in multiple threads/coroutines, but only one cursor can be + # used at a time + try: + async with conn.cursor(binary=True, row_factory=dict_row) as cur: + yield cur + finally: + if pipeline: + await self.pipe.sync() + elif pipeline: + # a connection not in pipeline mode can only be used by one + # thread/coroutine at a time, so we acquire a lock + async with self.lock, conn.pipeline(), conn.cursor( + binary=True, row_factory=dict_row + ) as cur: yield cur - finally: - if pipeline: - await self.pipe.sync() - elif pipeline: - # a connection not in pipeline mode can only be used by one - # thread/coroutine at a time, so we acquire a lock - async with self.lock, self.conn.pipeline(), self.conn.cursor( - binary=True - ) as cur: - yield cur - else: - async with self.lock, self.conn.cursor(binary=True) as cur: - yield cur + else: + async with self.lock, conn.cursor( + binary=True, row_factory=dict_row + ) as cur: + yield cur + + def list( + self, + config: Optional[RunnableConfig], + *, + filter: Optional[dict[str, Any]] = None, + before: Optional[RunnableConfig] = None, + limit: Optional[int] = None, + ) -> Iterator[CheckpointTuple]: + """List checkpoints from the database. + + This method retrieves a list of checkpoint tuples from the Postgres database based + on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first). + + Args: + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None. + limit (Optional[int]): Maximum number of checkpoints to return. + + Yields: + Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. + """ + aiter_ = self.alist(config, filter=filter, before=before, limit=limit) + while True: + try: + yield asyncio.run_coroutine_threadsafe( + anext(aiter_), self.loop + ).result() + except StopAsyncIteration: + break + + def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: + """Get a checkpoint tuple from the database. + + This method retrieves a checkpoint tuple from the Postgres database based on the + provided config. If the config contains a "checkpoint_id" key, the checkpoint with + the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint + for the given thread ID is retrieved. + + Args: + config (RunnableConfig): The config to use for retrieving the checkpoint. + + Returns: + Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found. + """ + return asyncio.run_coroutine_threadsafe( + self.aget_tuple(config), self.loop + ).result() + + def put( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + """Save a checkpoint to the database. + + This method saves a checkpoint to the Postgres database. The checkpoint is associated + with the provided config and its parent config (if any). + + Args: + config (RunnableConfig): The config to associate with the checkpoint. + checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. + new_versions (ChannelVersions): New channel versions as of this write. + + Returns: + RunnableConfig: Updated configuration after storing the checkpoint. + """ + return asyncio.run_coroutine_threadsafe( + self.aput(config, checkpoint, metadata, new_versions), self.loop + ).result() + + def put_writes( + self, + config: RunnableConfig, + writes: List[tuple[str, Any]], + task_id: str, + ) -> None: + """Store intermediate writes linked to a checkpoint. + + This method saves intermediate writes associated with a checkpoint to the database. + + Args: + config (RunnableConfig): Configuration of the related checkpoint. + writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. + task_id (str): Identifier for the task creating the writes. + """ + return asyncio.run_coroutine_threadsafe( + self.aput_writes(config, writes, task_id), self.loop + ).result() diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index 506a13a27..6cfbc5108 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -1,4 +1,3 @@ -from base64 import b64decode, b64encode from hashlib import md5 from typing import Any, List, Optional, Tuple @@ -6,13 +5,14 @@ from langchain_core.runnables import RunnableConfig from psycopg.types.json import Jsonb from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, Checkpoint, EmptyChannelError, get_checkpoint_id, ) from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -from langgraph.checkpoint.serde.types import ChannelProtocol +from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol MetadataInput = Optional[dict[str, Any]] @@ -57,7 +57,7 @@ MIGRATIONS = [ "ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;", ] -SELECT_SQL = """ +SELECT_SQL = f""" select thread_id, checkpoint, @@ -76,12 +76,20 @@ select ) as channel_values, ( select - array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob]) + array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob] order by cw.task_id, cw.idx) from checkpoint_writes cw where cw.thread_id = checkpoints.thread_id and cw.checkpoint_ns = checkpoints.checkpoint_ns and cw.checkpoint_id = checkpoints.checkpoint_id - ) as pending_writes + ) as pending_writes, + ( + select array_agg(array[cw.type::bytea, cw.blob] order by cw.idx) + from checkpoint_writes cw + where cw.thread_id = checkpoints.thread_id + and cw.checkpoint_ns = checkpoints.checkpoint_ns + and cw.checkpoint_id = checkpoints.parent_checkpoint_id + and cw.channel = '{TASKS}' + ) as pending_sends from checkpoints """ UPSERT_CHECKPOINT_BLOBS_SQL = """ @@ -105,15 +113,6 @@ UPSERT_CHECKPOINT_WRITES_SQL = """ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING """ -DELETE_WRITES_SQL = """ - DELETE FROM checkpoint_writes - WHERE thread_id = %s - AND checkpoint_ns = %s - AND checkpoint_id = %s - AND task_id = %s - AND idx >= %s -""" - class BasePostgresSaver(BaseCheckpointSaver): SELECT_SQL = SELECT_SQL @@ -121,29 +120,26 @@ class BasePostgresSaver(BaseCheckpointSaver): UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL - DELETE_WRITES_SQL = DELETE_WRITES_SQL jsonplus_serde = JsonPlusSerializer() - def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint: - if len(checkpoint["pending_sends"]) == 2 and all( - isinstance(a, str) for a in checkpoint["pending_sends"] - ): - type, bs = checkpoint["pending_sends"] - return { - **checkpoint, - "pending_sends": self.serde.loads_typed((type, b64decode(bs))), - } - - return checkpoint - - def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]: - type, bs = self.serde.dumps_typed(checkpoint["pending_sends"]) + def _load_checkpoint( + self, + checkpoint: dict[str, Any], + channel_values: list[tuple[bytes, bytes, bytes]], + pending_sends: list[tuple[bytes, bytes]], + ) -> Checkpoint: return { **checkpoint, - "pending_sends": (type, b64encode(bs).decode()), + "pending_sends": [ + self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends or [] + ], + "channel_values": self._load_blobs(channel_values), } + def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]: + return {**checkpoint, "pending_sends": []} + def _load_blobs( self, blob_values: list[tuple[bytes, bytes, bytes]] ) -> dict[str, Any]: @@ -210,7 +206,7 @@ class BasePostgresSaver(BaseCheckpointSaver): checkpoint_ns, checkpoint_id, task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) @@ -264,9 +260,14 @@ class BasePostgresSaver(BaseCheckpointSaver): if config: wheres.append("thread_id = %s ") param_values.append(config["configurable"]["thread_id"]) - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - wheres.append("checkpoint_ns = %s") - param_values.append(checkpoint_ns) + checkpoint_ns = config["configurable"].get("checkpoint_ns") + if checkpoint_ns is not None: + wheres.append("checkpoint_ns = %s") + param_values.append(checkpoint_ns) + + if checkpoint_id := get_checkpoint_id(config): + wheres.append("checkpoint_id = %s ") + param_values.append(checkpoint_id) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock index 813bdf9b0..520300e91 100644 --- a/libs/checkpoint-postgres/poetry.lock +++ b/libs/checkpoint-postgres/poetry.lock @@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.1" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -394,6 +394,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -764,7 +766,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -822,28 +823,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -969,4 +971,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "422b6d716b86db072ea3a612287ad20ff5700c18f22d9e9d59cc4e198514519d" +content-hash = "e294b6996aa6c8f671e6aaf65be8b4aba94c18e2f237dd3ee0e1b777849ce8a8" diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index d7f6899b0..6e3640e7c 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-postgres" -version = "1.0.3" +version = "1.0.6" description = "Library with a Postgres implementation of LangGraph checkpoint saver." authors = [] license = "MIT" @@ -10,12 +10,13 @@ packages = [{ include = "langgraph" }] [tool.poetry.dependencies] python = "^3.9.0,<4.0" -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" orjson = ">=3.10.1" -psycopg = {extras = ["binary"], version = ">=3.1.19"} +psycopg = "^3.0.0" +psycopg-pool = "^3.0.0" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" anyio = "^4.4.0" @@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1" pytest-mock = "^3.11.1" pytest-watch = "^4.2.0" mypy = "^1.10.0" -psycopg-pool = "^3.2.2" +psycopg = {extras = ["binary"], version = ">=3.0.0"} langgraph-checkpoint = {path = "../checkpoint", develop = true} [tool.pytest.ini_options] diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py index e94cf32ae..6f9f7d78b 100644 --- a/libs/checkpoint-postgres/tests/test_async.py +++ b/libs/checkpoint-postgres/tests/test_async.py @@ -87,29 +87,15 @@ class TestAsyncPostgresSaver: search_results_4 = [c async for c in saver.alist(None, filter=query_4)] assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = [ c async for c in saver.alist({"configurable": {"thread_id": "thread-2"}}) ] - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = [ - c - async for c in saver.alist( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ] - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-postgres/tests/test_sync.py b/libs/checkpoint-postgres/tests/test_sync.py index dfae82907..a2fbcbd88 100644 --- a/libs/checkpoint-postgres/tests/test_sync.py +++ b/libs/checkpoint-postgres/tests/test_sync.py @@ -88,27 +88,14 @@ class TestPostgresSaver: search_results_4 = list(saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - saver.list( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ) - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-sqlite/Makefile b/libs/checkpoint-sqlite/Makefile index 734e46308..94b1963d6 100644 --- a/libs/checkpoint-sqlite/Makefile +++ b/libs/checkpoint-sqlite/Makefile @@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint-sqlite/README.md b/libs/checkpoint-sqlite/README.md index 86ca9cafc..73fe94333 100644 --- a/libs/checkpoint-sqlite/README.md +++ b/libs/checkpoint-sqlite/README.md @@ -35,7 +35,6 @@ with SqliteSaver.from_conn_string(":memory:") as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -78,7 +77,6 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -89,4 +87,4 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: # list checkpoints [c async for c in checkpointer.alist(read_config)] -``` \ No newline at end of file +``` diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index 9bf4139ad..31bffd19c 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -1,12 +1,13 @@ import sqlite3 import threading -from contextlib import contextmanager +from contextlib import closing, contextmanager from hashlib import md5 from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -103,10 +104,12 @@ class SqliteSaver(BaseCheckpointSaver): with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory: ... """ - with sqlite3.connect( - conn_string, - # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/ - check_same_thread=False, + with closing( + sqlite3.connect( + conn_string, + # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/ + check_same_thread=False, + ) ) as conn: yield SqliteSaver(conn) @@ -162,14 +165,15 @@ class SqliteSaver(BaseCheckpointSaver): Yields: sqlite3.Cursor: A cursor for the SQLite database. """ - self.setup() - cur = self.conn.cursor() - try: - yield cur - finally: - if transaction: - self.conn.commit() - cur.close() + with self.lock: + self.setup() + cur = self.conn.cursor() + try: + yield cur + finally: + if transaction: + self.conn.commit() + cur.close() def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. @@ -243,7 +247,7 @@ class SqliteSaver(BaseCheckpointSaver): } # find any pending writes cur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", ( str(config["configurable"]["thread_id"]), checkpoint_ns, @@ -318,7 +322,7 @@ class SqliteSaver(BaseCheckpointSaver): ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - with self.cursor(transaction=False) as cur: + with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur: cur.execute(query, param_values) for ( thread_id, @@ -329,6 +333,10 @@ class SqliteSaver(BaseCheckpointSaver): checkpoint, metadata, ) in cur: + wcur.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", + (thread_id, checkpoint_ns, checkpoint_id), + ) yield CheckpointTuple( { "configurable": { @@ -350,6 +358,10 @@ class SqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + for task_id, channel, type, value in wcur + ], ) def put( @@ -387,7 +399,7 @@ class SqliteSaver(BaseCheckpointSaver): checkpoint_ns = config["configurable"]["checkpoint_ns"] type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint) serialized_metadata = self.jsonplus_serde.dumps(metadata) - with self.lock, self.cursor() as cur: + with self.cursor() as cur: cur.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", ( @@ -423,26 +435,16 @@ class SqliteSaver(BaseCheckpointSaver): writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. task_id (str): Identifier for the task creating the writes. """ - with self.lock, self.cursor() as cur: - cur.execute( - "DELETE FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? AND task_id = ? AND idx >= ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["checkpoint_ns"]), - str(config["configurable"]["checkpoint_id"]), - task_id, - len(writes), - ), - ) + with self.cursor() as cur: cur.executemany( - "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ ( str(config["configurable"]["thread_id"]), str(config["configurable"]["checkpoint_ns"]), str(config["configurable"]["checkpoint_id"]), task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index 56d14f613..28ad76109 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -1,11 +1,11 @@ import asyncio -import functools from contextlib import asynccontextmanager from typing import ( Any, AsyncIterator, Dict, Iterator, + List, Optional, Sequence, Tuple, @@ -16,6 +16,7 @@ import aiosqlite from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -30,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where T = TypeVar("T", bound=callable) -def not_implemented_sync_method(func: T) -> T: - @functools.wraps(func) - def wrapper(*args, **kwargs): - raise NotImplementedError( - "The AsyncSqliteSaver does not support synchronous methods. " - "Consider using the SqliteSaver instead.\n" - "from langgraph.checkpoint.sqlite import SqliteSaver\n" - "See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver " - "for more information." - ) - - return wrapper - - class AsyncSqliteSaver(BaseCheckpointSaver): """An asynchronous checkpoint saver that stores checkpoints in a SQLite database. @@ -131,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): self.jsonplus_serde = JsonPlusSerializer() self.conn = conn self.lock = asyncio.Lock() + self.loop = asyncio.get_running_loop() self.is_setup = False @classmethod @@ -149,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver): async with aiosqlite.connect(conn_string) as conn: yield AsyncSqliteSaver(conn) - @not_implemented_sync_method def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. - Note: - This method is not implemented for the AsyncSqliteSaver. Use `aget` instead. - Or consider using the [SqliteSaver][sqlitesaver] checkpointer. - """ + This method retrieves a checkpoint tuple from the SQLite database based on the + provided config. If the config contains a "checkpoint_id" key, the checkpoint with + the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint + for the given thread ID is retrieved. + + Args: + config (RunnableConfig): The config to use for retrieving the checkpoint. + + Returns: + Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found. + """ + return asyncio.run_coroutine_threadsafe( + self.aget_tuple(config), self.loop + ).result() - @not_implemented_sync_method def list( self, config: Optional[RunnableConfig], @@ -167,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver): before: Optional[RunnableConfig] = None, limit: Optional[int] = None, ) -> Iterator[CheckpointTuple]: - """List checkpoints from the database. + """List checkpoints from the database asynchronously. - Note: - This method is not implemented for the AsyncSqliteSaver. Use `alist` instead. - Or consider using the [SqliteSaver][sqlitesaver] checkpointer. + This method retrieves a list of checkpoint tuples from the SQLite database based + on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first). + + Args: + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None. + limit (Optional[int]): Maximum number of checkpoints to return. + + Yields: + Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ + aiter_ = self.alist(config, filter=filter, before=before, limit=limit) + while True: + try: + yield asyncio.run_coroutine_threadsafe( + anext(aiter_), self.loop + ).result() + except StopAsyncIteration: + break - @not_implemented_sync_method def put( self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, + new_versions: ChannelVersions, ) -> RunnableConfig: - """Save a checkpoint to the database. FOO""" + """Save a checkpoint to the database. + + This method saves a checkpoint to the SQLite database. The checkpoint is associated + with the provided config and its parent config (if any). + + Args: + config (RunnableConfig): The config to associate with the checkpoint. + checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. + new_versions (ChannelVersions): New channel versions as of this write. + + Returns: + RunnableConfig: Updated configuration after storing the checkpoint. + """ + return asyncio.run_coroutine_threadsafe( + self.aput(config, checkpoint, metadata, new_versions), self.loop + ).result() + + def put_writes( + self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str + ) -> None: + return asyncio.run_coroutine_threadsafe( + self.aput_writes(config, writes, task_id), self.loop + ).result() async def setup(self) -> None: """Set up the checkpoint database asynchronously. @@ -241,7 +276,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): """ await self.setup() checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - async with self.conn.cursor() as cur: + async with self.lock, self.conn.cursor() as cur: # find the latest checkpoint for the thread_id if checkpoint_id := get_checkpoint_id(config): await cur.execute( @@ -277,7 +312,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): } # find any pending writes await cur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", ( str(config["configurable"]["thread_id"]), checkpoint_ns, @@ -329,14 +364,16 @@ class AsyncSqliteSaver(BaseCheckpointSaver): AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples. """ await self.setup() - where, param_values = search_where(config, filter, before) + where, params = search_where(config, filter, before) query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints {where} ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - async with self.conn.execute(query, param_values) as cursor: + async with self.lock, self.conn.execute( + query, params + ) as cur, self.conn.cursor() as wcur: async for ( thread_id, checkpoint_ns, @@ -345,7 +382,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver): type, checkpoint, metadata, - ) in cursor: + ) in cur: + await wcur.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", + (thread_id, checkpoint_ns, checkpoint_id), + ) yield CheckpointTuple( { "configurable": { @@ -367,6 +408,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + async for task_id, channel, type, value in wcur + ], ) async def aput( @@ -395,7 +440,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): checkpoint_ns = config["configurable"]["checkpoint_ns"] type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint) serialized_metadata = self.jsonplus_serde.dumps(metadata) - async with self.conn.execute( + async with self.lock, self.conn.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", ( str(config["configurable"]["thread_id"]), @@ -432,26 +477,16 @@ class AsyncSqliteSaver(BaseCheckpointSaver): task_id (str): Identifier for the task creating the writes. """ await self.setup() - async with self.conn.cursor() as cur: - await cur.execute( - "DELETE FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? AND task_id = ? AND idx >= ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["checkpoint_ns"]), - str(config["configurable"]["checkpoint_id"]), - task_id, - len(writes), - ), - ) + async with self.lock, self.conn.cursor() as cur: await cur.executemany( - "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ ( str(config["configurable"]["thread_id"]), str(config["configurable"]["checkpoint_ns"]), str(config["configurable"]["checkpoint_id"]), task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py index 6e1baf5ae..a8ceb496a 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py @@ -70,9 +70,14 @@ def search_where( if config is not None: wheres.append("thread_id = ?") param_values.append(config["configurable"]["thread_id"]) - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - wheres.append("checkpoint_ns = ?") - param_values.append(checkpoint_ns) + checkpoint_ns = config["configurable"].get("checkpoint_ns") + if checkpoint_ns is not None: + wheres.append("checkpoint_ns = ?") + param_values.append(checkpoint_ns) + + if checkpoint_id := get_checkpoint_id(config): + wheres.append("checkpoint_id = ?") + param_values.append(checkpoint_id) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint-sqlite/poetry.lock b/libs/checkpoint-sqlite/poetry.lock index cd7aa8376..8d5dbe3af 100644 --- a/libs/checkpoint-sqlite/poetry.lock +++ b/libs/checkpoint-sqlite/poetry.lock @@ -252,7 +252,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.1" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -380,6 +380,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -649,7 +651,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -707,28 +708,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -832,4 +834,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0" -content-hash = "e073e1a73cdae1fae8ea46499c39e55980cb61c7c8bd6be774c64f80a627eb31" +content-hash = "752a22dc2b57a0818a3a4d9bf5f62226ba7f8e0c458892551bf8b4c93723dbc1" diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 0a39f96cb..1b9462085 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-sqlite" -version = "1.0.0" +version = "1.0.3" description = "Library with a SQLite implementation of LangGraph checkpoint saver." authors = [] license = "MIT" @@ -10,11 +10,11 @@ packages = [{ include = "langgraph" }] [tool.poetry.dependencies] python = "^3.9.0" -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" aiosqlite = "^0.20.0" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" diff --git a/libs/checkpoint-sqlite/tests/test_aiosqlite.py b/libs/checkpoint-sqlite/tests/test_aiosqlite.py index 59f830dae..038030172 100644 --- a/libs/checkpoint-sqlite/tests/test_aiosqlite.py +++ b/libs/checkpoint-sqlite/tests/test_aiosqlite.py @@ -84,29 +84,15 @@ class TestAsyncSqliteSaver: search_results_4 = [c async for c in saver.alist(None, filter=query_4)] assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = [ c async for c in saver.alist({"configurable": {"thread_id": "thread-2"}}) ] - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = [ - c - async for c in saver.alist( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ] - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-sqlite/tests/test_sqlite.py b/libs/checkpoint-sqlite/tests/test_sqlite.py index 2147cca87..99b7a3728 100644 --- a/libs/checkpoint-sqlite/tests/test_sqlite.py +++ b/libs/checkpoint-sqlite/tests/test_sqlite.py @@ -87,28 +87,15 @@ class TestSqliteSaver: search_results_4 = list(saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - saver.list( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ) - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint/Makefile b/libs/checkpoint/Makefile index 734e46308..94b1963d6 100644 --- a/libs/checkpoint/Makefile +++ b/libs/checkpoint/Makefile @@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint/README.md b/libs/checkpoint/README.md index 7f6b26d6e..19c7d3807 100644 --- a/libs/checkpoint/README.md +++ b/libs/checkpoint/README.md @@ -74,7 +74,6 @@ checkpoint = { } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 86c8b0eec..17caf46d8 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -1,4 +1,3 @@ -from abc import ABC from datetime import datetime, timezone from typing import ( Any, @@ -22,6 +21,8 @@ from langgraph.checkpoint.base.id import uuid6 from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.serde.types import ( + ERROR, + SCHEDULED, ChannelProtocol, SendProtocol, ) @@ -51,10 +52,10 @@ class CheckpointMetadata(TypedDict, total=False): Mapping from node name to writes emitted by that node. """ - score: Optional[int] - """The score of the checkpoint. + parents: dict[str, str] + """The IDs of the parent checkpoints. - The score can be used to mark a checkpoint as "good". + Mapping from checkpoint namespace to checkpoint ID. """ @@ -96,8 +97,6 @@ class Checkpoint(TypedDict): pending_sends: List[SendProtocol] """List of packets sent to nodes but not yet processed. Cleared by the next checkpoint.""" - current_tasks: Dict[str, TaskInfo] - """Map from task ID to task info.""" def empty_checkpoint() -> Checkpoint: @@ -109,7 +108,6 @@ def empty_checkpoint() -> Checkpoint: channel_versions={}, versions_seen={}, pending_sends=[], - current_tasks={}, ) @@ -122,7 +120,6 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: channel_versions=checkpoint["channel_versions"].copy(), versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()}, pending_sends=checkpoint.get("pending_sends", []).copy(), - current_tasks=checkpoint.get("current_tasks", {}).copy(), ) @@ -140,6 +137,8 @@ def create_checkpoint( else: values: dict[str, Any] = {} for k, v in channels.items(): + if k not in checkpoint["channel_versions"]: + continue try: values[k] = v.checkpoint() except EmptyChannelError: @@ -152,7 +151,6 @@ def create_checkpoint( channel_versions=checkpoint["channel_versions"], versions_seen=checkpoint["versions_seen"], pending_sends=checkpoint.get("pending_sends", []), - current_tasks={}, ) @@ -194,7 +192,7 @@ CheckpointId = ConfigurableFieldSpec( ) -class BaseCheckpointSaver(ABC): +class BaseCheckpointSaver: """Base class for creating a graph checkpointer. Checkpointers allow LangGraph agents to persist their state @@ -437,3 +435,13 @@ def get_checkpoint_id(config: RunnableConfig) -> Optional[str]: return config["configurable"].get( "checkpoint_id", config["configurable"].get("thread_ts") ) + + +""" +Mapping from error type to error index. +Regular writes just map to their index in the list of writes being saved. +Special writes (e.g. errors) map to negative indices, to avoid those writes from +conflicting with regular writes. +Each Checkpointer implementation should use this mapping in put_writes. +""" +WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2} diff --git a/libs/checkpoint/langgraph/checkpoint/base/id.py b/libs/checkpoint/langgraph/checkpoint/base/id.py index 459ac7995..807dc8ad0 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/id.py +++ b/libs/checkpoint/langgraph/checkpoint/base/id.py @@ -3,7 +3,7 @@ https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L95 Bundled in to avoid install issues with uuid6 package """ -import secrets +import random import time import uuid from typing import Optional, Tuple @@ -96,9 +96,9 @@ def uuid6(node: Optional[int] = None, clock_seq: Optional[int] = None) -> UUID: timestamp = _last_v6_timestamp + 1 _last_v6_timestamp = timestamp if clock_seq is None: - clock_seq = secrets.randbits(14) # instead of stable storage + clock_seq = random.getrandbits(14) # instead of stable storage if node is None: - node = secrets.randbits(48) + node = random.getrandbits(48) time_high_and_time_mid = (timestamp >> 12) & 0xFFFFFFFFFFFF time_low_and_version = timestamp & 0x0FFF uuid_int = time_high_and_time_mid << 80 diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 3e8d328e6..372e5ba2f 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -8,6 +8,7 @@ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -16,6 +17,7 @@ from langgraph.checkpoint.base import ( SerializerProtocol, get_checkpoint_id, ) +from langgraph.checkpoint.serde.types import TASKS class MemorySaver( @@ -52,6 +54,9 @@ class MemorySaver( # thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping storage: defaultdict[str, dict[str, dict[str, tuple[bytes, bytes, Optional[str]]]]] + writes: defaultdict[ + tuple[str, str, str], dict[tuple[str, int], tuple[str, str, bytes]] + ] def __init__( self, @@ -60,7 +65,7 @@ class MemorySaver( ) -> None: super().__init__(serde=serde) self.storage = defaultdict(lambda: defaultdict(dict)) - self.writes = defaultdict(list) + self.writes = defaultdict(dict) def __enter__(self) -> "MemorySaver": return self @@ -103,10 +108,23 @@ class MemorySaver( if checkpoint_id := get_checkpoint_id(config): if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id): checkpoint, metadata, parent_checkpoint_id = saved - writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)] + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + if parent_checkpoint_id: + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] return CheckpointTuple( config=config, - checkpoint=self.serde.loads_typed(checkpoint), + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, metadata=self.serde.loads_typed(metadata), pending_writes=[ (id, c, self.serde.loads_typed(v)) for id, c, v in writes @@ -125,7 +143,17 @@ class MemorySaver( if checkpoints := self.storage[thread_id][checkpoint_ns]: checkpoint_id = max(checkpoints.keys()) checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id] - writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)] + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + if parent_checkpoint_id: + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] return CheckpointTuple( config={ "configurable": { @@ -134,7 +162,10 @@ class MemorySaver( "checkpoint_id": checkpoint_id, } }, - checkpoint=self.serde.loads_typed(checkpoint), + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, metadata=self.serde.loads_typed(metadata), pending_writes=[ (id, c, self.serde.loads_typed(v)) for id, c, v in writes @@ -173,57 +204,94 @@ class MemorySaver( Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage - checkpoint_ns = ( - config["configurable"].get("checkpoint_ns", "") if config else "" + config_checkpoint_ns = ( + config["configurable"].get("checkpoint_ns") if config else None ) + config_checkpoint_id = get_checkpoint_id(config) if config else None for thread_id in thread_ids: - for checkpoint_id, (checkpoint, metadata_b, parent_checkpoint_id) in sorted( - self.storage[thread_id][checkpoint_ns].items(), - key=lambda x: x[0], - reverse=True, - ): - # filter by checkpoint ID + for checkpoint_ns in self.storage[thread_id].keys(): if ( - before - and (before_checkpoint_id := get_checkpoint_id(before)) - and checkpoint_id >= before_checkpoint_id + config_checkpoint_ns is not None + and checkpoint_ns != config_checkpoint_ns ): continue - # filter by metadata - metadata = self.serde.loads_typed(metadata_b) - if filter and not all( - query_value == metadata[query_key] - for query_key, query_value in filter.items() + for checkpoint_id, ( + checkpoint, + metadata_b, + parent_checkpoint_id, + ) in sorted( + self.storage[thread_id][checkpoint_ns].items(), + key=lambda x: x[0], + reverse=True, ): - continue + # filter by checkpoint ID from config + if config_checkpoint_id and checkpoint_id != config_checkpoint_id: + continue - # limit search results - if limit is not None and limit <= 0: - break - elif limit is not None: - limit -= 1 + # filter by checkpoint ID from `before` config + if ( + before + and (before_checkpoint_id := get_checkpoint_id(before)) + and checkpoint_id >= before_checkpoint_id + ): + continue - yield CheckpointTuple( - config={ - "configurable": { - "thread_id": thread_id, - "checkpoint_ns": checkpoint_ns, - "checkpoint_id": checkpoint_id, + # filter by metadata + metadata = self.serde.loads_typed(metadata_b) + if filter and not all( + query_value == metadata.get(query_key) + for query_key, query_value in filter.items() + ): + continue + + # limit search results + if limit is not None and limit <= 0: + break + elif limit is not None: + limit -= 1 + + writes = self.writes[ + (thread_id, checkpoint_ns, checkpoint_id) + ].values() + + if parent_checkpoint_id: + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] + + yield CheckpointTuple( + config={ + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint_id, + } + }, + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, + metadata=metadata, + parent_config={ + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": parent_checkpoint_id, + } } - }, - checkpoint=self.serde.loads_typed(checkpoint), - metadata=metadata, - parent_config={ - "configurable": { - "thread_id": thread_id, - "checkpoint_ns": checkpoint_ns, - "checkpoint_id": parent_checkpoint_id, - } - } - if parent_checkpoint_id - else None, - ) + if parent_checkpoint_id + else None, + pending_writes=[ + (id, c, self.serde.loads_typed(v)) for id, c, v in writes + ], + ) def put( self, @@ -246,12 +314,14 @@ class MemorySaver( Returns: RunnableConfig: The updated config containing the saved checkpoint's timestamp. """ + c = checkpoint.copy() + c.pop("pending_sends") thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"]["checkpoint_ns"] self.storage[thread_id][checkpoint_ns].update( { checkpoint["id"]: ( - self.serde.dumps_typed(checkpoint), + self.serde.dumps_typed(c), self.serde.dumps_typed(metadata), config["configurable"].get("checkpoint_id"), # parent ) @@ -287,11 +357,10 @@ class MemorySaver( thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"]["checkpoint_ns"] checkpoint_id = config["configurable"]["checkpoint_id"] - key = (thread_id, checkpoint_ns, checkpoint_id) - self.writes[key] = [w for w in self.writes[key] if w[0] != task_id] - self.writes[key].extend( - [(task_id, c, self.serde.dumps_typed(v)) for c, v in writes] - ) + outer_key = (thread_id, checkpoint_ns, checkpoint_id) + for idx, (c, v) in enumerate(writes): + inner_key = (task_id, WRITES_IDX_MAP.get(c, idx)) + self.writes[outer_key][inner_key] = (task_id, c, self.serde.dumps_typed(v)) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Asynchronous version of get_tuple. @@ -330,7 +399,14 @@ class MemorySaver( """ loop = asyncio.get_running_loop() iter = await loop.run_in_executor( - None, partial(self.list, before=before, limit=limit, filter=filter), config + None, + partial( + self.list, + before=before, + limit=limit, + filter=filter, + ), + config, ) while True: # handling StopIteration exception inside coroutine won't work diff --git a/libs/checkpoint/langgraph/checkpoint/serde/base.py b/libs/checkpoint/langgraph/checkpoint/serde/base.py index 5e4fba5be..58be7e08c 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/base.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/base.py @@ -12,17 +12,13 @@ class SerializerProtocol(Protocol): Valid implementations include the `pickle`, `json` and `orjson` modules. """ - def dumps(self, obj: Any) -> bytes: - ... + def dumps(self, obj: Any) -> bytes: ... - def dumps_typed(self, obj: Any) -> tuple[str, bytes]: - ... + def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ... - def loads(self, data: bytes) -> Any: - ... + def loads(self, data: bytes) -> Any: ... - def loads_typed(self, data: tuple[str, bytes]) -> Any: - ... + def loads_typed(self, data: tuple[str, bytes]) -> Any: ... class SerializerCompat(SerializerProtocol): diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 5465b2802..e777a8176 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -7,6 +7,7 @@ import re from collections import deque from datetime import date, datetime, time, timedelta, timezone from enum import Enum +from inspect import isclass from ipaddress import ( IPv4Address, IPv4Interface, @@ -50,9 +51,13 @@ class JsonPlusSerializer(SerializerProtocol): if isinstance(obj, Serializable): return obj.to_json() elif hasattr(obj, "model_dump") and callable(obj.model_dump): - return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump()) + return self._encode_constructor_args( + obj.__class__, method=[None, "model_construct"], kwargs=obj.model_dump() + ) elif hasattr(obj, "dict") and callable(obj.dict): - return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) + return self._encode_constructor_args( + obj.__class__, method=[None, "construct"], kwargs=obj.dict() + ) elif isinstance(obj, pathlib.Path): return self._encode_constructor_args(pathlib.Path, args=obj.parts) elif isinstance(obj, re.Pattern): @@ -111,7 +116,7 @@ class JsonPlusSerializer(SerializerProtocol): obj.__class__, method="fromhex", args=[obj.hex()] ) elif isinstance(obj, BaseException): - return self._encode_constructor_args(obj.__class__, args=obj.args) + return repr(obj) else: raise TypeError( f"Object of type {obj.__class__.__name__} is not JSON serializable" @@ -131,19 +136,30 @@ class JsonPlusSerializer(SerializerProtocol): # Import class cls = getattr(mod, name) # Instantiate class - if value["method"] is not None: - method = getattr(cls, value["method"]) + if isinstance(value["method"], str): + methods = [getattr(cls, value["method"])] + elif isinstance(value["method"], list): + methods = [ + cls if method is None else getattr(cls, method) + for method in value["method"] + ] else: - method = cls - if value["args"] and value["kwargs"]: - return method(*value["args"], **value["kwargs"]) - elif value["args"]: - return method(*value["args"]) - elif value["kwargs"]: - return method(**value["kwargs"]) - else: - return method() - except (ImportError, AttributeError): + methods = [cls] + for method in methods: + try: + if isclass(method) and issubclass(method, BaseException): + return None + if value["args"] and value["kwargs"]: + return method(*value["args"], **value["kwargs"]) + elif value["args"]: + return method(*value["args"]) + elif value["kwargs"]: + return method(**value["kwargs"]) + else: + return method() + except Exception: + continue + except Exception: return None return LC_REVIVER(value) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index de61ef78b..3fc82b68d 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -12,6 +12,10 @@ from typing import ( from langchain_core.runnables import RunnableConfig from typing_extensions import Self +ERROR = "__error__" +SCHEDULED = "__scheduled__" +TASKS = "__pregel_tasks" + Value = TypeVar("Value") Update = TypeVar("Update") C = TypeVar("C") @@ -20,34 +24,26 @@ C = TypeVar("C") class ChannelProtocol(Protocol[Value, Update, C]): # Mirrors langgraph.channels.base.BaseChannel @property - def ValueType(self) -> Any: - ... + def ValueType(self) -> Any: ... @property - def UpdateType(self) -> Any: - ... + def UpdateType(self) -> Any: ... - def checkpoint(self) -> Optional[C]: - ... + def checkpoint(self) -> Optional[C]: ... def from_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> Generator[Self, None, None]: - ... + ) -> Generator[Self, None, None]: ... async def afrom_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> AsyncGenerator[Self, None]: - ... + ) -> AsyncGenerator[Self, None]: ... - def update(self, values: Sequence[Update]) -> bool: - ... + def update(self, values: Sequence[Update]) -> bool: ... - def get(self) -> Value: - ... + def get(self) -> Value: ... - def consume(self) -> bool: - ... + def consume(self) -> bool: ... @runtime_checkable @@ -56,11 +52,8 @@ class SendProtocol(Protocol): node: str arg: Any - def __hash__(self) -> int: - ... + def __hash__(self) -> int: ... - def __repr__(self) -> str: - ... + def __repr__(self) -> str: ... - def __eq__(self, value: object) -> bool: - ... + def __eq__(self, value: object) -> bool: ... diff --git a/libs/checkpoint/poetry.lock b/libs/checkpoint/poetry.lock index 7bed6ad81..f8e3cd636 100644 --- a/libs/checkpoint/poetry.lock +++ b/libs/checkpoint/poetry.lock @@ -227,13 +227,13 @@ files = [ [[package]] name = "langchain-core" -version = "0.2.24" +version = "0.2.38" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.2.24-py3-none-any.whl", hash = "sha256:9444fc082d21ef075d925590a684a73fe1f9688a3d90087580ec929751be55e7"}, - {file = "langchain_core-0.2.24.tar.gz", hash = "sha256:f2e3fa200b124e8c45d270da9bf836bed9c09532612c96ff3225e59b9a232f5a"}, + {file = "langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a"}, + {file = "langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3"}, ] [package.dependencies] @@ -246,6 +246,7 @@ pydantic = [ ] PyYAML = ">=5.3" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" +typing-extensions = ">=4.7" [[package]] name = "langsmith" @@ -380,6 +381,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -707,28 +710,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -847,4 +851,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "3bee25f1adc1349de4358a88037693cec47f6a2b88b809490f9198e6313239f6" +content-hash = "d4c13800471766fa9e2d11d2f1092f02fbf28cc507189aeea8a3d5b297286068" diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index b17a77238..00183d2f8 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.3" +version = "1.0.9" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" @@ -10,10 +10,10 @@ packages = [{ include = "langgraph" }] [tool.poetry.dependencies] python = "^3.9.0,<4.0" -langchain-core = ">=0.2.22,<0.3" +langchain-core = ">=0.2.38,<0.4" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" diff --git a/libs/checkpoint/tests/test_jsonplus.py b/libs/checkpoint/tests/test_jsonplus.py index d5a53d248..0ead0ed53 100644 --- a/libs/checkpoint/tests/test_jsonplus.py +++ b/libs/checkpoint/tests/test_jsonplus.py @@ -10,9 +10,9 @@ from enum import Enum from ipaddress import IPv4Address import dataclasses_json -from langchain_core.pydantic_v1 import BaseModel as LcBaseModel from langchain_core.runnables import RunnableMap from pydantic import BaseModel +from pydantic.v1 import BaseModel as BaseModelV1 from zoneinfo import ZoneInfo from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer @@ -23,7 +23,7 @@ class MyPydantic(BaseModel): bar: int -class MyFunnyPydantic(LcBaseModel): +class MyFunnyPydantic(BaseModelV1): foo: str bar: int @@ -122,7 +122,7 @@ def test_serde_jsonplus() -> None: assert dumped == ( "json", - b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", + b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": [null, "model_construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": [null, "construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", ) assert serde.loads_typed(dumped) == { diff --git a/libs/checkpoint/tests/test_memory.py b/libs/checkpoint/tests/test_memory.py index a0bc8d738..34c13b2d0 100644 --- a/libs/checkpoint/tests/test_memory.py +++ b/libs/checkpoint/tests/test_memory.py @@ -82,26 +82,20 @@ class TestMemorySaver: assert search_results_2[0].metadata == self.metadata_2 search_results_3 = list(self.memory_saver.list(None, filter=query_3)) - assert len(search_results_3) == 2 + assert len(search_results_3) == 3 search_results_4 = list(self.memory_saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( self.memory_saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - self.memory_saver.list( - {"configurable": {"thread_id": "thread-2", "checkpoint_ns": "inner"}} - ) - ) - assert len(search_results_6) == 1 - assert search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params @@ -110,6 +104,7 @@ class TestMemorySaver: # save checkpoints self.memory_saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {}) self.memory_saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {}) + self.memory_saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {}) # call method / assertions query_1: CheckpointMetadata = {"source": "input"} # search by 1 key @@ -135,7 +130,7 @@ class TestMemorySaver: search_results_3 = [ c async for c in self.memory_saver.alist(None, filter=query_3) ] - assert len(search_results_3) == 2 + assert len(search_results_3) == 3 search_results_4 = [ c async for c in self.memory_saver.alist(None, filter=query_4) diff --git a/libs/cli/Makefile b/libs/cli/Makefile index 680c8f53c..424946fdf 100644 --- a/libs/cli/Makefile +++ b/libs/cli/Makefile @@ -21,11 +21,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) \ No newline at end of file + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/cli/examples/graphs/storm.py b/libs/cli/examples/graphs/storm.py index e5ef2346e..dbc38ab95 100644 --- a/libs/cli/examples/graphs/storm.py +++ b/libs/cli/examples/graphs/storm.py @@ -14,12 +14,12 @@ from langchain_core.messages import ( ) from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.runnables import RunnableConfig, RunnableLambda from langchain_core.runnables import chain as as_runnable from langchain_core.tools import tool from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langgraph.graph import END, StateGraph +from pydantic import BaseModel, Field from typing_extensions import TypedDict fast_llm = ChatOpenAI(model="gpt-3.5-turbo") @@ -339,7 +339,7 @@ async def gen_answer( # We could be more precise about handling max token length if we wanted to here dumped = json.dumps(all_query_results)[:max_str_len] ai_message: AIMessage = queries["raw"] - tool_call = queries["raw"].additional_kwargs["tool_calls"][0] + tool_call = queries["raw"].tool_calls[0] tool_id = tool_call["id"] tool_message = ToolMessage(tool_call_id=tool_id, content=dumped) swapped_state["messages"].extend([ai_message, tool_message]) diff --git a/libs/cli/examples/poetry.lock b/libs/cli/examples/poetry.lock index ec32912f9..936020585 100644 --- a/libs/cli/examples/poetry.lock +++ b/libs/cli/examples/poetry.lock @@ -22,197 +22,6 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] -[[package]] -name = "appnope" -version = "0.1.4" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = ">=3.6" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -description = "Argon2 for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[package.dependencies] -argon2-cffi-bindings = "*" - -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -description = "Low-level CFFI bindings for Argon2" -optional = false -python-versions = ">=3.6" -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, -] - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "async-lru" -version = "2.0.4" -description = "Simple LRU cache for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, - {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "babel" -version = "2.15.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bleach" -version = "6.1.0" -description = "An easy safelist-based HTML-sanitizing tool." -optional = false -python-versions = ">=3.8" -files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, -] - -[package.dependencies] -six = ">=1.9.0" -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] - [[package]] name = "certifi" version = "2024.7.4" @@ -224,169 +33,6 @@ files = [ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - [[package]] name = "click" version = "8.1.7" @@ -412,76 +58,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "comm" -version = "0.2.2" -description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -optional = false -python-versions = ">=3.8" -files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, -] - -[package.dependencies] -traitlets = ">=4" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "debugpy" -version = "1.8.1" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, - {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, - {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, - {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, - {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, - {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, - {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, - {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, - {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, - {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, - {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, - {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, - {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, - {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, - {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, - {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, - {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, - {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, - {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, - {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, - {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, - {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -496,45 +72,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "executing" -version = "2.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastjsonschema" -version = "2.20.0" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "fqdn" -version = "1.5.1" -description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -optional = false -python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" -files = [ - {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, - {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, -] - [[package]] name = "h11" version = "0.14.0" @@ -613,499 +150,9 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "importlib-metadata" -version = "7.1.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "ipykernel" -version = "6.29.4" -description = "IPython Kernel for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, - {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -comm = ">=0.1.1" -debugpy = ">=1.6.5" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=24" -tornado = ">=6.1" -traitlets = ">=5.4.0" - -[package.extras] -cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] -pyqt5 = ["pyqt5"] -pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipython" -version = "8.18.1" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] - -[[package]] -name = "ipywidgets" -version = "8.1.3" -description = "Jupyter interactive widgets" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, - {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, -] - -[package.dependencies] -comm = ">=0.1.3" -ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.11,<3.1.0" -traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.11,<4.1.0" - -[package.extras] -test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] - -[[package]] -name = "isoduration" -version = "20.11.0" -description = "Operations with ISO 8601 durations" -optional = false -python-versions = ">=3.7" -files = [ - {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, - {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, -] - -[package.dependencies] -arrow = ">=0.15.0" - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.4" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "json5" -version = "0.9.25" -description = "A Python implementation of the JSON5 data format." -optional = false -python-versions = ">=3.8" -files = [ - {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, - {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.22.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} -rpds-py = ">=0.7.1" -uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-specifications" -version = "2023.12.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "jupyter" -version = "1.0.0" -description = "Jupyter metapackage. Install all the Jupyter components in one go." -optional = false -python-versions = "*" -files = [ - {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, - {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"}, - {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, -] - -[package.dependencies] -ipykernel = "*" -ipywidgets = "*" -jupyter-console = "*" -nbconvert = "*" -notebook = "*" -qtconsole = "*" - -[[package]] -name = "jupyter-client" -version = "8.6.2" -description = "Jupyter protocol implementation and client libraries" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = ">=5.3" - -[package.extras] -docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] - -[[package]] -name = "jupyter-console" -version = "6.6.3" -description = "Jupyter terminal console" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, - {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, -] - -[package.dependencies] -ipykernel = ">=6.14" -ipython = "*" -jupyter-client = ">=7.0.0" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -prompt-toolkit = ">=3.0.30" -pygments = "*" -pyzmq = ">=17" -traitlets = ">=5.4" - -[package.extras] -test = ["flaky", "pexpect", "pytest"] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -description = "Jupyter core package. A base package on which Jupyter projects rely." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, -] - -[package.dependencies] -platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = ">=5.3" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-events" -version = "0.10.0" -description = "Jupyter Event System library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, - {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, -] - -[package.dependencies] -jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} -python-json-logger = ">=2.0.4" -pyyaml = ">=5.3" -referencing = "*" -rfc3339-validator = "*" -rfc3986-validator = ">=0.1.1" -traitlets = ">=5.3" - -[package.extras] -cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] -test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] - -[[package]] -name = "jupyter-lsp" -version = "2.2.5" -description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, - {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-server = ">=1.1.2" - -[[package]] -name = "jupyter-server" -version = "2.14.1" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_server-2.14.1-py3-none-any.whl", hash = "sha256:16f7177c3a4ea8fe37784e2d31271981a812f0b2874af17339031dc3510cc2a5"}, - {file = "jupyter_server-2.14.1.tar.gz", hash = "sha256:12558d158ec7a0653bf96cc272bc7ad79e0127d503b982ed144399346694f726"}, -] - -[package.dependencies] -anyio = ">=3.1.0" -argon2-cffi = ">=21.1" -jinja2 = ">=3.0.3" -jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.9.0" -jupyter-server-terminals = ">=0.4.4" -nbconvert = ">=6.4.4" -nbformat = ">=5.3.0" -overrides = ">=5.0" -packaging = ">=22.0" -prometheus-client = ">=0.9" -pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} -pyzmq = ">=24" -send2trash = ">=1.8.2" -terminado = ">=0.8.3" -tornado = ">=6.2.0" -traitlets = ">=5.6.0" -websocket-client = ">=1.7" - -[package.extras] -docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] -test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] - -[[package]] -name = "jupyter-server-terminals" -version = "0.5.3" -description = "A Jupyter Server Extension Providing Terminals." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, - {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, -] - -[package.dependencies] -pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} -terminado = ">=0.8.3" - -[package.extras] -docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] -test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] - -[[package]] -name = "jupyterlab" -version = "4.2.2" -description = "JupyterLab computational environment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"}, - {file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"}, -] - -[package.dependencies] -async-lru = ">=1.0.0" -httpx = ">=0.25.0" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -ipykernel = ">=6.5.0" -jinja2 = ">=3.0.3" -jupyter-core = "*" -jupyter-lsp = ">=2.0.0" -jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2" -packaging = "*" -setuptools = ">=40.1.0" -tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} -tornado = ">=6.2.0" -traitlets = "*" - -[package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] -docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] -test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -description = "Pygments theme using JupyterLab CSS variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, - {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, -] - -[[package]] -name = "jupyterlab-server" -version = "2.27.2" -description = "A set of server components for JupyterLab and JupyterLab like applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_server-2.27.2-py3-none-any.whl", hash = "sha256:54aa2d64fd86383b5438d9f0c032f043c4d8c0264b8af9f60bd061157466ea43"}, - {file = "jupyterlab_server-2.27.2.tar.gz", hash = "sha256:15cbb349dc45e954e09bacf81b9f9bcb10815ff660fb2034ecd7417db3a7ea27"}, -] - -[package.dependencies] -babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.3" -json5 = ">=0.9.0" -jsonschema = ">=4.18.0" -jupyter-server = ">=1.21,<3" -packaging = ">=21.3" -requests = ">=2.31" - -[package.extras] -docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] -openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] -test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] - -[[package]] -name = "jupyterlab-widgets" -version = "3.0.11" -description = "Jupyter interactive widgets for JupyterLab" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, - {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, -] - [[package]] name = "langgraph-cli" -version = "0.1.48" +version = "0.1.52" description = "CLI for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" @@ -1117,11 +164,11 @@ click = "^8.1.7" [package.source] type = "directory" -url = "../../cli" +url = ".." [[package]] name = "langgraph-sdk" -version = "0.1.23" +version = "0.1.29" description = "SDK for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" @@ -1137,232 +184,6 @@ orjson = ">=3.10.1" type = "directory" url = "../../sdk-py" -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mistune" -version = "3.0.2" -description = "A sane and fast Markdown parser with useful plugins and renderers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, -] - -[[package]] -name = "nbclient" -version = "0.10.0" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, -] - -[package.dependencies] -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" -traitlets = ">=5.4" - -[package.extras] -dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.16.4" -description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "!=5.0.0" -defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<4" -nbclient = ">=0.5.0" -nbformat = ">=5.7" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.1" - -[package.extras] -all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["pyqtwebengine (>=5.15)"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] -webpdf = ["playwright"] - -[[package]] -name = "nbformat" -version = "5.10.4" -description = "The Jupyter Notebook format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, -] - -[package.dependencies] -fastjsonschema = ">=2.15" -jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -traitlets = ">=5.1" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "notebook" -version = "7.2.1" -description = "Jupyter Notebook - A web-based notebook environment for interactive computing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, - {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, -] - -[package.dependencies] -jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.2.0,<4.3" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2,<0.3" -tornado = ">=6.2.0" - -[package.extras] -dev = ["hatch", "pre-commit"] -docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] - -[[package]] -name = "notebook-shim" -version = "0.2.4" -description = "A shim layer for notebook traits and config" -optional = false -python-versions = ">=3.7" -files = [ - {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, - {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, -] - -[package.dependencies] -jupyter-server = ">=1.8,<3" - -[package.extras] -test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] - [[package]] name = "orjson" version = "3.10.5" @@ -1418,667 +239,6 @@ files = [ {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -description = "Utilities for writing pandoc filters in python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, - {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "prometheus-client" -version = "0.20.0" -description = "Python client for the Prometheus monitoring system." -optional = false -python-versions = ">=3.8" -files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, -] - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.47" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" -optional = false -python-versions = ">=3.6" -files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, -] - -[[package]] -name = "pywin32" -version = "306" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, -] - -[[package]] -name = "pywinpty" -version = "2.0.13" -description = "Pseudo terminal support for Windows from Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, - {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, - {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, - {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, - {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, - {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyzmq" -version = "26.0.3" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "qtconsole" -version = "5.5.2" -description = "Jupyter Qt console" -optional = false -python-versions = ">=3.8" -files = [ - {file = "qtconsole-5.5.2-py3-none-any.whl", hash = "sha256:42d745f3d05d36240244a04e1e1ec2a86d5d9b6edb16dbdef582ccb629e87e0b"}, - {file = "qtconsole-5.5.2.tar.gz", hash = "sha256:6b5fb11274b297463706af84dcbbd5c92273b1f619e6d25d08874b0a88516989"}, -] - -[package.dependencies] -ipykernel = ">=4.1" -jupyter-client = ">=4.1" -jupyter-core = "*" -packaging = "*" -pygments = "*" -pyzmq = ">=17.1" -qtpy = ">=2.4.0" -traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" - -[package.extras] -doc = ["Sphinx (>=1.3)"] -test = ["flaky", "pytest", "pytest-qt"] - -[[package]] -name = "qtpy" -version = "2.4.1" -description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." -optional = false -python-versions = ">=3.7" -files = [ - {file = "QtPy-2.4.1-py3-none-any.whl", hash = "sha256:1c1d8c4fa2c884ae742b069151b0abe15b3f70491f3972698c683b8e38de839b"}, - {file = "QtPy-2.4.1.tar.gz", hash = "sha256:a5a15ffd519550a1361bdc56ffc07fda56a6af7292f17c7b395d4083af632987"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] - -[[package]] -name = "referencing" -version = "0.35.1" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -description = "Pure python rfc3986 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, - {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, -] - -[[package]] -name = "rpds-py" -version = "0.18.1" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, - {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, - {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, - {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, - {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, - {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, - {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, - {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, - {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, - {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, - {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, - {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, - {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, -] - -[[package]] -name = "send2trash" -version = "1.8.3" -description = "Send file to trash natively under Mac OS X, Windows and Linux" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, - {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, -] - -[package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] - -[[package]] -name = "setuptools" -version = "70.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -2090,132 +250,6 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "terminado" -version = "0.18.1" -description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, - {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, -] - -[package.dependencies] -ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} -tornado = ">=6.1.0" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] -typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] - -[[package]] -name = "tinycss2" -version = "1.3.0" -description = "A tiny CSS parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, - {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["pytest", "ruff"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.4.1" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">=3.8" -files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20240316" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -2227,117 +261,7 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "uri-template" -version = "1.3.0" -description = "RFC 6570 URI Template Processor" -optional = false -python-versions = ">=3.7" -files = [ - {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, - {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, -] - -[package.extras] -dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "webcolors" -version = "24.6.0" -description = "A library for working with the color formats defined by HTML and CSS." -optional = false -python-versions = ">=3.8" -files = [ - {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, - {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, -] - -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["coverage[toml]"] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "widgetsnbextension" -version = "4.0.11" -description = "Jupyter interactive widgets for Jupyter Notebook" -optional = false -python-versions = ">=3.7" -files = [ - {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, - {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, -] - -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "47f805bc137b65d4f0c807763eefcb24af09c1dccda03917a23a05bef00d7341" +content-hash = "ec5109729f30d2033a10a10e8f8d3ed94c7d96d5d31025b4815b0123664bb063" diff --git a/libs/cli/examples/pyproject.toml b/libs/cli/examples/pyproject.toml index 5399e9184..c4d15111c 100644 --- a/libs/cli/examples/pyproject.toml +++ b/libs/cli/examples/pyproject.toml @@ -9,7 +9,6 @@ package-mode = false [tool.poetry.dependencies] python = "^3.9.0,<4.0" -jupyter = "^1.0.0" langgraph-cli = {path = "../../cli", develop = true} langgraph-sdk = {path = "../../sdk-py", develop = true} diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 1e3ffa2a7..ebd7e0701 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -372,7 +372,11 @@ def _build( pull: bool, tag: str, ): - base_image = base_image or "langchain/langgraph-api" + base_image = base_image or ( + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api" + ) # pull latest images if pull: @@ -380,7 +384,9 @@ def _build( subp_exec( "docker", "pull", - f"{base_image}:{config_json['python_version']}", + f"{base_image}:{config_json['node_version']}" + if config_json.get("node_version") + else f"{base_image}:{config_json['python_version']}", verbose=True, ) ) @@ -462,7 +468,11 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path): with open(save_path, "w") as f: f.write( langgraph_cli.config.config_to_docker( - config, config_json, "langchain/langgraph-api" + config, + config_json, + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api", ) ) @@ -500,7 +510,9 @@ def prepare_args_and_stdin( config_path, config, watch=watch, - base_image="langchain/langgraph-api", + base_image="langchain/langgraphjs-api" + if config.get("node_version") + else "langchain/langgraph-api", ) return args, stdin @@ -527,7 +539,9 @@ def prepare( subp_exec( "docker", "pull", - f"langchain/langgraph-api:{config['python_version']}", + f"langchain/langgraphjs-api:{config['node_version']}" + if config.get("node_version") + else f"langchain/langgraph-api:{config['python_version']}", verbose=verbose, ) ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 177125c84..49c1102b4 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -9,6 +9,7 @@ import click class Config(TypedDict): python_version: str + node_version: Optional[str] pip_config_file: Optional[str] dockerfile_lines: list[str] dependencies: list[str] @@ -17,27 +18,46 @@ class Config(TypedDict): def validate_config(config: Config) -> Config: - config = { - "python_version": config.get("python_version", "3.11"), - "pip_config_file": config.get("pip_config_file"), - "dockerfile_lines": config.get("dockerfile_lines", []), - "dependencies": config.get("dependencies", []), - "graphs": config.get("graphs", {}), - "env": config.get("env", {}), - } - if config["python_version"] not in ( - "3.11", - "3.12", - ): - raise click.UsageError( - f"Unsupported Python version: {config['python_version']}. " - "Supported versions are 3.11 and 3.12." - ) - if not config["dependencies"]: - raise click.UsageError( - "No dependencies found in config. " - "Add at least one dependency to 'dependencies' list." - ) + config = ( + { + "node_version": config.get("node_version"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + if config.get("node_version") + else { + "python_version": config.get("python_version", "3.11"), + "pip_config_file": config.get("pip_config_file"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "dependencies": config.get("dependencies", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + ) + + if config.get("node_version"): + if config["node_version"] not in ("20",): + raise click.UsageError( + f"Unsupported Node.js version: {config['node_version']}. " + "Currently only `node_version: \"20\"` is supported." + ) + + if config.get("python_version"): + if config["python_version"] not in ( + "3.11", + "3.12", + ): + raise click.UsageError( + f"Unsupported Python version: {config['python_version']}. " + "Supported versions are 3.11 and 3.12." + ) + if not config["dependencies"]: + raise click.UsageError( + "No dependencies found in config. " + "Add at least one dependency to 'dependencies' list." + ) + if not config["graphs"]: raise click.UsageError( "No graphs found in config. " @@ -191,7 +211,7 @@ def _update_graph_paths( config["graphs"][graph_id] = f"{module_str}:{attr_str}" -def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): +def python_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): # configure pip pip_install = ( "PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt" @@ -266,6 +286,29 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' {f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}""" +def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + faux_path = f"/deps/{config_path.parent.name}" + + return f"""FROM {base_image}:{config['node_version']} + +{os.linesep.join(config["dockerfile_lines"])} + +ADD . {faux_path} + +RUN cd {faux_path} && yarn install --frozen-lockfile + +ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' + +WORKDIR {faux_path}""" + + +def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + if config.get("node_version"): + return node_config_to_docker(config_path, config, base_image) + + return python_config_to_docker(config_path, config, base_image) + + def config_to_compose( config_path: pathlib.Path, config: Config, diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 79e0dd25a..f28274f4d 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -12,6 +12,15 @@ DEFAULT_POSTGRES_URI = ( "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable" ) +REDIS = """ + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 +""" DB = """ langgraph-postgres: @@ -165,18 +174,22 @@ def compose( interval: 5s""" compose_str = f"""{volumes}services: +{REDIS} {db} {debugger_compose(port=debugger_port, base_url=debugger_base_url)} langgraph-api: ports: - - "{port}:8000\"""" + - "{port}:8000\" + depends_on: + langgraph-redis: + condition: service_healthy""" if include_db: compose_str += """ - depends_on: langgraph-postgres: condition: service_healthy""" compose_str += f""" environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {postgres_uri} """ if capabilities.healthcheck_start_interval: diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock index 4f6c66396..101f65ea6 100644 --- a/libs/cli/poetry.lock +++ b/libs/cli/poetry.lock @@ -236,28 +236,29 @@ watchdog = ">=0.6.0" [[package]] name = "ruff" -version = "0.1.6" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -324,4 +325,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "5efa2f1ed4bd611a45e5d43d7c3fb907a8fa4447e2d1c30ce26b830411e189dd" +content-hash = "a1b0cc1de3e63b8d419342e311cd1e32b81606e89d3f8cf448be560b5727c202" diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 508571f06..20fe010d9 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-cli" -version = "0.1.50" +version = "0.1.52" description = "CLI for interacting with LangGraph API" authors = [] license = "MIT" @@ -16,7 +16,7 @@ python = "^3.9.0,<4.0" click = "^8.1.7" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" @@ -40,7 +40,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff] -select = [ +lint.select = [ # pycodestyle "E", # Pyflakes @@ -52,4 +52,4 @@ select = [ # isort "I", ] -ignore = [ "E501", "B008" ] +lint.ignore = [ "E501", "B008" ] diff --git a/libs/cli/tests/unit_tests/test_cli.py b/libs/cli/tests/unit_tests/test_cli.py index da9354c07..a4972f393 100644 --- a/libs/cli/tests/unit_tests/test_cli.py +++ b/libs/cli/tests/unit_tests/test_cli.py @@ -45,6 +45,13 @@ def test_prepare_args_and_stdin(): langgraph-data: driver: local services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-postgres: image: postgres:16 ports: @@ -76,9 +83,12 @@ services: ports: - "8000:8000" depends_on: + langgraph-redis: + condition: service_healthy langgraph-postgres: condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI} healthcheck: test: python /api/healthcheck.py diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 870f43faf..f53313986 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -232,6 +232,31 @@ ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin +# node.js build used for LangGraph Cloud +def test_config_to_docker_nodejs(): + graphs = {"agent": "./graphs/agent.js:graph"} + actual_docker_stdin = config_to_docker( + PATH_TO_CONFIG, + validate_config( + { + "node_version": "20", + "graphs": graphs, + "dockerfile_lines": ["ARG meow", "ARG foo"], + } + ), + "langchain/langgraphjs-api", + ) + expected_docker_stdin = """FROM langchain/langgraphjs-api:20 +ARG meow +ARG foo +ADD . /deps/unit_tests +RUN cd /deps/unit_tests && yarn install --frozen-lockfile +ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}' +WORKDIR /deps/unit_tests""" + + assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + + # config_to_compose def test_config_to_compose_simple_config(): graphs = {"agent": "./agent.py:graph"} diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py index 4d5e09b94..781c049c1 100644 --- a/libs/cli/tests/unit_tests/test_docker.py +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -20,10 +20,21 @@ def test_compose_with_no_debugger_and_custom_db(): DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str @@ -37,10 +48,21 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck(): postgres_uri=custom_postgres_uri, ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri} healthcheck: test: python /api/healthcheck.py @@ -59,10 +81,21 @@ def test_compose_with_debugger_and_custom_db(): postgres_uri=custom_postgres_uri, ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str @@ -74,6 +107,13 @@ def test_compose_with_debugger_and_default_db(): langgraph-data: driver: local services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-postgres: image: postgres:16 ports: @@ -94,8 +134,11 @@ services: ports: - "{port}:8000" depends_on: + langgraph-redis: + condition: service_healthy langgraph-postgres: condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile index fd513f65f..d5c6d7ee3 100644 --- a/libs/langgraph/Makefile +++ b/libs/langgraph/Makefile @@ -15,21 +15,21 @@ coverage: --cov-report term-missing:skip-covered start-postgres: - docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait + docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans stop-postgres: - docker compose -f tests/compose-postgres.yml down + docker compose -f tests/compose-postgres.yml down -v + +TEST_PATH ?= . test: - make start-postgres; \ - poetry run pytest; \ + make start-postgres && poetry run pytest $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE test_watch: - make start-postgres; \ - poetry run ptw .; \ + make start-postgres && poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE @@ -51,14 +51,14 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) spell_check: poetry run codespell --toml pyproject.toml diff --git a/libs/langgraph/README.md b/libs/langgraph/README.md index 87d19961f..c3da88ec4 100644 --- a/libs/langgraph/README.md +++ b/libs/langgraph/README.md @@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage from langchain_anthropic import ChatAnthropic from langchain_core.tools import tool from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import END, StateGraph, MessagesState +from langgraph.graph import END, START, StateGraph, MessagesState from langgraph.prebuilt import ToolNode @@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") # We now add a conditional edge workflow.add_conditional_edges( diff --git a/libs/langgraph/langgraph/channels/any_value.py b/libs/langgraph/langgraph/channels/any_value.py index 345daa7c7..e9bd85572 100644 --- a/libs/langgraph/langgraph/channels/any_value.py +++ b/libs/langgraph/langgraph/channels/any_value.py @@ -28,12 +28,6 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index fe47f0d8f..955b6ab76 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -2,9 +2,9 @@ from abc import ABC, abstractmethod from contextlib import asynccontextmanager, contextmanager from typing import ( Any, - AsyncGenerator, - Generator, + AsyncIterator, Generic, + Iterator, Optional, Sequence, TypeVar, @@ -21,6 +21,8 @@ C = TypeVar("C") class BaseChannel(Generic[Value, Update, C], ABC): + key: str = "" + @property @abstractmethod def ValueType(self) -> Any: @@ -33,29 +35,45 @@ class BaseChannel(Generic[Value, Update, C], ABC): # serialize/deserialize methods - @abstractmethod def checkpoint(self) -> Optional[C]: """Return a serializable representation of the channel's current state. Raises EmptyChannelError if the channel is empty (never updated yet), or doesn't support checkpoints.""" + return self.get() @contextmanager @abstractmethod def from_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> Generator[Self, None, None]: + ) -> Iterator[Self]: """Return a new identical channel, optionally initialized from a checkpoint. If the checkpoint contains complex data structures, they should be copied.""" + @contextmanager + def from_checkpoint_named( + self, checkpoint: Optional[C], config: RunnableConfig + ) -> Iterator[Self]: + with self.from_checkpoint(checkpoint, config) as value: + value.key = self.key + yield value + @asynccontextmanager async def afrom_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> AsyncGenerator[Self, None]: + ) -> AsyncIterator[Self]: """Return a new identical channel, optionally initialized from a checkpoint. If the checkpoint contains complex data structures, they should be copied.""" with self.from_checkpoint(checkpoint, config) as value: yield value + @asynccontextmanager + async def afrom_checkpoint_named( + self, checkpoint: Optional[C], config: RunnableConfig + ) -> AsyncIterator[Self]: + async with self.afrom_checkpoint(checkpoint, config) as value: + value.key = self.key + yield value + # state methods @abstractmethod diff --git a/libs/langgraph/langgraph/channels/binop.py b/libs/langgraph/langgraph/channels/binop.py index 72e76c0a7..02e33bd2e 100644 --- a/libs/langgraph/langgraph/channels/binop.py +++ b/libs/langgraph/langgraph/channels/binop.py @@ -73,12 +73,6 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/context.py b/libs/langgraph/langgraph/channels/context.py index 914de9348..3b4e26805 100644 --- a/libs/langgraph/langgraph/channels/context.py +++ b/libs/langgraph/langgraph/channels/context.py @@ -1,122 +1,5 @@ -from contextlib import asynccontextmanager, contextmanager -from inspect import signature -from typing import ( - Any, - AsyncContextManager, - AsyncGenerator, - ContextManager, - Generator, - Generic, - Optional, - Sequence, - Type, - Union, -) +from langgraph.managed.context import Context as ContextManagedValue -from langchain_core.runnables import RunnableConfig -from typing_extensions import Self +Context = ContextManagedValue.of -from langgraph.channels.base import BaseChannel, Value -from langgraph.errors import EmptyChannelError, InvalidUpdateError - - -class Context(Generic[Value], BaseChannel[Value, None, None]): - """Exposes the value of a context manager, for the duration of an invocation. - Context manager is entered before the first step, and exited after the last step. - Optionally, provide an equivalent async context manager, which will be used - instead for async invocations. - - ```python - import httpx - - client = Channels.Context(httpx.Client, httpx.AsyncClient) - ``` - """ - - value: Value - - def __init__( - self, - ctx: Union[ - None, Type[ContextManager[Value]], Type[AsyncContextManager[Value]] - ] = None, - actx: Optional[Type[AsyncContextManager[Value]]] = None, - ) -> None: - if ctx is None and actx is None: - raise ValueError("Must provide either sync or async context manager.") - self.ctx = ctx - self.actx = actx - - def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Context) - and value.ctx == self.ctx - and value.actx == self.actx - ) - - @property - def ValueType(self) -> Any: - """The type of the value stored in the channel.""" - return None - - @property - def UpdateType(self) -> Type[None]: - """The type of the update received by the channel.""" - return None - - def checkpoint(self) -> None: - raise EmptyChannelError() - - @contextmanager - def from_checkpoint( - self, checkpoint: None, config: RunnableConfig - ) -> Generator[Self, None, None]: - if self.ctx is None: - raise ValueError("Cannot enter sync context manager.") - - empty = self.__class__(ctx=self.ctx, actx=self.actx) - ctx = ( - self.ctx(config) - if signature(self.ctx).parameters.get("config") - else self.ctx() - ) - with ctx as value: - empty.value = value - yield empty - - @asynccontextmanager - async def afrom_checkpoint( - self, checkpoint: None, config: RunnableConfig - ) -> AsyncGenerator[Self, None]: - empty = self.__class__(ctx=self.ctx, actx=self.actx) - if self.actx is not None: - ctx = ( - self.actx(config) - if signature(self.actx).parameters.get("config") - else self.actx() - ) - else: - ctx = ( - self.ctx(config) - if signature(self.ctx).parameters.get("config") - else self.ctx() - ) - if hasattr(ctx, "__aenter__"): - async with ctx as value: - empty.value = value - yield empty - else: - with ctx as value: - empty.value = value - yield empty - - def update(self, values: Sequence[None]) -> bool: - if values: - raise InvalidUpdateError("Context channel does not accept writes.") - return False - - def get(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() +__all__ = ["Context"] diff --git a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py index bb0d447fa..64406b8f8 100644 --- a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py +++ b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py @@ -69,7 +69,7 @@ class DynamicBarrierValue( if wait_for_names := [v for v in values if isinstance(v, WaitForNames)]: if len(wait_for_names) > 1: raise InvalidUpdateError( - "Received multiple WaitForNames updates in the same step." + f"At key '{self.key}': Received multiple WaitForNames updates in the same step." ) self.names = wait_for_names[0].names return True diff --git a/libs/langgraph/langgraph/channels/ephemeral_value.py b/libs/langgraph/langgraph/channels/ephemeral_value.py index 15e11550d..59e34a5f0 100644 --- a/libs/langgraph/langgraph/channels/ephemeral_value.py +++ b/libs/langgraph/langgraph/channels/ephemeral_value.py @@ -28,12 +28,6 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig @@ -58,7 +52,7 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): return False if len(values) != 1 and self.guard: raise InvalidUpdateError( - "EphemeralValue can only receive one value per step." + f"At key '{self.key}': EphemeralValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values." ) self.value = values[-1] diff --git a/libs/langgraph/langgraph/channels/last_value.py b/libs/langgraph/langgraph/channels/last_value.py index a207ebce3..e5e59d111 100644 --- a/libs/langgraph/langgraph/channels/last_value.py +++ b/libs/langgraph/langgraph/channels/last_value.py @@ -27,12 +27,6 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig @@ -52,7 +46,9 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): if len(values) == 0: return False if len(values) != 1: - raise InvalidUpdateError("LastValue can only receive one value per step.") + raise InvalidUpdateError( + f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values." + ) self.value = values[-1] return True diff --git a/libs/langgraph/langgraph/channels/manager.py b/libs/langgraph/langgraph/channels/manager.py deleted file mode 100644 index 9a7c8d87e..000000000 --- a/libs/langgraph/langgraph/channels/manager.py +++ /dev/null @@ -1,39 +0,0 @@ -from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager -from typing import AsyncGenerator, Generator, Mapping - -from langchain_core.runnables import RunnableConfig - -from langgraph.channels.base import BaseChannel -from langgraph.checkpoint.base import Checkpoint - - -@contextmanager -def ChannelsManager( - channels: Mapping[str, BaseChannel], - checkpoint: Checkpoint, - config: RunnableConfig, -) -> Generator[Mapping[str, BaseChannel], None, None]: - """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - with ExitStack() as stack: - yield { - k: stack.enter_context( - v.from_checkpoint(checkpoint["channel_values"].get(k), config) - ) - for k, v in channels.items() - } - - -@asynccontextmanager -async def AsyncChannelsManager( - channels: Mapping[str, BaseChannel], - checkpoint: Checkpoint, - config: RunnableConfig, -) -> AsyncGenerator[Mapping[str, BaseChannel], None]: - """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - async with AsyncExitStack() as stack: - yield { - k: await stack.enter_async_context( - v.afrom_checkpoint(checkpoint["channel_values"].get(k), config) - ) - for k, v in channels.items() - } diff --git a/libs/langgraph/langgraph/channels/named_barrier_value.py b/libs/langgraph/langgraph/channels/named_barrier_value.py index bdfd4660b..023f54e6c 100644 --- a/libs/langgraph/langgraph/channels/named_barrier_value.py +++ b/libs/langgraph/langgraph/channels/named_barrier_value.py @@ -53,7 +53,9 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]): self.seen.add(value) updated = True else: - raise InvalidUpdateError(f"Value {value} not in {self.names}") + raise InvalidUpdateError( + f"At key '{self.key}': Value {value} not in {self.names}" + ) return updated def get(self) -> Value: diff --git a/libs/langgraph/langgraph/channels/topic.py b/libs/langgraph/langgraph/channels/topic.py index 5e7af6a26..7b4b0b27d 100644 --- a/libs/langgraph/langgraph/channels/topic.py +++ b/libs/langgraph/langgraph/channels/topic.py @@ -30,23 +30,15 @@ class Topic( accumulate: Whether to accumulate values across steps. If False, the channel will be emptied after each step. """ - def __init__( - self, typ: Type[Value], unique: bool = False, accumulate: bool = False - ) -> None: + def __init__(self, typ: Type[Value], accumulate: bool = False) -> None: # attrs self.typ = typ - self.unique = unique self.accumulate = accumulate # state - self.seen = set[Value]() self.values = list[Value]() def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Topic) - and value.unique == self.unique - and value.accumulate == self.accumulate - ) + return isinstance(value, Topic) and value.accumulate == self.accumulate @property def ValueType(self) -> Any: @@ -59,18 +51,20 @@ class Topic( return Union[self.typ, list[self.typ]] # type: ignore[name-defined] def checkpoint(self) -> tuple[set[Value], list[Value]]: - return (self.seen, self.values) + return self.values @contextmanager def from_checkpoint( self, - checkpoint: Optional[tuple[set[Value], list[Value]]], + checkpoint: Optional[list[Value]], config: RunnableConfig, ) -> Generator[Self, None, None]: - empty = self.__class__(self.typ, self.unique, self.accumulate) + empty = self.__class__(self.typ, self.accumulate) if checkpoint is not None: - empty.seen = checkpoint[0].copy() - empty.values = checkpoint[1].copy() + if isinstance(checkpoint, tuple): + empty.values = checkpoint[1].copy() + else: + empty.values = checkpoint.copy() try: yield empty finally: @@ -81,13 +75,7 @@ class Topic( if not self.accumulate: self.values = list[Value]() if flat_values := flatten(values): - if self.unique: - for value in flat_values: - if value not in self.seen: - self.seen.add(value) - self.values.append(value) - else: - self.values.extend(flat_values) + self.values.extend(flat_values) return self.values != current def get(self) -> Sequence[Value]: diff --git a/libs/langgraph/langgraph/channels/untracked_value.py b/libs/langgraph/langgraph/channels/untracked_value.py index 989bba35e..a112b0e81 100644 --- a/libs/langgraph/langgraph/channels/untracked_value.py +++ b/libs/langgraph/langgraph/channels/untracked_value.py @@ -49,7 +49,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]): return False if len(values) != 1 and self.guard: raise InvalidUpdateError( - "UntrackedValue can only receive one value per step." + f"At key '{self.key}': UntrackedValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values." ) self.value = values[-1] diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index af4ba4d8d..f19c562f5 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,29 +1,45 @@ -from typing import Any +from dataclasses import dataclass +from typing import Any, Literal INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" +CONFIG_KEY_STREAM = "__pregel_stream" +CONFIG_KEY_STORE = "__pregel_store" CONFIG_KEY_RESUMING = "__pregel_resuming" +CONFIG_KEY_TASK_ID = "__pregel_task_id" +# this one part of public API so more readable +CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" INTERRUPT = "__interrupt__" ERROR = "__error__" +SCHEDULED = "__scheduled__" TASKS = "__pregel_tasks" +SUBSCRIPTIONS = "__pregel_subscriptions" +RUNTIME_PLACEHOLDER = "__pregel_runtime_placeholder__" RESERVED = { + SCHEDULED, INTERRUPT, ERROR, TASKS, + SUBSCRIPTIONS, CONFIG_KEY_SEND, CONFIG_KEY_READ, CONFIG_KEY_CHECKPOINTER, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_STORE, CONFIG_KEY_RESUMING, + CONFIG_KEY_TASK_ID, INPUT, + RUNTIME_PLACEHOLDER, } TAG_HIDDEN = "langsmith:hidden" START = "__start__" END = "__end__" -CHECKPOINT_NAMESPACE_SEPARATOR = "|" +NS_SEP = "|" +NS_END = ":" class Send: @@ -93,3 +109,9 @@ class Send: and self.node == value.node and self.arg == value.arg ) + + +@dataclass +class Interrupt: + value: Any + when: Literal["during"] = "during" diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py index ea54cc55f..aa5b57857 100644 --- a/libs/langgraph/langgraph/errors.py +++ b/libs/langgraph/langgraph/errors.py @@ -1,4 +1,7 @@ +from typing import Any, Sequence + from langgraph.checkpoint.base import EmptyChannelError +from langgraph.constants import Interrupt class GraphRecursionError(RecursionError): @@ -29,7 +32,15 @@ class InvalidUpdateError(Exception): class GraphInterrupt(Exception): """Raised when a subgraph is interrupted.""" - pass + def __init__(self, interrupts: Sequence[Interrupt] = ()) -> None: + super().__init__(interrupts) + + +class NodeInterrupt(GraphInterrupt): + """Raised by a node to interrupt execution.""" + + def __init__(self, value: Any) -> None: + super().__init__([Interrupt(value)]) class EmptyInputError(Exception): @@ -38,10 +49,17 @@ class EmptyInputError(Exception): pass +class TaskNotFound(Exception): + """Raised when the executor is unable to find a task.""" + + pass + + __all__ = [ "GraphRecursionError", "InvalidUpdateError", "GraphInterrupt", + "NodeInterrupt", "EmptyInputError", "EmptyChannelError", ] diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index dc927446d..b04594bdd 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -1,3 +1,4 @@ +import asyncio import logging from collections import defaultdict from typing import ( @@ -26,8 +27,9 @@ from langchain_core.runnables.graph import Node as DrawableNode from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, END, + NS_END, + NS_SEP, START, TAG_HIDDEN, Send, @@ -37,7 +39,7 @@ from langgraph.pregel import Channel, Pregel from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry -from langgraph.utils import RunnableCallable, coerce_to_runnable +from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable logger = logging.getLogger(__name__) @@ -54,7 +56,7 @@ class Branch(NamedTuple): def run( self, - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], None], reader: Optional[Callable[[RunnableConfig], Any]] = None, ) -> None: return ChannelWrite.register_writer( @@ -74,7 +76,7 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Optional[Callable[[], Any]], - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], None], ) -> Runnable: if reader: value = reader(config) @@ -85,7 +87,7 @@ class Branch(NamedTuple): else: value = input result = self.path.invoke(value, config) - return self._finish(writer, input, result) + return self._finish(writer, input, result, config) async def _aroute( self, @@ -93,10 +95,10 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Optional[Callable[[], Any]], - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], Optional[Runnable]], ) -> Runnable: if reader: - value = reader(config) + value = await asyncio.to_thread(reader, config) # passthrough additional keys from node to branch # only doable when using dict states if isinstance(value, dict) and isinstance(input, dict): @@ -104,10 +106,14 @@ class Branch(NamedTuple): else: value = input result = await self.path.ainvoke(value, config) - return self._finish(writer, input, result) + return self._finish(writer, input, result, config) def _finish( - self, writer: Callable[[list[str]], Optional[Runnable]], input: Any, result: Any + self, + writer: Callable[[list[str], RunnableConfig], None], + input: Any, + result: Any, + config: RunnableConfig, ): if not isinstance(result, list): result = [result] @@ -119,7 +125,7 @@ class Branch(NamedTuple): raise ValueError("Branch did not return a valid destination") if any(p.node == END for p in destinations if isinstance(p, Send)): raise InvalidUpdateError("Cannot send a packet to the END node") - return writer(destinations) or input + return writer(destinations, config) or input class Graph: @@ -140,8 +146,7 @@ class Graph: node: RunnableLike, *, metadata: Optional[dict[str, Any]] = None, - ) -> None: - ... + ) -> None: ... @overload def add_node( @@ -150,8 +155,7 @@ class Graph: action: RunnableLike, *, metadata: Optional[dict[str, Any]] = None, - ) -> None: - ... + ) -> None: ... def add_node( self, @@ -160,10 +164,12 @@ class Graph: *, metadata: Optional[dict[str, Any]] = None, ) -> None: - if isinstance(node, str) and CHECKPOINT_NAMESPACE_SEPARATOR in node: - raise ValueError( - f"'{CHECKPOINT_NAMESPACE_SEPARATOR}' is a reserved character and is not allowed in the node names." - ) + if isinstance(node, str): + for character in (NS_SEP, NS_END): + if character in node: + raise ValueError( + f"'{character}' is a reserved character and is not allowed in the node names." + ) if self.compiled: logger.warning( @@ -192,12 +198,14 @@ class Graph: raise ValueError("END cannot be a start node") if end_key == START: raise ValueError("START cannot be an end node") - if not self.support_multiple_edges and start_key in set( + + # run this validation only for non-StateGraph graphs + if not hasattr(self, "channels") and start_key in set( start for start, _ in self.edges ): raise ValueError( f"Already found path for node '{start_key}'.\n" - "For multiple edges, use StateGraph with an annotated state key." + "For multiple edges, use StateGraph with an Annotated state key." ) self.edges.add((start_key, end_key)) @@ -421,6 +429,10 @@ class Graph: class CompiledGraph(Pregel): builder: Graph + def __init__(self, *, builder: Graph, **kwargs): + super().__init__(**kwargs) + self.builder = builder + def attach_node(self, key: str, node: NodeSpec) -> None: self.channels[key] = EphemeralValue(Any) self.nodes[key] = ( @@ -442,7 +454,9 @@ class CompiledGraph(Pregel): self.nodes[end].channels.append(start) def attach_branch(self, start: str, name: str, branch: Branch) -> None: - def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]: + def branch_writer( + packets: list[Union[str, Send]], config: RunnableConfig + ) -> Optional[ChannelWrite]: writes = [ ( ChannelWriteEntry(f"branch:{start}:{name}:{p}" if p != END else END) @@ -481,6 +495,10 @@ class CompiledGraph(Pregel): START: graph.add_node(self.get_input_schema(config), START) } end_nodes: dict[str, DrawableNode] = {} + if xray: + subgraphs = dict(self.get_subgraphs()) + else: + subgraphs = {} def add_edge( start: str, end: str, label: Optional[str] = None, conditional: bool = False @@ -494,17 +512,19 @@ class CompiledGraph(Pregel): for key, n in self.builder.nodes.items(): node = n.runnable metadata = n.metadata or {} - if key in self.interrupt_before_nodes: + if key in self.interrupt_before_nodes and key in self.interrupt_after_nodes: + metadata["__interrupt"] = "before,after" + elif key in self.interrupt_before_nodes: metadata["__interrupt"] = "before" elif key in self.interrupt_after_nodes: metadata["__interrupt"] = "after" if xray: subgraph = ( - node.get_graph( + subgraphs[key].get_graph( config=config, xray=xray - 1 if isinstance(xray, int) and xray > 0 else xray, ) - if isinstance(node, CompiledGraph) + if key in subgraphs else node.get_graph(config=config) ) subgraph.trim_first_node() diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 5810aaf29..baec31cdd 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -1,3 +1,4 @@ +import inspect import logging import typing import warnings @@ -5,6 +6,7 @@ from functools import partial from inspect import isclass, isfunction, signature from typing import ( Any, + Callable, NamedTuple, Optional, Sequence, @@ -15,36 +17,36 @@ from typing import ( overload, ) -from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable, RunnableConfig from langchain_core.runnables.base import RunnableLike -from langchain_core.runnables.utils import ( - create_model, -) +from langchain_core.runnables.utils import create_model +from pydantic import BaseModel +from pydantic.v1 import BaseModel as BaseModelV1 from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate -from langgraph.channels.context import Context from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitForNames from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.channels.named_barrier_value import NamedBarrierValue from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR, TAG_HIDDEN +from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN from langgraph.errors import InvalidUpdateError -from langgraph.graph.graph import ( - END, - START, - Branch, - CompiledGraph, - Graph, - Send, +from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send +from langgraph.managed.base import ( + ChannelKeyPlaceholder, + ChannelTypePlaceholder, + ConfiguredManagedValue, + ManagedValueSpec, + is_managed_value, + is_writable_managed_value, ) -from langgraph.managed.base import ManagedValue, is_managed_value from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry -from langgraph.utils import RunnableCallable, coerce_to_runnable +from langgraph.store.base import BaseStore +from langgraph.utils.fields import get_field_default +from langgraph.utils.runnable import coerce_to_runnable logger = logging.getLogger(__name__) @@ -121,8 +123,8 @@ class StateGraph(Graph): nodes: dict[str, StateNodeSpec] channels: dict[str, BaseChannel] - managed: dict[str, Type[ManagedValue]] - schemas: dict[Type[Any], dict[str, Union[BaseChannel, Type[ManagedValue]]]] + managed: dict[str, ManagedValueSpec] + schemas: dict[Type[Any], dict[str, Union[BaseChannel, ManagedValueSpec]]] def __init__( self, @@ -184,10 +186,6 @@ class StateGraph(Graph): ) else: self.managed[key] = managed - if any( - isinstance(c, BinaryOperatorAggregate) for c in self.channels.values() - ): - self.support_multiple_edges = True @overload def add_node( @@ -312,20 +310,24 @@ class StateGraph(Graph): if node == END or node == START: raise ValueError(f"Node `{node}` is reserved.") - if CHECKPOINT_NAMESPACE_SEPARATOR in node: - raise ValueError( - f"'{CHECKPOINT_NAMESPACE_SEPARATOR}' is a reserved character and is not allowed in the node names." - ) + for character in (NS_SEP, NS_END): + if character in node: + raise ValueError( + f"'{character}' is a reserved character and is not allowed in the node names." + ) try: if isfunction(action) and ( hints := get_type_hints(action.__call__) or get_type_hints(action) ): if input is None: - input_hint = hints[list(hints.keys())[0]] - if isinstance(input_hint, type) and get_type_hints(input_hint): - input = input_hint - except TypeError: + first_parameter_name = next( + iter(inspect.signature(action).parameters.keys()) + ) + if input_hint := hints.get(first_parameter_name): + if isinstance(input_hint, type) and get_type_hints(input_hint): + input = input_hint + except (TypeError, StopIteration): pass if input is not None: self._add_schema(input) @@ -366,7 +368,7 @@ class StateGraph(Graph): raise ValueError(f"Need to add_node `{start}` first") if end_key == START: raise ValueError("START cannot be an end node") - if end_key not in self.nodes: + if end_key != END and end_key not in self.nodes: raise ValueError(f"Need to add_node `{end_key}` first") self.waiting_edges.add((tuple(start_key), end_key)) @@ -374,6 +376,8 @@ class StateGraph(Graph): def compile( self, checkpointer: Optional[BaseCheckpointSaver] = None, + *, + store: Optional[BaseStore] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, @@ -415,16 +419,14 @@ class StateGraph(Graph): else [ key for key, val in self.schemas[self.output].items() - if not isinstance(val, Context) and not is_managed_value(val) + if not is_managed_value(val) ] ) stream_channels = ( "__root__" if len(self.channels) == 1 and "__root__" in self.channels else [ - key - for key, val in self.channels.items() - if not isinstance(val, Context) and not is_managed_value(val) + key for key, val in self.channels.items() if not is_managed_value(val) ] ) @@ -432,7 +434,11 @@ class StateGraph(Graph): builder=self, config_type=self.config_schema, nodes={}, - channels={**self.channels, START: EphemeralValue(self.input)}, + channels={ + **self.channels, + **self.managed, + START: EphemeralValue(self.input), + }, input_channels=START, stream_mode="updates", output_channels=output_channels, @@ -442,6 +448,7 @@ class StateGraph(Graph): interrupt_after_nodes=interrupt_after, auto_validate=False, debug=debug, + store=store, ) compiled.attach_node(START, None) @@ -467,10 +474,8 @@ class CompiledStateGraph(CompiledGraph): def get_input_schema( self, config: Optional[RunnableConfig] = None ) -> type[BaseModel]: - from pydantic import BaseModel as BaseModelP - if isclass(self.builder.input) and issubclass( - self.builder.input, (BaseModel, BaseModelP) + self.builder.input, (BaseModel, BaseModelV1) ): return self.builder.input else: @@ -484,20 +489,26 @@ class CompiledStateGraph(CompiledGraph): return create_model( # type: ignore[call-overload] self.get_name("Input"), **{ - k: (self.channels[k].UpdateType, None) + k: ( + self.channels[k].UpdateType, + ( + get_field_default( + k, + self.channels[k].UpdateType, + self.builder.input, + ) + ), + ) for k in self.builder.schemas[self.builder.input] - if k in self.channels - and not isinstance(self.channels[k], Context) + if isinstance(self.channels[k], BaseChannel) }, ) def get_output_schema( self, config: Optional[RunnableConfig] = None ) -> type[BaseModel]: - from pydantic import BaseModel as BaseModelP - - if isclass(self.builder.input) and issubclass( - self.builder.output, (BaseModel, BaseModelP) + if isclass(self.builder.output) and issubclass( + self.builder.output, (BaseModel, BaseModelV1) ): return self.builder.output @@ -508,14 +519,16 @@ class CompiledStateGraph(CompiledGraph): output_keys = [ k for k, v in self.builder.schemas[self.builder.input].items() - if not isinstance(v, Context) and not is_managed_value(v) + if not is_managed_value(v) ] else: - output_keys = list(self.builder.channels) + output_keys = list(self.builder.channels) + [ + k + for k, v in self.builder.managed.items() + if is_writable_managed_value(v) + ] - def _get_state_key( - input: Union[None, dict, Any], config: RunnableConfig, *, key: str - ) -> Any: + def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any: if input is None: return SKIP_WRITE elif isinstance(input, dict): @@ -531,12 +544,7 @@ class CompiledStateGraph(CompiledGraph): [ChannelWriteEntry("__root__", skip_none=True)] if output_keys == ["__root__"] else [ - ChannelWriteEntry( - key, - mapper=RunnableCallable( - _get_state_key, key=key, trace=False, recurse=False - ), - ) + ChannelWriteEntry(key, mapper=partial(_get_state_key, key=key)) for key in output_keys ] ) @@ -557,10 +565,7 @@ class CompiledStateGraph(CompiledGraph): ) else: input_schema = node.input if node else self.builder.schema - input_values = { - k: v if is_managed_value(v) else k - for k, v in self.builder.schemas[input_schema].items() - } + input_values = {k: k for k in self.builder.schemas[input_schema]} is_single_input = len(input_values) == 1 and "__root__" in input_values self.channels[key] = EphemeralValue(Any, guard=False) @@ -582,7 +587,8 @@ class CompiledStateGraph(CompiledGraph): ], metadata=node.metadata, retry_policy=node.retry_policy, - ).pipe(node.runnable) + bound=node.runnable, + ) def attach_edge(self, starts: Union[str, Sequence[str]], end: str) -> None: if isinstance(starts, str): @@ -612,7 +618,9 @@ class CompiledStateGraph(CompiledGraph): ) def attach_branch(self, start: str, name: str, branch: Branch) -> None: - def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]: + def branch_writer( + packets: list[Union[str, Send]], config: RunnableConfig + ) -> Optional[ChannelWrite]: if filtered := [p for p in packets if p != END]: writes = [ ( @@ -631,10 +639,17 @@ class CompiledStateGraph(CompiledGraph): ), ) ) - return ChannelWrite(writes, tags=[TAG_HIDDEN]) + ChannelWrite.do_write(config, writes) # attach branch publisher - self.nodes[start] |= branch.run(branch_writer, _get_state_reader(self.builder)) + schema = ( + self.builder.nodes[start].input + if start in self.builder.nodes + else self.builder.schema + ) + self.nodes[start] |= branch.run( + branch_writer, _get_state_reader(self.builder, schema) + ) # attach branch subscribers ends = ( @@ -660,16 +675,17 @@ class CompiledStateGraph(CompiledGraph): ) -def _get_state_reader(graph: StateGraph) -> ChannelRead: - state_keys = list(graph.channels) +def _get_state_reader( + builder: StateGraph, schema: Type[Any] +) -> Callable[[RunnableConfig], Any]: + state_keys = list(builder.channels) + select = list(builder.schemas[schema]) return partial( ChannelRead.do_read, - channel=state_keys[0] if state_keys == ["__root__"] else state_keys, + select=select[0] if select == ["__root__"] else select, fresh=True, # coerce state dict to schema class (eg. pydantic model) - mapper=( - None if state_keys == ["__root__"] else partial(_coerce_state, graph.schema) - ), + mapper=(None if state_keys == ["__root__"] else partial(_coerce_state, schema)), ) @@ -679,12 +695,12 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]: def _get_channels( schema: Type[dict], -) -> tuple[dict[str, BaseChannel], dict[str, Type[ManagedValue]]]: +) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec]]: if not hasattr(schema, "__annotations__"): - return {"__root__": _get_channel(schema, allow_managed=False)}, {} + return {"__root__": _get_channel("__root__", schema, allow_managed=False)}, {} all_keys = { - name: _get_channel(typ) + name: _get_channel(name, typ) for name, typ in get_type_hints(schema, include_extras=True).items() if name != "__slots__" } @@ -695,18 +711,23 @@ def _get_channels( def _get_channel( - annotation: Any, *, allow_managed: bool = True -) -> Union[BaseChannel, Type[ManagedValue]]: - if manager := _is_field_managed_value(annotation): + name: str, annotation: Any, *, allow_managed: bool = True +) -> Union[BaseChannel, ManagedValueSpec]: + if manager := _is_field_managed_value(name, annotation): if allow_managed: return manager else: raise ValueError(f"This {annotation} not allowed in this position") elif channel := _is_field_channel(annotation): + channel.key = name return channel elif channel := _is_field_binop(annotation): + channel.key = name return channel - return LastValue(annotation) + + fallback = LastValue(annotation) + fallback.key = name + return fallback def _is_field_channel(typ: Type[Any]) -> Optional[BaseChannel]: @@ -736,12 +757,18 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: return None -def _is_field_managed_value(typ: Type[Any]) -> Optional[Type[ManagedValue]]: +def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[ManagedValueSpec]: if hasattr(typ, "__metadata__"): meta = typ.__metadata__ if len(meta) >= 1: decoration = get_origin(meta[-1]) or meta[-1] if is_managed_value(decoration): + if isinstance(decoration, ConfiguredManagedValue): + for k, v in decoration.kwargs.items(): + if v is ChannelKeyPlaceholder: + decoration.kwargs[k] = name + if v is ChannelTypePlaceholder: + decoration.kwargs[k] = typ.__origin__ return decoration return None diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index 0455ed58b..b86388930 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -1,13 +1,13 @@ -import asyncio from abc import ABC, abstractmethod -from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager +from contextlib import asynccontextmanager, contextmanager from inspect import isclass from typing import ( Any, - AsyncGenerator, - Generator, + AsyncIterator, Generic, + Iterator, NamedTuple, + Sequence, Type, TypeVar, Union, @@ -16,18 +16,22 @@ from typing import ( from langchain_core.runnables import RunnableConfig from typing_extensions import Self, TypeGuard +from langgraph.constants import RUNTIME_PLACEHOLDER + V = TypeVar("V") +U = TypeVar("U") class ManagedValue(ABC, Generic[V]): + runtime: bool = False + """Whether the managed value is always created at runtime, ie. never stored.""" + def __init__(self, config: RunnableConfig) -> None: self.config = config @classmethod @contextmanager - def enter( - cls, config: RunnableConfig, **kwargs: Any - ) -> Generator[Self, None, None]: + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: try: value = cls(config, **kwargs) yield value @@ -41,9 +45,7 @@ class ManagedValue(ABC, Generic[V]): @classmethod @asynccontextmanager - async def aenter( - cls, config: RunnableConfig, **kwargs: Any - ) -> AsyncGenerator[Self, None]: + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: try: value = cls(config, **kwargs) yield value @@ -56,8 +58,15 @@ class ManagedValue(ABC, Generic[V]): pass @abstractmethod - def __call__(self, step: int) -> V: - ... + def __call__(self, step: int) -> V: ... + + +class WritableManagedValue(Generic[V, U], ManagedValue[V], ABC): + @abstractmethod + def update(self, writes: Sequence[U]) -> None: ... + + @abstractmethod + async def aupdate(self, writes: Sequence[U]) -> None: ... class ConfiguredManagedValue(NamedTuple): @@ -67,8 +76,6 @@ class ConfiguredManagedValue(NamedTuple): ManagedValueSpec = Union[Type[ManagedValue], ConfiguredManagedValue] -ManagedValueMapping = dict[str, ManagedValue] - def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]: return (isclass(value) and issubclass(value, ManagedValue)) or isinstance( @@ -76,46 +83,65 @@ def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]: ) -@contextmanager -def ManagedValuesManager( - values: dict[str, ManagedValueSpec], - config: RunnableConfig, -) -> Generator[ManagedValueMapping, None, None]: - if values: - with ExitStack() as stack: - yield { - key: stack.enter_context( - value.cls.enter(config, **value.kwargs) - if isinstance(value, ConfiguredManagedValue) - else value.enter(config) - ) - for key, value in values.items() - } - else: - yield {} +def is_readonly_managed_value(value: Any) -> TypeGuard[Type[ManagedValue]]: + return ( + isclass(value) + and issubclass(value, ManagedValue) + and not issubclass(value, WritableManagedValue) + ) or ( + isinstance(value, ConfiguredManagedValue) + and not issubclass(value.cls, WritableManagedValue) + ) -@asynccontextmanager -async def AsyncManagedValuesManager( - values: dict[str, ManagedValueSpec], - config: RunnableConfig, -) -> AsyncGenerator[ManagedValueMapping, None]: - if values: - async with AsyncExitStack() as stack: - # create enter tasks with reference to spec - tasks = { - asyncio.create_task( - stack.enter_async_context( - value.cls.aenter(config, **value.kwargs) - if isinstance(value, ConfiguredManagedValue) - else value.aenter(config) - ) - ): key - for key, value in values.items() - } - # wait for all enter tasks - done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - # build mapping from spec to result - yield {tasks[task]: task.result() for task in done} - else: - yield {} +def is_writable_managed_value(value: Any) -> TypeGuard[Type[WritableManagedValue]]: + return (isclass(value) and issubclass(value, WritableManagedValue)) or ( + isinstance(value, ConfiguredManagedValue) + and issubclass(value.cls, WritableManagedValue) + ) + + +ChannelKeyPlaceholder = object() +ChannelTypePlaceholder = object() + + +class ManagedValueMapping(dict[str, ManagedValue]): + def replace_runtime_values(self, step: int, values: Union[dict[str, Any], Any]): + if not self or not values: + return + if all(not mv.runtime for mv in self.values()): + return + if isinstance(values, dict): + for key, value in values.items(): + for chan, mv in self.items(): + if mv.runtime and mv(step) is value: + values[key] = {RUNTIME_PLACEHOLDER: chan} + elif hasattr(values, "__dir__") and callable(values.__dir__): + for key in dir(values): + try: + value = getattr(values, key) + for chan, mv in self.items(): + if mv.runtime and mv(step) is value: + setattr(values, key, {RUNTIME_PLACEHOLDER: chan}) + except AttributeError: + pass + + def replace_runtime_placeholders( + self, step: int, values: Union[dict[str, Any], Any] + ): + if not self or not values: + return + if all(not mv.runtime for mv in self.values()): + return + if isinstance(values, dict): + for key, value in values.items(): + if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: + values[key] = self[value[RUNTIME_PLACEHOLDER]](step) + elif hasattr(values, "__dir__") and callable(values.__dir__): + for key in dir(values): + try: + value = getattr(values, key) + if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: + setattr(values, key, self[value[RUNTIME_PLACEHOLDER]](step)) + except AttributeError: + pass diff --git a/libs/langgraph/langgraph/managed/context.py b/libs/langgraph/langgraph/managed/context.py new file mode 100644 index 000000000..43cff5e67 --- /dev/null +++ b/libs/langgraph/langgraph/managed/context.py @@ -0,0 +1,87 @@ +from contextlib import asynccontextmanager, contextmanager +from inspect import signature +from typing import ( + Any, + AsyncContextManager, + AsyncIterator, + ContextManager, + Iterator, + Optional, + Type, + Union, +) + +from langchain_core.runnables import RunnableConfig +from typing_extensions import Self + +from langgraph.managed.base import ConfiguredManagedValue, ManagedValue, V + + +class Context(ManagedValue): + runtime = True + + value: V + + @staticmethod + def of( + ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None, + actx: Optional[Type[AsyncContextManager[V]]] = None, + ) -> ConfiguredManagedValue: + if ctx is None and actx is None: + raise ValueError("Must provide either sync or async context manager.") + return ConfiguredManagedValue(Context, {"ctx": ctx, "actx": actx}) + + @classmethod + @contextmanager + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: + with super().enter(config, **kwargs) as self: + if self.ctx is None: + raise ValueError( + "Synchronous context manager not found. Please initialize Context value with a sync context manager, or invoke your graph asynchronously." + ) + ctx = ( + self.ctx(config) + if signature(self.ctx).parameters.get("config") + else self.ctx() + ) + with ctx as v: + self.value = v + yield self + + @classmethod + @asynccontextmanager + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: + async with super().aenter(config, **kwargs) as self: + if self.actx is not None: + ctx = ( + self.actx(config) + if signature(self.actx).parameters.get("config") + else self.actx() + ) + else: + ctx = ( + self.ctx(config) + if signature(self.ctx).parameters.get("config") + else self.ctx() + ) + if hasattr(ctx, "__aenter__"): + async with ctx as v: + self.value = v + yield self + else: + with ctx as v: + self.value = v + yield self + + def __init__( + self, + config: RunnableConfig, + *, + ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None, + actx: Optional[Type[AsyncContextManager[V]]] = None, + ) -> None: + self.ctx = ctx + self.actx = actx + + def __call__(self, step: int) -> V: + return self.value diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py new file mode 100644 index 000000000..f5e0561bd --- /dev/null +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -0,0 +1,125 @@ +import collections.abc +from contextlib import asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncIterator, + Iterator, + Optional, + Sequence, + Type, +) + +from langchain_core.runnables import RunnableConfig +from typing_extensions import NotRequired, Required, Self + +from langgraph.constants import CONFIG_KEY_STORE +from langgraph.errors import InvalidUpdateError +from langgraph.managed.base import ( + ChannelKeyPlaceholder, + ChannelTypePlaceholder, + ConfiguredManagedValue, + WritableManagedValue, +) +from langgraph.store.base import BaseStore + +V = dict[str, Any] + + +Value = dict[str, V] +Update = dict[str, Optional[V]] + + +# Adapted from typing_extensions +def _strip_extras(t): + """Strips Annotated, Required and NotRequired from a given type.""" + if hasattr(t, "__origin__"): + return _strip_extras(t.__origin__) + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired): + return _strip_extras(t.__args__[0]) + + return t + + +class SharedValue(WritableManagedValue[Value, Update]): + @staticmethod + def on(scope: str) -> ConfiguredManagedValue: + return ConfiguredManagedValue( + SharedValue, + { + "scope": scope, + "key": ChannelKeyPlaceholder, + "typ": ChannelTypePlaceholder, + }, + ) + + @classmethod + @contextmanager + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: + with super().enter(config, **kwargs) as value: + if value.store is not None: + saved = value.store.list([value.ns]) + value.value = saved[value.ns] or {} + yield value + + @classmethod + @asynccontextmanager + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: + async with super().aenter(config, **kwargs) as value: + if value.store is not None: + saved = await value.store.alist([value.ns]) + value.value = saved[value.ns] or {} + yield value + + def __init__( + self, config: RunnableConfig, *, typ: Type[Any], scope: str, key: str + ) -> None: + if typ := _strip_extras(typ): + if typ not in ( + dict, + collections.abc.Mapping, + collections.abc.MutableMapping, + ): + raise ValueError("SharedValue must be a dict") + self.scope = scope + self.value: Value = {} + self.store: BaseStore = config["configurable"].get(CONFIG_KEY_STORE) + if self.store is None: + self.ns: Optional[str] = None + elif scope_value := config["configurable"].get(self.scope): + self.ns = f"scoped:{scope}:{key}:{scope_value}" + else: + raise ValueError( + f"Scope {scope} for shared state key not in config.configurable" + ) + + def __call__(self, step: int) -> Value: + return self.value.copy() + + def _process_update( + self, values: Sequence[Update] + ) -> list[tuple[str, str, Optional[dict[str, Any]]]]: + writes = [] + for vv in values: + for k, v in vv.items(): + if v is None: + if k in self.value: + self.value[k] = None + writes.append((self.ns, k, None)) + elif not isinstance(v, dict): + raise InvalidUpdateError("Received a non-dict value") + else: + self.value[k] = v + writes.append((self.ns, k, v)) + return writes + + def update(self, values: Sequence[Update]) -> None: + if self.store is None: + self._process_update(values) + else: + return self.store.put(self._process_update(values)) + + async def aupdate(self, writes: Sequence[Update]) -> None: + if self.store is None: + self._process_update(writes) + else: + return await self.store.aput(self._process_update(writes)) diff --git a/libs/langgraph/langgraph/prebuilt/__init__.py b/libs/langgraph/langgraph/prebuilt/__init__.py index 4ad055730..0554b8036 100644 --- a/libs/langgraph/langgraph/prebuilt/__init__.py +++ b/libs/langgraph/langgraph/prebuilt/__init__.py @@ -1,4 +1,5 @@ """langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools.""" + from langgraph.prebuilt.chat_agent_executor import create_react_agent from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation from langgraph.prebuilt.tool_node import InjectedState, ToolNode, tools_condition diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py index 546ad53bc..d5a1dc88e 100644 --- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py @@ -130,7 +130,7 @@ def _get_model_preprocessing_runnable( @deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0") def create_react_agent( model: LanguageModelLike, - tools: Union[ToolExecutor, Sequence[BaseTool]], + tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode], *, state_schema: Optional[StateSchemaType] = None, messages_modifier: Optional[MessagesModifier] = None, @@ -144,7 +144,7 @@ def create_react_agent( Args: model: The `LangChain` chat model that supports tool calling. - tools: A list of tools or a ToolExecutor instance. + tools: A list of tools, a ToolExecutor, or a ToolNode instance. state_schema: An optional state schema that defines graph state. Must have `messages` and `is_last_step` keys. Defaults to `AgentState` that defines those two keys. @@ -419,8 +419,13 @@ def create_react_agent( if isinstance(tools, ToolExecutor): tool_classes = tools.tools + tool_node = ToolNode(tool_classes) + elif isinstance(tools, ToolNode): + tool_classes = tools.tools_by_name.values() + tool_node = tools else: tool_classes = tools + tool_node = ToolNode(tool_classes) model = model.bind_tools(tool_classes) # Define the function that determines whether to continue or not @@ -474,7 +479,7 @@ def create_react_agent( # Define the two nodes we will cycle between workflow.add_node("agent", RunnableLambda(call_model, acall_model)) - workflow.add_node("tools", ToolNode(tool_classes)) + workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called diff --git a/libs/langgraph/langgraph/prebuilt/tool_executor.py b/libs/langgraph/langgraph/prebuilt/tool_executor.py index 7590b3836..341f4874d 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_executor.py +++ b/libs/langgraph/langgraph/prebuilt/tool_executor.py @@ -6,7 +6,7 @@ from langchain_core.tools import BaseTool from langchain_core.tools import tool as create_tool from langgraph._api.deprecation import deprecated -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable INVALID_TOOL_MSG_TEMPLATE = ( "{requested_tool_name} is not a valid tool, " diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index ed7b69553..c5636145b 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import asyncio import json from copy import copy from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -14,14 +17,25 @@ from typing import ( cast, ) -from langchain_core.messages import AIMessage, AnyMessage, ToolCall, ToolMessage +from langchain_core.messages import ( + AIMessage, + AnyMessage, + ToolCall, + ToolMessage, +) from langchain_core.runnables import RunnableConfig -from langchain_core.runnables.config import get_config_list, get_executor_for_config +from langchain_core.runnables.config import ( + get_config_list, + get_executor_for_config, +) from langchain_core.tools import BaseTool, InjectedToolArg from langchain_core.tools import tool as create_tool -from typing_extensions import get_args +from typing_extensions import Annotated, get_args, get_origin -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable + +if TYPE_CHECKING: + from pydantic import BaseModel INVALID_TOOL_NAME_ERROR_TEMPLATE = ( "Error: {requested_tool} is not a valid tool, try one of [{available_tools}]." @@ -34,7 +48,7 @@ def str_output(output: Any) -> str: return output else: try: - return json.dumps(output) + return json.dumps(output, ensure_ascii=False) except Exception: return str(output) @@ -82,21 +96,35 @@ class ToolNode(RunnableCallable): self.tools_by_name[tool_.name] = tool_ def _func( - self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig + self, + input: Union[ + list[AnyMessage], + dict[str, Any], + BaseModel, + ], + config: RunnableConfig, ) -> Any: tool_calls, output_type = self._parse_input(input) config_list = get_config_list(config, len(tool_calls)) with get_executor_for_config(config) as executor: outputs = [*executor.map(self._run_one, tool_calls, config_list)] + # TypedDict, pydantic, dataclass, etc. should all be able to load from dict return outputs if output_type == "list" else {"messages": outputs} async def _afunc( - self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig + self, + input: Union[ + list[AnyMessage], + dict[str, Any], + BaseModel, + ], + config: RunnableConfig, ) -> Any: tool_calls, output_type = self._parse_input(input) outputs = await asyncio.gather( *(self._arun_one(call, config) for call in tool_calls) ) + # TypedDict, pydantic, dataclass, etc. should all be able to load from dict return outputs if output_type == "list" else {"messages": outputs} def _run_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage: @@ -135,12 +163,21 @@ class ToolNode(RunnableCallable): return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) def _parse_input( - self, input: Union[list[AnyMessage], dict[str, Any]] + self, + input: Union[ + list[AnyMessage], + dict[str, Any], + BaseModel, + ], ) -> Tuple[List[ToolCall], Literal["list", "dict"]]: if isinstance(input, list): output_type = "list" message: AnyMessage = input[-1] - elif messages := input.get("messages", []): + elif isinstance(input, dict) and (messages := input.get("messages", [])): + output_type = "dict" + message = messages[-1] + elif messages := getattr(input, "messages", None): + # Assume dataclass-like state that can coerce from dict output_type = "dict" message = messages[-1] else: @@ -166,12 +203,18 @@ class ToolNode(RunnableCallable): return None def _inject_state( - self, tool_call: ToolCall, input: Union[list[AnyMessage], dict[str, Any]] + self, + tool_call: ToolCall, + input: Union[ + list[AnyMessage], + dict[str, Any], + BaseModel, + ], ) -> ToolCall: if tool_call["name"] not in self.tools_by_name: return tool_call state_args = _get_state_args(self.tools_by_name[tool_call["name"]]) - if state_args and not isinstance(input, dict): + if state_args and isinstance(input, list): required_fields = list(state_args.values()) if ( len(required_fields) == 1 @@ -188,26 +231,35 @@ class ToolNode(RunnableCallable): required_fields_str = ", ".join(f for f in required_fields if f) err_msg += f" State should contain fields {required_fields_str}." raise ValueError(err_msg) + if isinstance(input, dict): + tool_state_args = { + tool_arg: input[state_field] if state_field else input + for tool_arg, state_field in state_args.items() + } + + else: + tool_state_args = { + tool_arg: getattr(input, state_field) if state_field else input + for tool_arg, state_field in state_args.items() + } + tool_call_copy: ToolCall = copy(tool_call) tool_call_copy["args"] = { **tool_call_copy["args"], - **{ - tool_arg: cast(dict, input)[state_field] if state_field else input - for tool_arg, state_field in state_args.items() - }, + **tool_state_args, } return tool_call_copy def tools_condition( - state: Union[list[AnyMessage], dict[str, Any]], + state: Union[list[AnyMessage], dict[str, Any], BaseModel], ) -> Literal["tools", "__end__"]: """Use in the conditional_edge to route to the ToolNode if the last message has tool calls. Otherwise, route to the end. Args: - state (Union[list[AnyMessage], dict[str, Any]]): The state to check for + state (Union[list[AnyMessage], dict[str, Any], BaseModel]): The state to check for tool calls. Must have a list of messages (MessageGraph) or have the "messages" key (StateGraph). @@ -253,7 +305,9 @@ def tools_condition( """ if isinstance(state, list): ai_message = state[-1] - elif messages := state.get("messages", []): + elif isinstance(state, dict) and (messages := state.get("messages", [])): + ai_message = messages[-1] + elif messages := getattr(state, "messages", []): ai_message = messages[-1] else: raise ValueError(f"No messages found in input state to tool_edge: {state}") @@ -328,12 +382,20 @@ class InjectedState(InjectedToolArg): def _get_state_args(tool: BaseTool) -> Dict[str, Optional[str]]: full_schema = tool.get_input_schema() tool_args_to_state_fields: Dict = {} + + def _is_injection(type_arg: Any): + if isinstance(type_arg, InjectedState) or ( + isinstance(type_arg, type) and issubclass(type_arg, InjectedState) + ): + return True + origin_ = get_origin(type_arg) + if origin_ is Union or origin_ is Annotated: + return any(_is_injection(ta) for ta in get_args(type_arg)) + return False + for name, type_ in full_schema.__annotations__.items(): injections = [ - type_arg - for type_arg in get_args(type_) - if isinstance(type_arg, InjectedState) - or (isinstance(type_arg, type) and issubclass(type_arg, InjectedState)) + type_arg for type_arg in get_args(type_) if _is_injection(type_arg) ] if len(injections) > 1: raise ValueError( diff --git a/libs/langgraph/langgraph/prebuilt/tool_validator.py b/libs/langgraph/langgraph/prebuilt/tool_validator.py index 73b2cce2b..2222e7f2f 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_validator.py +++ b/libs/langgraph/langgraph/prebuilt/tool_validator.py @@ -24,20 +24,22 @@ from langchain_core.messages import ( ToolCall, ToolMessage, ) -from langchain_core.pydantic_v1 import BaseModel, ValidationError from langchain_core.runnables import ( RunnableConfig, ) from langchain_core.runnables.config import get_executor_for_config from langchain_core.tools import BaseTool, create_schema_from_function -from pydantic import BaseModel as BaseModelV2 -from pydantic import ValidationError as ValidationErrorV2 +from pydantic import BaseModel, ValidationError +from pydantic.v1 import BaseModel as BaseModelV1 +from pydantic.v1 import ValidationError as ValidationErrorV1 -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable def _default_format_error( - error: BaseException, call: ToolCall, schema: Type[BaseModel] + error: BaseException, + call: ToolCall, + schema: Union[Type[BaseModel], Type[BaseModelV1]], ) -> str: """Default error formatting function.""" return f"{repr(error)}\n\nRespond after fixing all validation errors." @@ -75,7 +77,7 @@ class ValidationNode(RunnableCallable): >>> from typing import Literal, Annotated, TypedDict ... >>> from langchain_anthropic import ChatAnthropic - >>> from langchain_core.pydantic_v1 import BaseModel, validator + >>> from pydantic import BaseModel, validator ... >>> from langgraph.graph import END, START, StateGraph >>> from langgraph.prebuilt import ValidationNode @@ -176,7 +178,7 @@ class ValidationNode(RunnableCallable): ) self.schemas_by_name[schema.name] = schema.args_schema elif isinstance(schema, type) and issubclass( - schema, (BaseModel, BaseModelV2) + schema, (BaseModel, BaseModelV1) ): self.schemas_by_name[schema.__name__] = cast(Type[BaseModel], schema) elif callable(schema): @@ -212,13 +214,22 @@ class ValidationNode(RunnableCallable): def run_one(call: ToolCall): schema = self.schemas_by_name[call["name"]] try: - output = schema.validate(call["args"]) + if issubclass(schema, BaseModel): + output = schema.model_validate(call["args"]) + content = output.model_dump_json() + elif issubclass(schema, BaseModelV1): + output = schema.validate(call["args"]) + content = output.json() + else: + raise ValueError( + f"Unsupported schema type: {type(schema)}. Expected BaseModel or BaseModelV1." + ) return ToolMessage( - content=output.json(), + content=content, name=call["name"], tool_call_id=cast(str, call["id"]), ) - except (ValidationError, ValidationErrorV2) as e: + except (ValidationError, ValidationErrorV1) as e: return ToolMessage( content=self._format_error(e, call, schema), name=call["name"], diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 7b2fdb5f9..8c7a05e13 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1,15 +1,11 @@ from __future__ import annotations import asyncio -import concurrent.futures -import time from collections import deque from functools import partial -from inspect import signature from typing import ( Any, AsyncIterator, - Awaitable, Callable, Dict, Iterator, @@ -26,39 +22,34 @@ from uuid import UUID, uuid5 from langchain_core.globals import get_debug from langchain_core.load.dump import dumpd -from langchain_core.pydantic_v1 import BaseModel, Field, root_validator from langchain_core.runnables import ( Runnable, + RunnableLambda, RunnableSequence, - RunnableSerializable, ) -from langchain_core.runnables.base import Input, Output, coerce_to_runnable +from langchain_core.runnables.base import Input, Output from langchain_core.runnables.config import ( RunnableConfig, ensure_config, get_async_callback_manager_for_config, get_callback_manager_for_config, - patch_config, ) from langchain_core.runnables.utils import ( ConfigurableFieldSpec, create_model, + get_function_nonlocals, get_unique_config_specs, ) from langchain_core.tracers._streaming import _StreamingCallbackHandler +from pydantic import BaseModel from typing_extensions import Self from langgraph.channels.base import ( BaseChannel, ) -from langgraph.channels.context import Context -from langgraph.channels.last_value import LastValue -from langgraph.channels.manager import ( - AsyncChannelsManager, - ChannelsManager, -) from langgraph.checkpoint.base import ( BaseCheckpointSaver, + CheckpointTuple, copy_checkpoint, create_checkpoint, empty_checkpoint, @@ -68,51 +59,42 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, - ERROR, + CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, INTERRUPT, + NS_END, + NS_SEP, ) from langgraph.errors import GraphRecursionError, InvalidUpdateError -from langgraph.managed.base import ( - AsyncManagedValuesManager, - ManagedValuesManager, - ManagedValueSpec, - is_managed_value, -) +from langgraph.managed.base import ManagedValueSpec from langgraph.pregel.algo import ( + PregelTaskWrites, apply_writes, local_read, + local_write, prepare_next_tasks, ) -from langgraph.pregel.debug import ( - map_debug_task_results, - print_step_checkpoint, - print_step_tasks, - print_step_writes, - tasks_w_writes, -) -from langgraph.pregel.io import ( - map_output_updates, - read_channels, -) +from langgraph.pregel.debug import tasks_w_writes +from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop +from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager from langgraph.pregel.read import PregelNode -from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry -from langgraph.pregel.types import ( - All, - PregelExecutableTask, - StateSnapshot, - StreamMode, -) +from langgraph.pregel.retry import RetryPolicy +from langgraph.pregel.runner import PregelRunner +from langgraph.pregel.types import All, StateSnapshot, StreamMode from langgraph.pregel.utils import get_new_channel_versions from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry +from langgraph.store.base import BaseStore +from langgraph.utils.config import ( + merge_configs, + patch_checkpoint_map, + patch_config, + patch_configurable, +) +from langgraph.utils.runnable import RunnableCallable -WriteValue = Union[ - Runnable[Input, Output], - Callable[[Input], Output], - Callable[[Input], Awaitable[Output]], - Any, -] +WriteValue = Union[Callable[[Input], Output], Any] class Channel: @@ -124,8 +106,7 @@ class Channel: *, key: Optional[str] = None, tags: Optional[list[str]] = None, - ) -> PregelNode: - ... + ) -> PregelNode: ... @overload @classmethod @@ -135,8 +116,7 @@ class Channel: *, key: None = None, tags: Optional[list[str]] = None, - ) -> PregelNode: - ... + ) -> PregelNode: ... @classmethod def subscribe_to( @@ -179,24 +159,18 @@ class Channel: return ChannelWrite( [ChannelWriteEntry(c) for c in channels] + [ - ( - ChannelWriteEntry(k, skip_none=True, mapper=coerce_to_runnable(v)) - if isinstance(v, Runnable) or callable(v) - else ChannelWriteEntry(k, value=v) - ) + ChannelWriteEntry(k, mapper=v) + if callable(v) + else ChannelWriteEntry(k, value=v) for k, v in kwargs.items() ] ) -class Pregel( - RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] -): +class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): nodes: Mapping[str, PregelNode] - channels: Mapping[str, BaseChannel] = Field(default_factory=dict) - - auto_validate: bool = True + channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] stream_mode: StreamMode = "values" """Mode to stream output, defaults to 'values'.""" @@ -206,53 +180,79 @@ class Pregel( stream_channels: Optional[Union[str, Sequence[str]]] = None """Channels to stream, defaults to all channels not in reserved channels""" - interrupt_after_nodes: Union[All, Sequence[str]] = Field(default_factory=list) + interrupt_after_nodes: Union[All, Sequence[str]] - interrupt_before_nodes: Union[All, Sequence[str]] = Field(default_factory=list) + interrupt_before_nodes: Union[All, Sequence[str]] input_channels: Union[str, Sequence[str]] step_timeout: Optional[float] = None """Maximum time to wait for a step to complete, in seconds. Defaults to None.""" - debug: bool = Field(default_factory=get_debug) + debug: bool """Whether to print debug information during execution. Defaults to False.""" checkpointer: Optional[BaseCheckpointSaver] = None """Checkpointer used to save and load graph state. Defaults to None.""" + store: Optional[BaseStore] = None + """Memory store to use for SharedValues. Defaults to None.""" + retry_policy: Optional[RetryPolicy] = None """Retry policy to use when running tasks. Set to None to disable.""" config_type: Optional[Type[Any]] = None + config: Optional[RunnableConfig] = None + name: str = "LangGraph" - class Config: - arbitrary_types_allowed = True + def __init__( + self, + *, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = None, + auto_validate: bool = True, + stream_mode: StreamMode = "values", + output_channels: Union[str, Sequence[str]], + stream_channels: Optional[Union[str, Sequence[str]]] = None, + interrupt_after_nodes: Union[All, Sequence[str]] = (), + interrupt_before_nodes: Union[All, Sequence[str]] = (), + input_channels: Union[str, Sequence[str]], + step_timeout: Optional[float] = None, + debug: Optional[bool] = None, + checkpointer: Optional[BaseCheckpointSaver] = None, + store: Optional[BaseStore] = None, + retry_policy: Optional[RetryPolicy] = None, + config_type: Optional[Type[Any]] = None, + config: Optional[RunnableConfig] = None, + name: str = "LangGraph", + ) -> None: + self.nodes = nodes + self.channels = channels or {} + self.stream_mode = stream_mode + self.output_channels = output_channels + self.stream_channels = stream_channels + self.interrupt_after_nodes = interrupt_after_nodes + self.interrupt_before_nodes = interrupt_before_nodes + self.input_channels = input_channels + self.step_timeout = step_timeout + self.debug = debug if debug is not None else get_debug() + self.checkpointer = checkpointer + self.store = store + self.retry_policy = retry_policy + self.config_type = config_type + self.config = config + self.name = name + if auto_validate: + self.validate() - @classmethod - def is_lc_serializable(cls) -> bool: - """Return whether the graph can be serialized by Langchain.""" - return True + def copy(self, update: dict[str, Any]) -> Self: + attrs = {**self.__dict__, **update} + return self.__class__(**attrs) - @root_validator(skip_on_failure=True) - def validate_on_init(cls, values: dict[str, Any]) -> dict[str, Any]: - if not values["auto_validate"]: - return values - validate_graph( - values["nodes"], - values["channels"], - values["input_channels"], - values["output_channels"], - values["stream_channels"], - values["interrupt_after_nodes"], - values["interrupt_before_nodes"], - ) - if values["interrupt_after_nodes"] or values["interrupt_before_nodes"]: - if not values["checkpointer"]: - raise ValueError("Interrupts require a checkpointer") - return values + def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self: + return self.copy({"config": merge_configs(self.config, config, kwargs)}) def validate(self) -> Self: validate_graph( @@ -304,6 +304,7 @@ class Pregel( def get_input_schema( self, config: Optional[RunnableConfig] = None ) -> Type[BaseModel]: + config = merge_configs(self.config, config) if isinstance(self.input_channels, str): return super().get_input_schema(config) else: @@ -323,6 +324,7 @@ class Pregel( def get_output_schema( self, config: Optional[RunnableConfig] = None ) -> Type[BaseModel]: + config = merge_configs(self.config, config) if isinstance(self.output_channels, str): return super().get_output_schema(config) else: @@ -341,93 +343,266 @@ class Pregel( @property def stream_channels_asis(self) -> Union[str, Sequence[str]]: return self.stream_channels or [ - k for k in self.channels if not isinstance(self.channels[k], Context) + k for k in self.channels if isinstance(self.channels[k], BaseChannel) ] - @property - def managed_values_dict(self) -> dict[str, ManagedValueSpec]: - return { - k: v - for node in self.nodes.values() - if isinstance(node.channels, dict) - for k, v in node.channels.items() - if is_managed_value(v) - } + def get_subgraphs(self, recurse: bool = False) -> Iterator[tuple[str, Pregel]]: + for name, node in self.nodes.items(): + # find the subgraph, if any + graph: Optional[Pregel] = None + candidates = [node.bound] + for candidate in candidates: + if isinstance(candidate, Pregel): + graph = candidate + break + elif isinstance(candidate, RunnableSequence): + candidates.extend(candidate.steps) + elif isinstance(candidate, RunnableLambda): + candidates.extend(candidate.deps) + elif isinstance(candidate, RunnableCallable): + if candidate.func is not None: + candidates.extend( + nl.__self__ if hasattr(nl, "__self__") else nl + for nl in get_function_nonlocals(candidate.func) + ) + if candidate.afunc is not None: + candidates.extend( + nl.__self__ if hasattr(nl, "__self__") else nl + for nl in get_function_nonlocals(candidate.afunc) + ) + # if found, yield recursively + if graph: + yield name, graph + if recurse: + yield from ( + (f"{name}{NS_SEP}{n}", s) + for n, s in graph.get_subgraphs(recurse=recurse) + ) - def get_state(self, config: RunnableConfig) -> StateSnapshot: - """Get the current state of the graph.""" - if not self.checkpointer: - raise ValueError("No checkpointer set") + async def aget_subgraphs( + self, recurse: bool = False + ) -> AsyncIterator[tuple[str, Pregel]]: + for name, node in self.get_subgraphs(recurse=recurse): + yield name, node + + def _prepare_state_snapshot( + self, + config: RunnableConfig, + saved: Optional[CheckpointTuple], + recurse: Optional[BaseCheckpointSaver] = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) - saved = self.checkpointer.get_tuple(config) - checkpoint = saved.checkpoint if saved else empty_checkpoint() - config = saved.config if saved else config with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + self.channels, saved.checkpoint, saved.config, skip_context=True + ) as (channels, managed): + # tasks for this checkpoint next_tasks = prepare_next_tasks( - checkpoint, + saved.checkpoint, self.nodes, channels, managed, - config, - saved.metadata.get("step", -1) + 1 if saved else -1, + saved.config, + saved.metadata.get("step", -1) + 1, for_execution=False, ) + # get the subgraphs + subgraphs = dict(self.get_subgraphs()) + parent_ns = saved.config["configurable"].get("checkpoint_ns", "") + task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {} + for task in next_tasks.values(): + if task.name not in subgraphs: + continue + # assemble checkpoint_ns for this task + task_ns = f"{task.name}{NS_END}{task.id}" + if parent_ns: + task_ns = f"{parent_ns}{NS_SEP}{task_ns}" + if not recurse: + # set config as signal that subgraph checkpoints exist + config = { + "configurable": { + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + "configurable": { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = subgraphs[task.name].get_state( + config, subgraphs=True + ) + # assemble the state snapshot return StateSnapshot( read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config if saved else config, - saved.metadata if saved else None, - saved.checkpoint["ts"] if saved else None, - saved.parent_config if saved else None, - tasks_w_writes(next_tasks, saved.pending_writes), + tuple(t.name for t in next_tasks.values()), + patch_checkpoint_map(saved.config, saved.metadata), + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks.values(), saved.pending_writes, task_states), ) - async def aget_state(self, config: RunnableConfig) -> StateSnapshot: + async def _aprepare_state_snapshot( + self, + config: RunnableConfig, + saved: Optional[CheckpointTuple], + recurse: Optional[BaseCheckpointSaver] = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) + + async with AsyncChannelsManager( + self.channels, saved.checkpoint, saved.config, skip_context=True + ) as ( + channels, + managed, + ): + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + saved.checkpoint, + self.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + # get the subgraphs + subgraphs = {n: g async for n, g in self.aget_subgraphs()} + parent_ns = saved.config["configurable"].get("checkpoint_ns", "") + task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {} + for task in next_tasks.values(): + if task.name not in subgraphs: + continue + # assemble checkpoint_ns for this task + task_ns = f"{task.name}{NS_END}{task.id}" + if parent_ns: + task_ns = f"{parent_ns}{NS_SEP}{task_ns}" + if not recurse: + # set config as signal that subgraph checkpoints exist + config = { + "configurable": { + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + "configurable": { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = await subgraphs[task.name].aget_state( + config, subgraphs=recurse + ) + # assemble the state snapshot + return StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(t.name for t in next_tasks.values()), + patch_checkpoint_map(saved.config, saved.metadata), + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks.values(), saved.pending_writes, task_states), + ) + + def get_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: """Get the current state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") - saved = await self.checkpointer.aget_tuple(config) - checkpoint = saved.checkpoint if saved else empty_checkpoint() + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return pregel.get_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") - config = saved.config if saved else config - async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - saved.metadata.get("step", -1) + 1 if saved else -1, - for_execution=False, - ) - return StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config if saved else config, - saved.metadata if saved else None, - saved.checkpoint["ts"] if saved else None, - saved.parent_config if saved else None, - tasks_w_writes(next_tasks, saved.pending_writes), + config = merge_configs(self.config, config) if self.config else config + saved = checkpointer.get_tuple(config) + return self._prepare_state_snapshot( + config, saved, recurse=checkpointer if subgraphs else None + ) + + async def aget_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: + """Get the current state of the graph.""" + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) ) + # find the subgraph with the matching name + async for name, pregel in self.aget_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return await pregel.aget_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + + config = merge_configs(self.config, config) if self.config else config + saved = await checkpointer.aget_tuple(config) + return await self._aprepare_state_snapshot( + config, saved, recurse=checkpointer if subgraphs else None + ) def get_state_history( self, @@ -438,48 +613,44 @@ class Pregel( limit: Optional[int] = None, ) -> Iterator[StateSnapshot]: """Get the history of the state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + if ( - filter is not None - and signature(self.checkpointer.list).parameters.get("filter") is None + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + yield from pregel.get_state_history( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + filter=filter, + before=before, + limit=limit, + ) + return + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + + config = merge_configs( + self.config, config, {"configurable": {"checkpoint_ns": checkpoint_ns}} + ) + # eagerly consume list() to avoid holding up the db cursor + for checkpoint_tuple in list( + checkpointer.list(config, before=before, limit=limit, filter=filter) ): - raise ValueError("Checkpointer does not support filtering") - for ( - config, - checkpoint, - metadata, - parent_config, - pending_writes, - ) in self.checkpointer.list(config, before=before, limit=limit, filter=filter): - with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - metadata.get("step", -1) + 1, - for_execution=False, - ) - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, - tasks_w_writes(next_tasks, pending_writes), - ) + yield self._prepare_state_snapshot( + checkpoint_tuple.config, checkpoint_tuple + ) async def aget_state_history( self, @@ -490,48 +661,48 @@ class Pregel( limit: Optional[int] = None, ) -> AsyncIterator[StateSnapshot]: """Get the history of the state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + if ( - filter is not None - and signature(self.checkpointer.list).parameters.get("filter") is None - ): - raise ValueError("Checkpointer does not support filtering") - async for ( - config, - checkpoint, - metadata, - parent_config, - pending_writes, - ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): - async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - metadata.get("step", -1) + 1, - for_execution=False, - ) - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, - tasks_w_writes(next_tasks, pending_writes), - ) + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + async for name, pregel in self.aget_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + async for state in pregel.aget_state_history( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + filter=filter, + before=before, + limit=limit, + ): + yield state + return + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + + config = merge_configs( + self.config, config, {"configurable": {"checkpoint_ns": checkpoint_ns}} + ) + # eagerly consume list() to avoid holding up the db cursor + for checkpoint_tuple in [ + c + async for c in checkpointer.alist( + config, before=before, limit=limit, filter=filter + ) + ]: + yield await self._aprepare_state_snapshot( + checkpoint_tuple.config, checkpoint_tuple + ) def update_state( self, @@ -543,44 +714,62 @@ class Pregel( node `as_node`. If `as_node` is not provided, it will be set to the last node that updated the state, if not ambiguous. """ - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + # delegate to subgraph + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return pregel.update_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + values, + as_node, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + # get last checkpoint - saved = self.checkpointer.get_tuple(config) + config = merge_configs(self.config, config) if self.config else config + saved = checkpointer.get_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( - saved.checkpoint["channel_versions"] if saved else {} + saved.checkpoint["channel_versions"].copy() if saved else {} ) step = saved.metadata.get("step", -1) if saved else -1 # merge configurable fields with previous checkpoint config - checkpoint_config = { - **config, - "configurable": { - **config["configurable"], - # TODO: add proper support for updating nested subgraph state - "checkpoint_ns": "", - }, - } + checkpoint_config = patch_configurable( + config, + {"checkpoint_ns": config["configurable"].get("checkpoint_ns", "")}, + ) if saved: - checkpoint_config = { - "configurable": { - **config.get("configurable", {}), - **saved.config["configurable"], - } - } + checkpoint_config = patch_configurable(config, saved.config["configurable"]) # find last node that updated the state, if not provided if values is None and as_node is None: - return self.checkpointer.put( + next_config = checkpointer.put( checkpoint_config, create_checkpoint(checkpoint, None, step), { "source": "update", - "step": step, + "step": step + 1, "writes": {}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, {}, ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) elif as_node is None and not any( v for vv in checkpoint["versions_seen"].values() for v in vv.values() ): @@ -606,54 +795,68 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels - with ChannelsManager(self.channels, checkpoint, config) as channels: + with ChannelsManager(self.channels, checkpoint, config) as ( + channels, + managed, + ): # create task to run all writers of the chosen node - writers = self.nodes[as_node].get_writers() + writers = self.nodes[as_node].flat_writers if not writers: raise InvalidUpdateError(f"Node {as_node} has no writers") - task = PregelExecutableTask( - as_node, - values, - RunnableSequence(*writers) if len(writers) > 1 else writers[0], - deque(), - None, - [INTERRUPT], - None, - str(uuid5(UUID(checkpoint["id"]), INTERRUPT)), - ) + writes = deque() + task = PregelTaskWrites(as_node, writes, [INTERRUPT]) + task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT)) + run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] # execute task - task.proc.invoke( - task.input, + run.invoke( + values, patch_config( config, run_name=self.name + "UpdateState", configurable={ # deque.extend is thread-safe - CONFIG_KEY_SEND: task.writes.extend, + CONFIG_KEY_SEND: partial( + local_write, + step + 1, + writes.extend, + self.nodes, + channels, + managed, + ), CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task, config + local_read, + step + 1, + checkpoint, + channels, + managed, + task, + config, ), }, ), ) + # save task writes + if saved: + checkpointer.put_writes(checkpoint_config, task.writes, task_id) # apply to checkpoint and save - apply_writes( - checkpoint, channels, [task], self.checkpointer.get_next_version - ) - - new_versions = get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ) - return self.checkpointer.put( + assert not apply_writes( + checkpoint, channels, [task], checkpointer.get_next_version + ), "Can't write to SharedValues from update_state" + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + next_config = checkpointer.put( checkpoint_config, - create_checkpoint(checkpoint, channels, step + 1), + checkpoint, { "source": "update", "step": step + 1, "writes": {as_node: values}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, - new_versions, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) async def aupdate_state( self, @@ -661,14 +864,39 @@ class Pregel( values: dict[str, Any] | Any, as_node: Optional[str] = None, ) -> RunnableConfig: - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + # delegate to subgraph + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + async for name, pregel in self.aget_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return await pregel.aupdate_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + values, + as_node, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + # get last checkpoint - saved = await self.checkpointer.aget_tuple(config) + config = merge_configs(self.config, config) if self.config else config + saved = await checkpointer.aget_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( - saved.checkpoint["channel_versions"] if saved else {} + saved.checkpoint["channel_versions"].copy() if saved else {} ) step = saved.metadata.get("step", -1) if saved else -1 # merge configurable fields with previous checkpoint config @@ -689,16 +917,18 @@ class Pregel( } # find last node that updated the state, if not provided if values is None and as_node is None: - return await self.checkpointer.aput( + next_config = await checkpointer.aput( checkpoint_config, create_checkpoint(checkpoint, None, step), { "source": "update", - "step": step, + "step": step + 1, "writes": {}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, {}, ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) elif as_node is None and not saved: if ( isinstance(self.input_channels, str) @@ -722,69 +952,82 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels, acting as the chosen node - async with AsyncChannelsManager(self.channels, checkpoint, config) as channels: + async with AsyncChannelsManager(self.channels, checkpoint, config) as ( + channels, + managed, + ): # create task to run all writers of the chosen node - writers = self.nodes[as_node].get_writers() + writers = self.nodes[as_node].flat_writers if not writers: raise InvalidUpdateError(f"Node {as_node} has no writers") - task = PregelExecutableTask( - as_node, - values, - RunnableSequence(*writers) if len(writers) > 1 else writers[0], - deque(), - None, - [INTERRUPT], - None, - str(uuid5(UUID(checkpoint["id"]), INTERRUPT)), - ) + writes = deque() + task = PregelTaskWrites(as_node, writes, [INTERRUPT]) + task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT)) + run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] # execute task - await task.proc.ainvoke( - task.input, + await run.ainvoke( + values, patch_config( config, run_name=self.name + "UpdateState", configurable={ # deque.extend is thread-safe - CONFIG_KEY_SEND: task.writes.extend, + CONFIG_KEY_SEND: partial( + local_write, + step + 1, + writes.extend, + self.nodes, + channels, + managed, + ), CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task, config + local_read, + step + 1, + checkpoint, + channels, + managed, + task, + config, ), }, ), ) + # save task writes + if saved: + await checkpointer.aput_writes(checkpoint_config, writes, task_id) # apply to checkpoint and save - apply_writes( - checkpoint, channels, [task], self.checkpointer.get_next_version - ) - - new_versions = get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ) - return await self.checkpointer.aput( + assert not apply_writes( + checkpoint, channels, [task], checkpointer.get_next_version + ), "Can't write to SharedValues from update_state" + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + next_config = await checkpointer.aput( checkpoint_config, - create_checkpoint(checkpoint, channels, step + 1), + checkpoint, { "source": "update", "step": step + 1, "writes": {as_node: values}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, - new_versions, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) def _defaults( self, - config: Optional[RunnableConfig] = None, + config: RunnableConfig, *, - stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None, - output_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before: Optional[Union[All, Sequence[str]]] = None, - interrupt_after: Optional[Union[All, Sequence[str]]] = None, - debug: Optional[bool] = None, + stream_mode: Optional[Union[StreamMode, list[StreamMode]]], + output_keys: Optional[Union[str, Sequence[str]]], + interrupt_before: Optional[Union[All, Sequence[str]]], + interrupt_after: Optional[Union[All, Sequence[str]]], + debug: Optional[bool], ) -> tuple[ bool, Sequence[StreamMode], Union[str, Sequence[str]], - Union[str, Sequence[str]], Optional[Sequence[str]], Optional[Sequence[str]], Optional[BaseCheckpointSaver], @@ -799,14 +1042,10 @@ class Pregel( stream_mode = stream_mode if stream_mode is not None else self.stream_mode if not isinstance(stream_mode, list): stream_mode = [stream_mode] - if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: + if CONFIG_KEY_TASK_ID in config.get("configurable", {}): # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if ( - config is not None - and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER) - and (interrupt_after or interrupt_before) - ): + if CONFIG_KEY_CHECKPOINTER in config.get("configurable", {}): checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ CONFIG_KEY_CHECKPOINTER ] @@ -831,6 +1070,7 @@ class Pregel( interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, + subgraphs: bool = False, ) -> Iterator[Union[dict[str, Any], Any]]: """Stream graph steps for a single input. @@ -847,6 +1087,7 @@ class Pregel( interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. debug: Whether to print debug information during execution, defaults to False. + subgraphs: Whether to stream subgraphs, defaults to False. Yields: The output of each step in the graph. The output shape depends on the stream_mode. @@ -898,7 +1139,23 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ - config = ensure_config(config) + + stream = deque() + + def output() -> Iterator: + while stream: + ns, mode, payload = stream.popleft() + if mode in stream_modes: + if subgraphs and isinstance(stream_mode, list): + yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) + elif isinstance(stream_mode, list): + yield (mode, payload) + elif subgraphs: + yield (tuple(ns.split(NS_SEP)) if ns else (), payload) + else: + yield payload + + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_callback_manager_for_config(config) run_manager = callback_manager.on_chain_start( dumpd(self), @@ -931,128 +1188,55 @@ class Pregel( ) with SyncPregelLoop( - input, config=config, checkpointer=checkpointer, graph=self + input, + stream=stream.append, + config=config, + store=self.store, + checkpointer=checkpointer, + nodes=self.nodes, + specs=self.channels, + output_keys=output_keys, + stream_keys=self.stream_channels_asis, + debug=debug, ) as loop: + # create runner + runner = PregelRunner( + submit=loop.submit, + put_writes=loop.put_writes, + ) + # enable subgraph streaming + if subgraphs: + loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates # channel updates from step N are only visible in step N+1 # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps while loop.tick( - output_keys=output_keys, + input_keys=self.input_channels, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, ): - # debug flag - if debug: - print_step_checkpoint( - loop.checkpoint_metadata, - loop.channels, - self.stream_channels_list, - ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # debug flag - if debug: - print_step_tasks(loop.step, loop.tasks) - - # execute tasks, and wait for one to fail or all to finish. - # each task is independent from all other concurrent tasks - # yield updates/debug output as each task finishes - futures = { - loop.submit( - run_with_retry, - task, - self.retry_policy, - ): task - for task in loop.tasks - if not task.writes - } - end_time = ( - self.step_timeout + time.monotonic() - if self.step_timeout - else None - ) - if not futures: - done, inflight = set(), set() - while futures: - done, inflight = concurrent.futures.wait( - futures, - return_when=concurrent.futures.FIRST_COMPLETED, - timeout=( - max(0, end_time - time.monotonic()) - if end_time - else None - ), - ) - if not done: - break # timed out - for fut in done: - task = futures.pop(fut) - if exc := _exception(fut): - # save error to checkpointer - loop.put_writes(task.id, [(ERROR, exc)]) - else: - # save task writes to checkpointer - loop.put_writes(task.id, task.writes) - # yield updates output for the finished task - if "updates" in stream_modes: - yield from _with_mode( - "updates", - isinstance(stream_mode, list), - map_output_updates(output_keys, [task]), - ) - if "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_task_results( - loop.step, - [task], - self.stream_channels_list, - ), - ) - else: - # remove references to loop vars - del fut, task - if _should_stop_others(done): - break - - # panic on failure or timeout - _panic_or_proceed(done, inflight, loop.step) - # don't keep futures around in memory longer than needed - del done, inflight, futures - # debug flag - if debug: - print_step_writes( - loop.step, - [w for t in loop.tasks for w in t.writes], - self.stream_channels_list, - ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # handle exit - if loop.status == "out_of_steps": - raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached " - "without hitting a stop condition. You can increase the " - "limit by setting the `recursion_limit` config key." - ) - # set final channel values as run output - run_manager.on_chain_end(read_channels(loop.channels, output_keys)) + for _ in runner.tick( + loop.tasks.values(), + timeout=self.step_timeout, + retry_policy=self.retry_policy, + ): + # emit output + for o in output(): + yield o + # emit output + yield from output() + # handle exit + if loop.status == "out_of_steps": + raise GraphRecursionError( + f"Recursion limit of {config['recursion_limit']} reached " + "without hitting a stop condition. You can increase the " + "limit by setting the `recursion_limit` config key." + ) + # set final channel values as run output + run_manager.on_chain_end(loop.output) except BaseException as e: run_manager.on_chain_error(e) raise @@ -1067,6 +1251,7 @@ class Pregel( interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, + subgraphs: bool = False, ) -> AsyncIterator[Union[dict[str, Any], Any]]: """Stream graph steps for a single input. @@ -1083,6 +1268,7 @@ class Pregel( interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. debug: Whether to print debug information during execution, defaults to False. + subgraphs: Whether to stream subgraphs, defaults to False. Yields: The output of each step in the graph. The output shape depends on the stream_mode. @@ -1134,7 +1320,23 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ - config = ensure_config(config) + + stream = deque() + + def output() -> Iterator: + while stream: + ns, mode, payload = stream.popleft() + if mode in stream_modes: + if subgraphs and isinstance(stream_mode, list): + yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) + elif isinstance(stream_mode, list): + yield (mode, payload) + elif subgraphs: + yield (tuple(ns.split(NS_SEP)) if ns else (), payload) + else: + yield payload + + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_async_callback_manager_for_config(config) run_manager = await callback_manager.on_chain_start( dumpd(self), @@ -1175,137 +1377,58 @@ class Pregel( debug=debug, ) async with AsyncPregelLoop( - input, config=config, checkpointer=checkpointer, graph=self + input, + stream=stream.append, + config=config, + store=self.store, + checkpointer=checkpointer, + nodes=self.nodes, + specs=self.channels, + output_keys=output_keys, + stream_keys=self.stream_channels_asis, ) as loop: - aioloop = asyncio.get_event_loop() + # create runner + runner = PregelRunner( + submit=loop.submit, + put_writes=loop.put_writes, + use_astream=do_stream is not None, + ) + # enable subgraph streaming + if subgraphs: + loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates # channel updates from step N are only visible in step N+1 # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps - while loop.tick( - output_keys=output_keys, + while await asyncio.to_thread( + loop.tick, + input_keys=self.input_channels, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, ): - # debug flag - if debug: - print_step_checkpoint( - loop.checkpoint_metadata, - loop.channels, - self.stream_channels_list, - ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # debug flag - if debug: - print_step_tasks(loop.step, loop.tasks) - - # execute tasks, and wait for one to fail or all to finish. - # each task is independent from all other concurrent tasks - # yield updates/debug output as each task finishes - futures = { - loop.submit( - arun_with_retry, - task, - self.retry_policy, - stream=do_stream, - __name__=task.name, - __cancel_on_exit__=True, - ): task - for task in loop.tasks - if not task.writes - } - end_time = ( - self.step_timeout + aioloop.time() - if self.step_timeout - else None - ) - if not futures: - done, inflight = set(), set() - while futures: - done, inflight = await asyncio.wait( - futures, - return_when=asyncio.FIRST_COMPLETED, - timeout=( - max(0, end_time - aioloop.time()) if end_time else None - ), - ) - if not done: - break # timed out - for fut in done: - task = futures.pop(fut) - if exc := _exception(fut): - # save error to checkpointer - loop.put_writes(task.id, [(ERROR, exc)]) - else: - # save task writes to checkpointer - loop.put_writes(task.id, task.writes) - # yield updates output for the finished task - if "updates" in stream_modes: - for chunk in _with_mode( - "updates", - isinstance(stream_mode, list), - map_output_updates(output_keys, [task]), - ): - yield chunk - if "debug" in stream_modes: - for chunk in _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_task_results( - loop.step, - [task], - self.stream_channels_list, - ), - ): - yield chunk - else: - # remove references to loop vars - del fut, task - if _should_stop_others(done): - break - - # panic on failure or timeout - _panic_or_proceed(done, inflight, loop.step, asyncio.TimeoutError) - # don't keep futures around in memory longer than needed - del done, inflight, futures - # debug flag - if debug: - print_step_writes( - loop.step, - [w for t in loop.tasks for w in t.writes], - self.stream_channels_list, - ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # handle exit - if loop.status == "out_of_steps": - raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached " - "without hitting a stop condition. You can increase the " - "limit by setting the `recursion_limit` config key." - ) - - # set final channel values as run output - await run_manager.on_chain_end( - read_channels(loop.channels, output_keys) + async for _ in runner.atick( + loop.tasks.values(), + timeout=self.step_timeout, + retry_policy=self.retry_policy, + ): + # emit output + for o in output(): + yield o + # emit output + for o in output(): + yield o + # handle exit + if loop.status == "out_of_steps": + raise GraphRecursionError( + f"Recursion limit of {config['recursion_limit']} reached " + "without hitting a stop condition. You can increase the " + "limit by setting the `recursion_limit` config key." ) + # set final channel values as run output + await run_manager.on_chain_end(loop.output) except BaseException as e: - # TODO use on_chain_end if exc is GraphInterrupt await asyncio.shield(run_manager.on_chain_error(e)) raise @@ -1413,60 +1536,3 @@ class Pregel( return latest else: return chunks - - -def _should_stop_others( - done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], -) -> bool: - for fut in done: - if fut.cancelled(): - return True - if fut.exception() is not None: - # TODO don't stop others if exception is interrupt - return True - else: - return False - - -def _exception( - fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]], -) -> Optional[BaseException]: - if fut.cancelled(): - if isinstance(fut, asyncio.Task): - return asyncio.CancelledError() - else: - return concurrent.futures.CancelledError() - else: - return fut.exception() - - -def _panic_or_proceed( - done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], - inflight: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], - step: int, - timeout_exc_cls: Type[Exception] = TimeoutError, -) -> None: - while done: - # if any task failed - if exc := done.pop().exception(): - # cancel all pending tasks - while inflight: - inflight.pop().cancel() - # raise the exception - raise exc - - if inflight: - # if we got here means we timed out - while inflight: - # cancel all pending tasks - inflight.pop().cancel() - # raise timeout error - raise timeout_exc_cls(f"Timed out at step {step}") - - -def _with_mode(mode: StreamMode, on: bool, iter: Iterator[Any]) -> Iterator[Any]: - if on: - for chunk in iter: - yield (mode, chunk) - else: - yield from iter diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index fd725627a..033fab8e0 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -1,6 +1,6 @@ -import json from collections import defaultdict, deque from functools import partial +from hashlib import sha1 from typing import ( Any, Callable, @@ -14,42 +14,36 @@ from typing import ( Union, overload, ) -from uuid import UUID, uuid5 +from uuid import UUID from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager -from langchain_core.runnables.config import ( - RunnableConfig, - merge_configs, - patch_config, -) +from langchain_core.runnables.config import RunnableConfig from langgraph.channels.base import BaseChannel -from langgraph.channels.context import Context -from langgraph.channels.manager import ChannelsManager -from langgraph.checkpoint.base import ( - BaseCheckpointSaver, - Checkpoint, - copy_checkpoint, - create_checkpoint, -) +from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint, copy_checkpoint from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, + CONFIG_KEY_TASK_ID, INTERRUPT, + NS_SEP, RESERVED, + SUBSCRIPTIONS, TAG_HIDDEN, TASKS, Send, ) from langgraph.errors import EmptyChannelError, InvalidUpdateError -from langgraph.managed.base import ManagedValueMapping, is_managed_value +from langgraph.managed.base import ManagedValueMapping from langgraph.pregel.io import read_channel, read_channels from langgraph.pregel.log import logger +from langgraph.pregel.manager import ChannelsManager from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All, PregelExecutableTask, PregelTask +from langgraph.utils.config import merge_configs, patch_config class WritesProtocol(Protocol): @@ -68,56 +62,75 @@ def should_interrupt( checkpoint: Checkpoint, interrupt_nodes: Union[All, Sequence[str]], tasks: list[PregelExecutableTask], -) -> bool: +) -> list[PregelExecutableTask]: version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) null_version = version_type() seen = checkpoint["versions_seen"].get(INTERRUPT, {}) + # interrupt if any channel has been updated since last interrupt + any_updates_since_prev_interrupt = any( + version > seen.get(chan, null_version) + for chan, version in checkpoint["channel_versions"].items() + ) + # and any triggered node is in interrupt_nodes list return ( - # interrupt if any channel has been updated since last interrupt - any( - version > seen.get(chan, null_version) - for chan, version in checkpoint["channel_versions"].items() - ) - # and any triggered node is in interrupt_nodes list - and any( - task.name + [ + task for task in tasks if ( (not task.config or TAG_HIDDEN not in task.config.get("tags")) if interrupt_nodes == "*" else task.name in interrupt_nodes ) - ) + ] + if any_updates_since_prev_interrupt + else [] ) def local_read( + step: int, checkpoint: Checkpoint, channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, task: WritesProtocol, config: RunnableConfig, select: Union[list[str], str], fresh: bool = False, ) -> Union[dict[str, Any], Any]: - if fresh: - new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1) - context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} - with ChannelsManager( - {k: v for k, v in channels.items() if k not in context_channels}, - new_checkpoint, - config, - ) as channels: - all_channels = {**channels, **context_channels} - apply_writes(new_checkpoint, all_channels, [task], None) - return read_channels(all_channels, select) + if isinstance(select, str): + managed_keys = [] + for c, _ in task.writes: + if c == select: + updated = {c} + break + else: + updated = set() else: - return read_channels(channels, select) + managed_keys = [k for k in select if k in managed] + select = [k for k in select if k not in managed] + updated = set(select).intersection(c for c, _ in task.writes) + if fresh and updated: + with ChannelsManager( + {k: v for k, v in channels.items() if k in updated}, + checkpoint, + config, + skip_context=True, + ) as (local_channels, _): + apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None) + values = read_channels({**channels, **local_channels}, select) + else: + values = read_channels(channels, select) + if managed_keys: + values.update({k: managed[k](step) for k in managed_keys}) + return values def local_write( + step: int, commit: Callable[[Sequence[tuple[str, Any]]], None], processes: Mapping[str, PregelNode], channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, writes: Sequence[tuple[str, Any]], ) -> None: for chan, value in writes: @@ -128,7 +141,9 @@ def local_write( ) if value.node not in processes: raise InvalidUpdateError(f"Invalid node name {value.node} in packet") - elif chan not in channels: + # replace any runtime values with placeholders + managed.replace_runtime_values(step, value.arg) + elif chan not in channels and chan not in managed: logger.warning(f"Skipping write for channel '{chan}' which has no readers") commit(writes) @@ -142,7 +157,7 @@ def apply_writes( channels: Mapping[str, BaseChannel], tasks: Sequence[WritesProtocol], get_next_version: Optional[Callable[[int, BaseChannel], int]], -) -> None: +) -> dict[str, list[Any]]: # update seen versions for task in tasks: checkpoint["versions_seen"].setdefault(task.name, {}).update( @@ -158,9 +173,13 @@ def apply_writes( max_version = max(checkpoint["channel_versions"].values()) else: max_version = None + # Consume all channels that were read for chan in { - chan for task in tasks for chan in task.triggers if chan not in RESERVED + chan + for task in tasks + for chan in task.triggers + if chan not in RESERVED and chan in channels }: if channels[chan].consume(): if get_next_version is not None: @@ -174,12 +193,15 @@ def apply_writes( # Group writes by channel pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) + pending_writes_by_managed: dict[str, list[Any]] = defaultdict(list) for task in tasks: for chan, val in task.writes: if chan == TASKS: checkpoint["pending_sends"].append(val) - else: + elif chan in channels: pending_writes_by_channel[chan].append(val) + else: + pending_writes_by_managed[chan].append(val) # Find the highest version of all channels if checkpoint["channel_versions"]: @@ -191,12 +213,7 @@ def apply_writes( updated_channels: set[str] = set() for chan, vals in pending_writes_by_channel.items(): if chan in channels: - try: - updated = channels[chan].update(vals) - except InvalidUpdateError as e: - raise InvalidUpdateError( - f"Invalid update for channel {chan} with values {vals}" - ) from e + updated = channels[chan].update(vals) if updated and get_next_version is not None: checkpoint["channel_versions"][chan] = get_next_version( max_version, channels[chan] @@ -211,6 +228,9 @@ def apply_writes( max_version, channels[chan] ) + # Return managed values writes to be applied externally + return pending_writes_by_managed + @overload def prepare_next_tasks( @@ -225,8 +245,7 @@ def prepare_next_tasks( is_resuming: bool = False, checkpointer: Literal[None] = None, manager: Literal[None] = None, -) -> list[PregelTask]: - ... +) -> dict[str, PregelTask]: ... @overload @@ -242,8 +261,7 @@ def prepare_next_tasks( is_resuming: bool, checkpointer: Optional[BaseCheckpointSaver], manager: Union[None, ParentRunManager, AsyncParentRunManager], -) -> list[PregelExecutableTask]: - ... +) -> dict[str, PregelExecutableTask]: ... def prepare_next_tasks( @@ -258,85 +276,167 @@ def prepare_next_tasks( is_resuming: bool = False, checkpointer: Optional[BaseCheckpointSaver] = None, manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, -) -> Union[list[PregelTask], list[PregelExecutableTask]]: - parent_ns = config.get("configurable", {}).get("checkpoint_ns", "") - tasks: Union[list[PregelTask], list[PregelExecutableTask]] = [] +) -> Union[dict[str, PregelTask], dict[str, PregelExecutableTask]]: + tasks: Union[dict[str, PregelTask], dict[str, PregelExecutableTask]] = {} # Consume pending packets - for packet in checkpoint["pending_sends"]: + for idx, _ in enumerate(checkpoint["pending_sends"]): + if task := prepare_single_task( + (TASKS, idx), + None, + checkpoint=checkpoint, + processes=processes, + channels=channels, + managed=managed, + config=config, + step=step, + for_execution=for_execution, + is_resuming=is_resuming, + checkpointer=checkpointer, + manager=manager, + ): + tasks[task.id] = task + # Check if any processes should be run in next step + # If so, prepare the values to be passed to them + for name in processes: + if task := prepare_single_task( + (SUBSCRIPTIONS, name), + None, + checkpoint=checkpoint, + processes=processes, + channels=channels, + managed=managed, + config=config, + step=step, + for_execution=for_execution, + is_resuming=is_resuming, + checkpointer=checkpointer, + manager=manager, + ): + tasks[task.id] = task + return tasks + + +def prepare_single_task( + task_path: tuple[str, Union[int, str]], + task_id_checksum: Optional[str], + *, + checkpoint: Checkpoint, + processes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, + config: RunnableConfig, + step: int, + for_execution: bool, + is_resuming: bool = False, + checkpointer: Optional[BaseCheckpointSaver] = None, + manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, +) -> Union[None, PregelTask, PregelExecutableTask]: + checkpoint_id = UUID(checkpoint["id"]).bytes + configurable = config.get("configurable", {}) + parent_ns = configurable.get("checkpoint_ns", "") + + if task_path[0] == TASKS: + idx = int(task_path[1]) + packet = checkpoint["pending_sends"][idx] if not isinstance(packet, Send): - logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends") - continue + logger.warning( + f"Ignoring invalid packet type {type(packet)} in pending sends" + ) + return if packet.node not in processes: - logger.warn(f"Ignoring unknown node name {packet.node} in pending sends") - continue + logger.warning(f"Ignoring unknown node name {packet.node} in pending sends") + return # create task id triggers = [TASKS] metadata = { "langgraph_step": step, "langgraph_node": packet.node, "langgraph_triggers": triggers, - "langgraph_task_idx": len(tasks), + "langgraph_path": task_path, } checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}" - if parent_ns - else packet.node + f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node ) - task_id = str( - uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) + task_id = _uuid5_str( + checkpoint_id, + checkpoint_ns, + str(step), + packet.node, + TASKS, + str(idx), ) + if task_id_checksum is not None: + assert task_id == task_id_checksum if for_execution: proc = processes[packet.node] - if node := proc.get_node(): + if node := proc.node: + managed.replace_runtime_placeholders(step, packet.arg) writes = deque() - tasks.append( - PregelExecutableTask( - packet.node, - packet.arg, - node, - writes, - patch_config( - merge_configs( - config, - processes[packet.node].config, - {"metadata": metadata}, - ), - run_name=packet.node, - callbacks=( - manager.get_child(f"graph:step:{step}") - if manager - else None - ), - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: partial( - local_write, writes.extend, processes, channels - ), - CONFIG_KEY_READ: partial( - local_read, - checkpoint, - channels, - PregelTaskWrites(packet.node, writes, triggers), - config, - ), - # in Send we can't checkpoint nested graphs - # as they could be running in parallel - }, + task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" + return PregelExecutableTask( + packet.node, + packet.arg, + node, + writes, + patch_config( + merge_configs( + config, + processes[packet.node].config, + {"metadata": metadata}, ), - triggers, - proc.retry_policy, - task_id, - ) + run_name=packet.node, + callbacks=( + manager.get_child(f"graph:step:{step}") if manager else None + ), + configurable={ + CONFIG_KEY_TASK_ID: task_id, + # deque.extend is thread-safe + CONFIG_KEY_SEND: partial( + local_write, + step, + writes.extend, + processes, + channels, + managed, + ), + CONFIG_KEY_READ: partial( + local_read, + step, + checkpoint, + channels, + managed, + PregelTaskWrites(packet.node, writes, triggers), + config, + ), + CONFIG_KEY_CHECKPOINTER: ( + checkpointer + or configurable.get(CONFIG_KEY_CHECKPOINTER) + ), + CONFIG_KEY_CHECKPOINT_MAP: { + **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}), + parent_ns: checkpoint["id"], + }, + CONFIG_KEY_RESUMING: is_resuming, + "checkpoint_id": None, + "checkpoint_ns": task_checkpoint_ns, + }, + ), + triggers, + proc.retry_policy, + None, + task_id, + task_path, ) + else: - tasks.append(PregelTask(task_id, packet.node)) - # Check if any processes should be run in next step - # If so, prepare the values to be passed to them - version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) - null_version = version_type() - if null_version is None: - return tasks - for name, proc in processes.items(): + return PregelTask(task_id, packet.node) + elif task_path[0] == SUBSCRIPTIONS: + name = str(task_path[1]) + proc = processes[name] + version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) + null_version = version_type() + if null_version is None: + return seen = checkpoint["versions_seen"].get(name, {}) # If any of the channels read by this process were updated if triggers := sorted( @@ -351,88 +451,96 @@ def prepare_next_tasks( try: val = next( _proc_input( - step, name, proc, managed, channels, for_execution=for_execution + step, proc, managed, channels, for_execution=for_execution ) ) except StopIteration: - continue + return # create task id metadata = { "langgraph_step": step, "langgraph_node": name, "langgraph_triggers": triggers, - "langgraph_task_idx": len(tasks), + "langgraph_path": task_path, } - checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{name}" - if parent_ns - else name - ) - task_id = str( - uuid5( - UUID(checkpoint["id"]), - json.dumps((checkpoint_ns, metadata)), - ) + checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name + task_id = _uuid5_str( + checkpoint_id, + checkpoint_ns, + str(step), + name, + SUBSCRIPTIONS, + *triggers, ) + if task_id_checksum is not None: + assert task_id == task_id_checksum if for_execution: - if node := proc.get_node(): + if node := proc.node: writes = deque() - tasks.append( - PregelExecutableTask( - name, - val, - node, - writes, - patch_config( - merge_configs( - config, - proc.config, - {"metadata": metadata}, - ), - run_name=name, - callbacks=( - manager.get_child(f"graph:step:{step}") - if manager - else None - ), - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: partial( - local_write, writes.extend, processes, channels - ), - CONFIG_KEY_READ: partial( - local_read, - checkpoint, - channels, - PregelTaskWrites(name, writes, triggers), - config, - ), - CONFIG_KEY_CHECKPOINTER: ( - checkpointer - or config["configurable"].get( - CONFIG_KEY_CHECKPOINTER - ) - ), - CONFIG_KEY_RESUMING: is_resuming, - "checkpoint_id": checkpoint["id"], - "checkpoint_ns": checkpoint_ns, - }, + task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" + return PregelExecutableTask( + name, + val, + node, + writes, + patch_config( + merge_configs( + config, + proc.config, + {"metadata": metadata}, ), - triggers, - proc.retry_policy, - task_id, - ) + run_name=name, + callbacks=( + manager.get_child(f"graph:step:{step}") + if manager + else None + ), + configurable={ + CONFIG_KEY_TASK_ID: task_id, + # deque.extend is thread-safe + CONFIG_KEY_SEND: partial( + local_write, + step, + writes.extend, + processes, + channels, + managed, + ), + CONFIG_KEY_READ: partial( + local_read, + step, + checkpoint, + channels, + managed, + PregelTaskWrites(name, writes, triggers), + config, + ), + CONFIG_KEY_CHECKPOINTER: ( + checkpointer + or configurable.get(CONFIG_KEY_CHECKPOINTER) + ), + CONFIG_KEY_CHECKPOINT_MAP: { + **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}), + parent_ns: checkpoint["id"], + }, + CONFIG_KEY_RESUMING: is_resuming, + "checkpoint_ns": task_checkpoint_ns, + }, + ), + triggers, + proc.retry_policy, + None, + task_id, + task_path, ) else: - tasks.append(PregelTask(task_id, name)) - return tasks + return PregelTask(task_id, name) def _proc_input( step: int, - name: str, proc: PregelNode, managed: ManagedValueMapping, channels: Mapping[str, BaseChannel], @@ -443,22 +551,17 @@ def _proc_input( # then invoke the process with the values of all non-empty channels if isinstance(proc.channels, dict): try: - val: dict = { - k: read_channel( - channels, - chan, - catch=chan not in proc.triggers, - ) - for k, chan in proc.channels.items() - if isinstance(chan, str) - } - - managed_values = {} - for key, chan in proc.channels.items(): - if is_managed_value(chan): - managed_values[key] = managed[key](step) - - val.update(managed_values) + val: dict[str, Any] = {} + for k, chan in proc.channels.items(): + if chan in proc.triggers: + val[k] = read_channel(channels, chan, catch=False) + elif chan in channels: + try: + val[k] = read_channel(channels, chan, catch=False) + except EmptyChannelError: + continue + else: + val[k] = managed[k](step) except EmptyChannelError: return elif isinstance(proc.channels, list): @@ -480,3 +583,12 @@ def _proc_input( val = proc.mapper(val) yield val + + +def _uuid5_str(namespace: bytes, *parts: str) -> str: + """Generate a UUID from the SHA-1 hash of a namespace UUID and a name.""" + + sha = sha1(namespace, usedforsecurity=False) + sha.update(b"".join(p.encode() for p in parts)) + hex = sha.hexdigest() + return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}" diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 931f3a180..cb6e45e0d 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -1,18 +1,18 @@ -import json from collections import defaultdict +from dataclasses import asdict from datetime import datetime, timezone from pprint import pformat from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypedDict, Union -from uuid import UUID, uuid5 +from uuid import UUID from langchain_core.runnables.config import RunnableConfig from langchain_core.utils.input import get_bolded_text, get_colored_text from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, PendingWrite -from langgraph.constants import ERROR, TAG_HIDDEN +from langgraph.constants import ERROR, INTERRUPT, TAG_HIDDEN from langgraph.pregel.io import read_channels -from langgraph.pregel.types import PregelExecutableTask, PregelTask +from langgraph.pregel.types import PregelExecutableTask, PregelTask, StateSnapshot class TaskPayload(TypedDict): @@ -25,6 +25,8 @@ class TaskPayload(TypedDict): class TaskResultPayload(TypedDict): id: str name: str + error: Optional[str] + interrupts: list[dict] result: list[tuple[str, Any]] @@ -32,6 +34,7 @@ class CheckpointTask(TypedDict): id: str name: str error: Optional[str] + interrupts: list[dict] class CheckpointPayload(TypedDict): @@ -74,50 +77,44 @@ def map_debug_tasks( step: int, tasks: list[PregelExecutableTask] ) -> Iterator[DebugOutputTask]: ts = datetime.now(timezone.utc).isoformat() - for name, input, _, _, config, triggers, _, _ in tasks: - if config is not None and TAG_HIDDEN in config.get("tags", []): + for task in tasks: + if task.config is not None and TAG_HIDDEN in task.config.get("tags", []): continue - metadata = config["metadata"].copy() - metadata.pop("checkpoint_id", None) - yield { "type": "task", "timestamp": ts, "step": step, "payload": { - "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), - "name": name, - "input": input, - "triggers": triggers, + "id": task.id, + "name": task.name, + "input": task.input, + "triggers": task.triggers, }, } def map_debug_task_results( step: int, - tasks: list[PregelExecutableTask], - stream_channels_list: Sequence[str], + task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]], + stream_keys: Union[str, Sequence[str]], ) -> Iterator[DebugOutputTaskResult]: - ts = datetime.now(timezone.utc).isoformat() - for name, _, _, writes, config, _, _, _ in tasks: - if config is not None and TAG_HIDDEN in config.get("tags", []): - continue - - metadata = config["metadata"].copy() - metadata.pop("checkpoint_id", None) - # TODO: make task IDs deterministic in tests and reuse task IDs for payload ID - - yield { - "type": "task_result", - "timestamp": ts, - "step": step, - "payload": { - "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), - "name": name, - "result": [w for w in writes if w[0] in stream_channels_list], - }, - } + stream_channels_list = ( + [stream_keys] if isinstance(stream_keys, str) else stream_keys + ) + task, writes = task_tup + yield { + "type": "task_result", + "timestamp": datetime.now(timezone.utc).isoformat(), + "step": step, + "payload": { + "id": task.id, + "name": task.name, + "error": next((w[1] for w in writes if w[0] == ERROR), None), + "result": [w for w in writes if w[0] in stream_channels_list], + "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], + }, + } def map_debug_checkpoint( @@ -149,8 +146,9 @@ def map_debug_checkpoint( else { "id": t.id, "name": t.name, + "interrupts": tuple(asdict(i) for i in t.interrupts), } - for t in tasks_w_writes(tasks, pending_writes) + for t in tasks_w_writes(tasks, pending_writes, None) ], }, } @@ -190,8 +188,11 @@ def print_step_writes( def print_step_checkpoint( - step: int, channels: Mapping[str, BaseChannel], whitelist: Sequence[str] + metadata: CheckpointMetadata, + channels: Mapping[str, BaseChannel], + whitelist: Sequence[str], ) -> None: + step = metadata["step"] print( f"{get_colored_text(f'[{step}:checkpoint]', color='blue')} " + get_bolded_text(f"State at the end of step {step}:\n") @@ -202,7 +203,9 @@ def print_step_checkpoint( def tasks_w_writes( tasks: list[PregelExecutableTask], pending_writes: Optional[list[PendingWrite]], + states: Optional[dict[str, Union[RunnableConfig, StateSnapshot]]], ) -> tuple[PregelTask, ...]: + pending_writes = pending_writes or [] return tuple( PregelTask( task.id, @@ -210,12 +213,15 @@ def tasks_w_writes( next( ( exc - for tid, n, exc in pending_writes or [] - if tid == task.id - if n == ERROR + for tid, n, exc in pending_writes + if tid == task.id and n == ERROR ), None, ), + tuple( + v for tid, n, v in pending_writes if tid == task.id and n == INTERRUPT + ), + states.get(task.id) if states else None, ) for task in tasks ) diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 5da78a26c..981ebbf7d 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -32,8 +32,7 @@ class Submit(Protocol[P, T]): __name__: Optional[str] = None, __cancel_on_exit__: bool = False, **kwargs: P.kwargs, - ) -> concurrent.futures.Future[T]: - ... + ) -> concurrent.futures.Future[T]: ... class BackgroundExecutor(ContextManager): @@ -100,6 +99,7 @@ class AsyncBackgroundExecutor(AsyncContextManager): self.context_not_supported = sys.version_info < (3, 11) self.tasks: dict[asyncio.Task, bool] = {} self.sentinel = object() + self.loop = asyncio.get_running_loop() def submit( self, @@ -111,23 +111,23 @@ class AsyncBackgroundExecutor(AsyncContextManager): ) -> asyncio.Task[T]: coro = fn(*args, **kwargs) if self.context_not_supported: - task = asyncio.create_task(coro, name=__name__) + task = self.loop.create_task(coro, name=__name__) else: - task = asyncio.create_task(coro, name=__name__, context=copy_context()) + task = self.loop.create_task(coro, name=__name__, context=copy_context()) self.tasks[task] = __cancel_on_exit__ task.add_done_callback(self.done) return task def done(self, task: asyncio.Task) -> None: try: - task.result() - except GraphInterrupt: - # This exception is an interruption signal, not an error - # so we don't want to re-raise it on exit - self.tasks.pop(task) - except BaseException: - pass - else: + if exc := task.exception(): + # This exception is an interruption signal, not an error + # so we don't want to re-raise it on exit + if isinstance(exc, GraphInterrupt): + self.tasks.pop(task) + else: + self.tasks.pop(task) + except asyncio.CancelledError: self.tasks.pop(task) async def __aenter__(self) -> Submit: @@ -146,12 +146,13 @@ class AsyncBackgroundExecutor(AsyncContextManager): # wait for all tasks to finish if self.tasks: await asyncio.wait(self.tasks) - # re-raise the first exception that occurred in a task + # if there's already an exception being raised, don't raise another one if exc_type is None: - # if there's already an exception being raised, don't raise another one + # re-raise the first exception that occurred in a task for task in self.tasks: try: - task.result() + if exc := task.exception(): + raise exc except asyncio.CancelledError: pass diff --git a/libs/langgraph/langgraph/pregel/get_state.py b/libs/langgraph/langgraph/pregel/get_state.py new file mode 100644 index 000000000..79e5f0bf8 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/get_state.py @@ -0,0 +1,36 @@ +from langgraph.constants import NS_SEP +from langgraph.pregel.types import StateSnapshot + + +def assemble_state_snapshot_hierarchy( + root_checkpoint_ns: str, + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], +) -> StateSnapshot: + checkpoint_ns_list_to_visit = sorted( + checkpoint_ns_to_state_snapshots.keys(), + key=lambda x: len(x.split(NS_SEP)), + ) + while checkpoint_ns_list_to_visit: + checkpoint_ns = checkpoint_ns_list_to_visit.pop() + state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] + *path, subgraph_node = checkpoint_ns.split(NS_SEP) + parent_checkpoint_ns = NS_SEP.join(path) + if subgraph_node and ( + parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( + parent_checkpoint_ns + ) + ): + parent_subgraph_snapshots = { + **(parent_state_snapshot.subgraphs or {}), + subgraph_node: state_snapshot, + } + checkpoint_ns_to_state_snapshots[parent_checkpoint_ns] = ( + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ]._replace(subgraphs=parent_subgraph_snapshots) + ) + + state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) + if state_snapshot is None: + raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") + return state_snapshot diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index 53e77557e..286282f1d 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -1,9 +1,9 @@ -from typing import Any, Iterator, Mapping, Optional, Sequence, TypeVar, Union +from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypeVar, Union from langchain_core.runnables.utils import AddableDict from langgraph.channels.base import BaseChannel, EmptyChannelError -from langgraph.constants import TAG_HIDDEN +from langgraph.constants import ERROR, INTERRUPT, TAG_HIDDEN from langgraph.pregel.log import logger from langgraph.pregel.types import PregelExecutableTask @@ -73,15 +73,19 @@ class AddableValuesDict(AddableDict): def map_output_values( output_channels: Union[str, Sequence[str]], - pending_writes: Sequence[tuple[str, Any]], + pending_writes: Union[Literal[True], Sequence[tuple[str, Any]]], channels: Mapping[str, BaseChannel], ) -> Iterator[Union[dict[str, Any], Any]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" if isinstance(output_channels, str): - if any(chan == output_channels for chan, _ in pending_writes): + if pending_writes is True or any( + chan == output_channels for chan, _ in pending_writes + ): yield read_channel(channels, output_channels) else: - if {c for c, _ in pending_writes if c in output_channels}: + if pending_writes is True or { + c for c, _ in pending_writes if c in output_channels + }: yield AddableValuesDict(read_channels(channels, output_channels)) @@ -95,31 +99,36 @@ class AddableUpdatesDict(AddableDict): def map_output_updates( output_channels: Union[str, Sequence[str]], - tasks: list[PregelExecutableTask], + tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], + cached: bool = False, ) -> Iterator[dict[str, Union[Any, dict[str, Any]]]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" output_tasks = [ - t for t in tasks if not t.config or TAG_HIDDEN not in t.config.get("tags") + (t, ww) + for t, ww in tasks + if (not t.config or TAG_HIDDEN not in t.config.get("tags")) + and ww[0][0] != ERROR + and ww[0][0] != INTERRUPT ] if not output_tasks: return if isinstance(output_channels, str): - updated = [ + updated = ( (task.name, value) - for task in output_tasks - for chan, value in task.writes + for task, writes in output_tasks + for chan, value in writes if chan == output_channels - ] + ) else: - updated = [ + updated = ( ( task.name, {chan: value for chan, value in task.writes if chan in output_channels}, ) - for task in output_tasks - if any(chan in output_channels for chan, _ in task.writes) - ] - grouped = {t.name: [] for t in output_tasks} + for task, writes in output_tasks + if any(chan in output_channels for chan, _ in writes) + ) + grouped = {t.name: [] for t, _ in output_tasks} for node, value in updated: grouped[node].append(value) for node, value in grouped.items(): @@ -127,6 +136,8 @@ def map_output_updates( grouped[node] = None if len(value) == 1: grouped[node] = value[0] + if cached: + grouped["__metadata__"] = {"cached": cached} yield AddableUpdatesDict(grouped) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index f43aa25a7..afd29ec1b 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -4,20 +4,22 @@ from collections import deque from contextlib import AsyncExitStack, ExitStack from types import TracebackType from typing import ( - TYPE_CHECKING, Any, AsyncContextManager, Callable, ContextManager, + Iterable, List, Literal, Mapping, Optional, + Protocol, Sequence, Tuple, Type, TypeVar, Union, + cast, ) from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager @@ -25,10 +27,6 @@ from langchain_core.runnables import RunnableConfig from typing_extensions import Self from langgraph.channels.base import BaseChannel -from langgraph.channels.manager import ( - AsyncChannelsManager, - ChannelsManager, -) from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -40,17 +38,21 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( - CONFIG_KEY_READ, + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_RESUMING, + CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, ERROR, INPUT, INTERRUPT, + SCHEDULED, + TAG_HIDDEN, ) from langgraph.errors import EmptyInputError, GraphInterrupt from langgraph.managed.base import ( - AsyncManagedValuesManager, ManagedValueMapping, - ManagedValuesManager, + ManagedValueSpec, + WritableManagedValue, ) from langgraph.pregel.algo import ( PregelTaskWrites, @@ -59,19 +61,33 @@ from langgraph.pregel.algo import ( prepare_next_tasks, should_interrupt, ) -from langgraph.pregel.debug import map_debug_checkpoint, map_debug_tasks +from langgraph.pregel.debug import ( + map_debug_checkpoint, + map_debug_task_results, + map_debug_tasks, + print_step_checkpoint, + print_step_tasks, + print_step_writes, +) from langgraph.pregel.executor import ( AsyncBackgroundExecutor, BackgroundExecutor, Submit, ) -from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single +from langgraph.pregel.io import ( + map_input, + map_output_updates, + map_output_values, + read_channels, + single, +) +from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager +from langgraph.pregel.read import PregelNode from langgraph.pregel.types import PregelExecutableTask from langgraph.pregel.utils import get_new_channel_versions - -if TYPE_CHECKING: - from langgraph.pregel import Pregel - +from langgraph.store.base import BaseStore +from langgraph.store.batch import AsyncBatchedStore +from langgraph.utils.config import patch_configurable V = TypeVar("V") INPUT_DONE = object() @@ -79,10 +95,32 @@ INPUT_RESUMING = object() EMPTY_SEQ = () +class StreamProtocol(Protocol): + def __call__(self, values: Iterable[Tuple[str, str, Any]]) -> None: ... + + +class DuplexStream(StreamProtocol): + def __init__(self, *queues: StreamProtocol) -> None: + self.queues = queues + + def __call__(self, value: Tuple[str, str, Any]) -> None: + for queue in self.queues: + queue(value) + + class PregelLoop: input: Optional[Any] config: RunnableConfig + store: Optional[BaseStore] checkpointer: Optional[BaseCheckpointSaver] + nodes: Mapping[str, PregelNode] + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]] + output_keys: Union[str, Sequence[str]] + stream_keys: Union[str, Sequence[str]] + stream: Optional[StreamProtocol] + skip_done_tasks: bool + is_nested: bool + checkpointer_get_next_version: Callable[[Optional[V]], V] checkpointer_put_writes: Optional[ Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], Any] @@ -98,8 +136,6 @@ class PregelLoop: Any, ] ] - graph: "Pregel" - submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping @@ -107,7 +143,6 @@ class PregelLoop: checkpoint_config: RunnableConfig checkpoint_metadata: CheckpointMetadata checkpoint_pending_writes: List[PendingWrite] - # (thread_id, checkpoint_ns -> channel_versions) checkpoint_previous_versions: dict[str, Union[str, float, int]] step: int @@ -115,9 +150,8 @@ class PregelLoop: status: Literal[ "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" ] - tasks: Sequence[PregelExecutableTask] - stream: deque[Tuple[str, Any]] - is_nested: bool + tasks: dict[str, PregelExecutableTask] + output: Union[None, dict[str, Any], Any] = None # public @@ -125,25 +159,56 @@ class PregelLoop: self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], - graph: "Pregel", + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]], + stream_keys: Union[str, Sequence[str]], + debug: bool = False, ) -> None: - self.stream = deque() + self.stream = stream self.input = input self.config = config + self.store = store self.checkpointer = checkpointer - self.graph = graph - # TODO if managed values no longer needs graph we can replace with - # managed_specs, channel_specs - self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) - - def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: - """Mark tasks as scheduled, to be used by queue-based executors.""" - raise NotImplementedError + self.nodes = nodes + self.specs = specs + self.output_keys = output_keys + self.stream_keys = stream_keys + self.is_nested = CONFIG_KEY_TASK_ID in self.config.get("configurable", {}) + self.skip_done_tasks = "checkpoint_id" not in config["configurable"] + self.debug = debug + if CONFIG_KEY_STREAM in config["configurable"]: + self.stream = DuplexStream( + self.stream, config["configurable"][CONFIG_KEY_STREAM] + ) + if not self.is_nested and config["configurable"].get("checkpoint_ns"): + self.config = patch_configurable( + config, {"checkpoint_ns": "", "checkpoint_id": None} + ) + if ( + CONFIG_KEY_CHECKPOINT_MAP in self.config["configurable"] + and self.config["configurable"].get("checkpoint_ns") + in self.config["configurable"][CONFIG_KEY_CHECKPOINT_MAP] + ): + self.checkpoint_config = patch_configurable( + self.config, + { + "checkpoint_id": config["configurable"][CONFIG_KEY_CHECKPOINT_MAP][ + self.config["configurable"]["checkpoint_ns"] + ] + }, + ) + else: + self.checkpoint_config = config def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: """Put writes for a task, to be read by the next tick.""" + if not writes: + return self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes) if self.checkpointer_put_writes is not None: self.submit( @@ -161,11 +226,12 @@ class PregelLoop: writes, task_id, ) + self._output_writes(task_id, writes) def tick( self, *, - output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + input_keys: Union[str, Sequence[str]], interrupt_after: Sequence[str] = EMPTY_SEQ, interrupt_before: Sequence[str] = EMPTY_SEQ, manager: Union[None, AsyncParentRunManager, ParentRunManager] = None, @@ -177,20 +243,32 @@ class PregelLoop: raise RuntimeError("Cannot tick when status is no longer 'pending'") if self.input not in (INPUT_DONE, INPUT_RESUMING): - self._first() - elif all(task.writes for task in self.tasks): - writes = [w for t in self.tasks for w in t.writes] + self._first(input_keys=input_keys) + elif all(task.writes for task in self.tasks.values()): + writes = [w for t in self.tasks.values() for w in t.writes] + # debug flag + if self.debug: + print_step_writes( + self.step, + writes, + [self.stream_keys] + if isinstance(self.stream_keys, str) + else self.stream_keys, + ) # all tasks have finished - apply_writes( + mv_writes = apply_writes( self.checkpoint, self.channels, - self.tasks, + self.tasks.values(), self.checkpointer_get_next_version, ) + # apply writes to managed values + for key, values in mv_writes.items(): + self._update_mv(key, values) # produce values output - self.stream.extend( - ("values", v) - for v in map_output_values(output_keys, writes, self.channels) + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "values", v) + for v in map_output_values(self.output_keys, writes, self.channels) ) # clear pending writes self.checkpoint_pending_writes.clear() @@ -199,17 +277,18 @@ class PregelLoop: { "source": "loop", "writes": single( - map_output_updates(output_keys, self.tasks) - if self.graph.stream_mode == "updates" - else map_output_values(output_keys, writes, self.channels) + map_output_updates( + self.output_keys, + [(t, t.writes) for t in self.tasks.values()], + ) ), } ) # after execution, check if we should interrupt - if should_interrupt(self.checkpoint, interrupt_after, self.tasks): + if should_interrupt(self.checkpoint, interrupt_after, self.tasks.values()): self.status = "interrupt_after" if self.is_nested: - raise GraphInterrupt(self) + raise GraphInterrupt() else: return False else: @@ -223,7 +302,7 @@ class PregelLoop: # prepare next tasks self.tasks = prepare_next_tasks( self.checkpoint, - self.graph.nodes, + self.nodes, self.channels, self.managed, self.config, @@ -236,16 +315,16 @@ class PregelLoop: # produce debug output if self._checkpointer_put_after_previous is not None: - self.stream.extend( - ("debug", v) + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_checkpoint( self.step - 1, # printing checkpoint for previous step self.checkpoint_config, self.channels, - self.graph.stream_channels_asis, + self.stream_keys, self.checkpoint_metadata, self.checkpoint, - self.tasks, + self.tasks.values(), self.checkpoint_pending_writes, ) ) @@ -256,24 +335,31 @@ class PregelLoop: return False # if there are pending writes from a previous loop, apply them - if self.checkpoint_pending_writes: + if self.skip_done_tasks and self.checkpoint_pending_writes: for tid, k, v in self.checkpoint_pending_writes: - if k == ERROR: # TODO same for INTERRUPT + if k in (ERROR, INTERRUPT): continue - if task := next((t for t in self.tasks if t.id == tid), None): - task.writes.append((k, v)) + if task := self.tasks.get(tid): + if k == SCHEDULED: + self.tasks[tid] = task._replace(scheduled=True) + else: + task.writes.append((k, v)) + # print output for any tasks we applied previous writes to + for task in self.tasks.values(): + if task.writes: + self._output_writes(task.id, task.writes, cached=True) # if all tasks have finished, re-tick - if all(task.writes for task in self.tasks): + if all(task.writes for task in self.tasks.values()): return self.tick( - output_keys=output_keys, + input_keys=input_keys, interrupt_after=interrupt_after, interrupt_before=interrupt_before, manager=manager, ) # before execution, check if we should interrupt - if should_interrupt(self.checkpoint, interrupt_before, self.tasks): + if should_interrupt(self.checkpoint, interrupt_before, self.tasks.values()): self.status = "interrupt_before" if self.is_nested: raise GraphInterrupt() @@ -281,13 +367,20 @@ class PregelLoop: return False # produce debug output - self.stream.extend(("debug", v) for v in map_debug_tasks(self.step, self.tasks)) + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) + for v in map_debug_tasks(self.step, self.tasks.values()) + ) + + # debug flag + if self.debug: + print_step_tasks(self.step, self.tasks.values()) return True # private - def _first(self) -> None: + def _first(self, *, input_keys: Union[str, Sequence[str]]) -> None: # resuming from previous checkpoint requires # - finding a previous checkpoint # - receiving None input (outer graph) or RESUMING flag (subgraph) @@ -303,12 +396,17 @@ class PregelLoop: if k in self.checkpoint["channel_versions"]: version = self.checkpoint["channel_versions"][k] self.checkpoint["versions_seen"][INTERRUPT][k] = version + # produce values output + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "values", v) + for v in map_output_values(self.output_keys, True, self.channels) + ) # map inputs to channel updates - elif input_writes := deque(map_input(self.graph.input_channels, self.input)): + elif input_writes := deque(map_input(input_keys, self.input)): # discard any unfinished tasks from previous checkpoint discard_tasks = prepare_next_tasks( self.checkpoint, - self.graph.nodes, + self.nodes, self.channels, self.managed, self.config, @@ -317,39 +415,39 @@ class PregelLoop: manager=None, ) # apply input writes - apply_writes( + assert not apply_writes( self.checkpoint, self.channels, - discard_tasks + [PregelTaskWrites(INPUT, input_writes, [])], + [*discard_tasks.values(), PregelTaskWrites(INPUT, input_writes, [])], self.checkpointer_get_next_version, - ) + ), "Can't write to SharedValues in graph input" # save input checkpoint - self._put_checkpoint({"source": "input", "writes": self.input}) + self._put_checkpoint({"source": "input", "writes": dict(input_writes)}) else: - raise EmptyInputError(f"Received no input for {self.graph.input_channels}") + raise EmptyInputError(f"Received no input for {input_keys}") # done with input self.input = INPUT_RESUMING if is_resuming else INPUT_DONE def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: # assign step metadata["step"] = self.step + metadata["parents"] = self.config["configurable"].get( + CONFIG_KEY_CHECKPOINT_MAP, {} + ) + # debug flag + if self.debug: + print_step_checkpoint( + metadata, + self.channels, + [self.stream_keys] + if isinstance(self.stream_keys, str) + else self.stream_keys, + ) + # create new checkpoint + self.checkpoint = create_checkpoint(self.checkpoint, self.channels, self.step) # bail if no checkpointer if self._checkpointer_put_after_previous is not None: - # create new checkpoint self.checkpoint_metadata = metadata - self.checkpoint = create_checkpoint( - self.checkpoint, - self.channels, - self.step, - # child graphs keep at most one checkpoint per parent checkpoint - # this is achieved by writing child checkpoints as progress is made - # (so that error recovery / resuming from interrupt don't lose work) - # but doing so always with an id equal to that of the parent checkpoint - id=self.config["configurable"]["checkpoint_id"] - if self.is_nested - else None, - ) - self.checkpoint_config = { **self.checkpoint_config, "configurable": { @@ -364,7 +462,6 @@ class PregelLoop: new_versions = get_new_channel_versions( self.checkpoint_previous_versions, channel_versions ) - self.checkpoint_previous_versions = channel_versions # save it, without blocking @@ -388,28 +485,79 @@ class PregelLoop: # increment step self.step += 1 + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + raise NotImplementedError + def _suppress_interrupt( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: - if exc_type is GraphInterrupt and not self.is_nested: + suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested + if suppress or exc_type is None: + # save final output + self.output = read_channels(self.channels, self.output_keys) + if suppress: + # suppress interrupt return True + def _emit(self, values: Sequence[tuple[str, str, Any]]) -> None: + if self.stream is None: + return + for v in values: + self.stream(v) + + def _output_writes( + self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False + ) -> None: + if task := self.tasks.get(task_id): + if task.config is not None and TAG_HIDDEN in task.config.get("tags"): + return + if writes[0][0] != ERROR and writes[0][0] != INTERRUPT: + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) + for v in map_output_updates( + self.output_keys, [(task, writes)], cached + ) + ) + if not cached: + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) + for v in map_debug_task_results( + self.step, (task, writes), self.stream_keys + ) + ) + class SyncPregelLoop(PregelLoop, ContextManager): def __init__( self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], - graph: "Pregel", + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + debug: bool = False, ) -> None: - super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) + super().__init__( + input, + stream=stream, + config=config, + checkpointer=checkpointer, + store=store, + nodes=nodes, + specs=specs, + output_keys=output_keys, + stream_keys=stream_keys, + debug=debug, + ) self.stack = ExitStack() - self.stack.push(self._suppress_interrupt) if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version self.checkpointer_put_writes = checkpointer.put_writes @@ -432,11 +580,16 @@ class SyncPregelLoop(PregelLoop, ContextManager): finally: self.checkpointer.put(config, checkpoint, metadata, new_versions) + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + return self.submit(cast(WritableManagedValue, self.managed[key]).update, values) + # context manager def __enter__(self) -> Self: saved = ( - self.checkpointer.get_tuple(self.config) if self.checkpointer else None + self.checkpointer.get_tuple(self.checkpoint_config) + if self.checkpointer + else None ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) self.checkpoint_config = { **self.config, @@ -448,15 +601,17 @@ class SyncPregelLoop(PregelLoop, ContextManager): } self.checkpoint = copy_checkpoint(saved.checkpoint) self.checkpoint_metadata = saved.metadata - self.checkpoint_pending_writes = saved.pending_writes or [] + self.checkpoint_pending_writes = ( + [(str(tid), k, v) for tid, k, v in saved.pending_writes] + if saved.pending_writes is not None + else [] + ) self.submit = self.stack.enter_context(BackgroundExecutor(self.config)) - self.channels = self.stack.enter_context( - ChannelsManager(self.graph.channels, self.checkpoint, self.config) - ) - self.managed = self.stack.enter_context( - ManagedValuesManager(self.graph.managed_values_dict, self.config) + self.channels, self.managed = self.stack.enter_context( + ChannelsManager(self.specs, self.checkpoint, self.config, self.store) ) + self.stack.push(self._suppress_interrupt) self.status = "pending" self.step = self.checkpoint_metadata["step"] + 1 self.stop = self.step + self.config["recursion_limit"] + 1 @@ -471,7 +626,6 @@ class SyncPregelLoop(PregelLoop, ContextManager): traceback: Optional[TracebackType], ) -> Optional[bool]: # unwind stack - del self.graph return self.stack.__exit__(exc_type, exc_value, traceback) @@ -480,13 +634,30 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], - graph: "Pregel", + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + debug: bool = False, ) -> None: - super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) + super().__init__( + input, + stream=stream, + config=config, + checkpointer=checkpointer, + store=store, + nodes=nodes, + specs=specs, + output_keys=output_keys, + stream_keys=stream_keys, + debug=debug, + ) + self.store = AsyncBatchedStore(self.store) if self.store else None self.stack = AsyncExitStack() - self.stack.push(self._suppress_interrupt) if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version self.checkpointer_put_writes = checkpointer.aput_writes @@ -509,11 +680,16 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): finally: await self.checkpointer.aput(config, checkpoint, metadata, new_versions) + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + return self.submit( + cast(WritableManagedValue, self.managed[key]).aupdate, values + ) + # context manager async def __aenter__(self) -> Self: saved = ( - await self.checkpointer.aget_tuple(self.config) + await self.checkpointer.aget_tuple(self.checkpoint_config) if self.checkpointer else None ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) @@ -527,15 +703,17 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): } self.checkpoint = copy_checkpoint(saved.checkpoint) self.checkpoint_metadata = saved.metadata - self.checkpoint_pending_writes = saved.pending_writes or [] + self.checkpoint_pending_writes = ( + [(str(tid), k, v) for tid, k, v in saved.pending_writes] + if saved.pending_writes is not None + else [] + ) self.submit = await self.stack.enter_async_context(AsyncBackgroundExecutor()) - self.channels = await self.stack.enter_async_context( - AsyncChannelsManager(self.graph.channels, self.checkpoint, self.config) - ) - self.managed = await self.stack.enter_async_context( - AsyncManagedValuesManager(self.graph.managed_values_dict, self.config) + self.channels, self.managed = await self.stack.enter_async_context( + AsyncChannelsManager(self.specs, self.checkpoint, self.config, self.store) ) + self.stack.push(self._suppress_interrupt) self.status = "pending" self.step = self.checkpoint_metadata["step"] + 1 self.stop = self.step + self.config["recursion_limit"] + 1 @@ -551,7 +729,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): traceback: Optional[TracebackType], ) -> Optional[bool]: # unwind stack - del self.graph return await asyncio.shield( self.stack.__aexit__(exc_type, exc_value, traceback) ) diff --git a/libs/langgraph/langgraph/pregel/manager.py b/libs/langgraph/langgraph/pregel/manager.py new file mode 100644 index 000000000..f70c86d46 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/manager.py @@ -0,0 +1,117 @@ +import asyncio +from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager +from typing import AsyncIterator, Iterator, Mapping, Optional, Union + +from langchain_core.runnables import RunnableConfig + +from langgraph.channels.base import BaseChannel +from langgraph.checkpoint.base import Checkpoint +from langgraph.constants import CONFIG_KEY_STORE +from langgraph.managed.base import ( + ConfiguredManagedValue, + ManagedValueMapping, + ManagedValueSpec, +) +from langgraph.managed.context import Context +from langgraph.store.base import BaseStore +from langgraph.utils.config import patch_configurable + + +@contextmanager +def ChannelsManager( + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + checkpoint: Checkpoint, + config: RunnableConfig, + store: Optional[BaseStore] = None, + *, + skip_context: bool = False, +) -> Iterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]: + """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" + config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store}) + channel_specs: Mapping[str, BaseChannel] = {} + managed_specs: Mapping[str, ManagedValueSpec] = {} + for k, v in specs.items(): + if isinstance(v, BaseChannel): + channel_specs[k] = v + elif ( + skip_context and isinstance(v, ConfiguredManagedValue) and v.cls is Context + ): + managed_specs[k] = Context.of(noop_context) + else: + managed_specs[k] = v + with ExitStack() as stack: + yield ( + { + k: stack.enter_context( + v.from_checkpoint_named(checkpoint["channel_values"].get(k), config) + ) + for k, v in channel_specs.items() + }, + ManagedValueMapping( + { + key: stack.enter_context( + value.cls.enter(config_for_managed, **value.kwargs) + if isinstance(value, ConfiguredManagedValue) + else value.enter(config_for_managed) + ) + for key, value in managed_specs.items() + } + ), + ) + + +@asynccontextmanager +async def AsyncChannelsManager( + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + checkpoint: Checkpoint, + config: RunnableConfig, + store: Optional[BaseStore] = None, + *, + skip_context: bool = False, +) -> AsyncIterator[Mapping[str, BaseChannel]]: + """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" + config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store}) + channel_specs: Mapping[str, BaseChannel] = {} + managed_specs: Mapping[str, ManagedValueSpec] = {} + for k, v in specs.items(): + if isinstance(v, BaseChannel): + channel_specs[k] = v + elif ( + skip_context and isinstance(v, ConfiguredManagedValue) and v.cls is Context + ): + managed_specs[k] = Context.of(noop_context) + else: + managed_specs[k] = v + async with AsyncExitStack() as stack: + # managed: create enter tasks with reference to spec, await them + if tasks := { + asyncio.create_task( + stack.enter_async_context( + value.cls.aenter(config_for_managed, **value.kwargs) + if isinstance(value, ConfiguredManagedValue) + else value.aenter(config_for_managed) + ) + ): key + for key, value in managed_specs.items() + }: + done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) + else: + done = set() + yield ( + # channels: enter each channel with checkpoint + { + k: await stack.enter_async_context( + v.afrom_checkpoint_named( + checkpoint["channel_values"].get(k), config + ) + ) + for k, v in channel_specs.items() + }, + # managed: build mapping from spec to result + ManagedValueMapping({tasks[task]: task.result() for task in done}), + ) + + +@contextmanager +def noop_context() -> Iterator[None]: + yield None diff --git a/libs/langgraph/tests/checkpoint/__init__.py b/libs/langgraph/langgraph/pregel/metadata.py similarity index 100% rename from libs/langgraph/tests/checkpoint/__init__.py rename to libs/langgraph/langgraph/pregel/metadata.py diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index 6e492e2ac..4d9944661 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -1,24 +1,31 @@ from __future__ import annotations -from typing import Any, Callable, Mapping, Optional, Sequence, Union +from functools import cached_property +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + Mapping, + Optional, + Sequence, + Union, +) -from langchain_core.pydantic_v1 import Field from langchain_core.runnables import ( Runnable, RunnableConfig, RunnablePassthrough, - RunnableSequence, RunnableSerializable, ) -from langchain_core.runnables.base import Other, RunnableBindingBase, coerce_to_runnable -from langchain_core.runnables.config import merge_configs +from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable from langchain_core.runnables.utils import ConfigurableFieldSpec from langgraph.constants import CONFIG_KEY_READ -from langgraph.managed.base import ManagedValueSpec from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.write import ChannelWrite -from langgraph.utils import RunnableCallable +from langgraph.utils.config import merge_configs +from langgraph.utils.runnable import RunnableCallable, RunnableSeq READ_TYPE = Callable[[str, bool], Union[Any, dict[str, Any]]] @@ -68,19 +75,19 @@ class ChannelRead(RunnableCallable): def _read(self, _: Any, config: RunnableConfig) -> Any: return self.do_read( - config, channel=self.channel, fresh=self.fresh, mapper=self.mapper + config, select=self.channel, fresh=self.fresh, mapper=self.mapper ) async def _aread(self, _: Any, config: RunnableConfig) -> Any: return self.do_read( - config, channel=self.channel, fresh=self.fresh, mapper=self.mapper + config, select=self.channel, fresh=self.fresh, mapper=self.mapper ) @staticmethod def do_read( config: RunnableConfig, *, - channel: Union[str, list[str]], + select: Union[str, list[str]], fresh: bool = False, mapper: Optional[Callable[[Any], Any]] = None, ) -> Any: @@ -92,30 +99,58 @@ class ChannelRead(RunnableCallable): "Make sure to call in the context of a Pregel process" ) if mapper: - return mapper(read(channel, fresh)) + return mapper(read(select, fresh)) else: - return read(channel, fresh) + return read(select, fresh) DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough() -class PregelNode(RunnableBindingBase): - channels: Union[list[str], Mapping[str, Union[str, ManagedValueSpec]]] +class PregelNode(Runnable): + channels: Union[list[str], Mapping[str, str]] - triggers: list[str] = Field(default_factory=list) + triggers: list[str] - mapper: Optional[Callable[[Any], Any]] = None + mapper: Optional[Callable[[Any], Any]] - writers: list[Runnable] = Field(default_factory=list) + writers: list[Runnable] - bound: Runnable[Any, Any] = Field(default=DEFAULT_BOUND) + bound: Runnable[Any, Any] - kwargs: Mapping[str, Any] = Field(default_factory=dict) + retry_policy: Optional[RetryPolicy] - retry_policy: Optional[RetryPolicy] = None + config: RunnableConfig - def get_writers(self) -> list[Runnable]: + def __init__( + self, + *, + channels: Union[list[str], Mapping[str, str]], + triggers: Sequence[str], + mapper: Optional[Callable[[Any], Any]] = None, + writers: Optional[list[Runnable]] = None, + tags: Optional[list[str]] = None, + metadata: Optional[Mapping[str, Any]] = None, + bound: Optional[Runnable[Any, Any]] = None, + retry_policy: Optional[RetryPolicy] = None, + config: Optional[RunnableConfig] = None, + ) -> None: + self.channels = channels + self.triggers = list(triggers) + self.mapper = mapper + self.writers = writers or [] + self.bound = bound if bound is not None else DEFAULT_BOUND + self.retry_policy = retry_policy + self.config = merge_configs( + config, {"tags": tags or [], "metadata": metadata or {}} + ) + + def copy(self, update: dict[str, Any]) -> PregelNode: + attrs = {**self.__dict__, **update} + return PregelNode(**attrs) + + @cached_property + def flat_writers(self) -> list[Runnable]: """Get writers with optimizations applied.""" writers = self.writers.copy() while ( @@ -133,51 +168,20 @@ class PregelNode(RunnableBindingBase): writers.pop() return writers - def get_node(self) -> Optional[Runnable[Any, Any]]: - writers = self.get_writers() + @cached_property + def node(self) -> Optional[Runnable[Any, Any]]: + writers = self.flat_writers if self.bound is DEFAULT_BOUND and not writers: return None elif self.bound is DEFAULT_BOUND and len(writers) == 1: return writers[0] elif self.bound is DEFAULT_BOUND: - return RunnableSequence(*writers) + return RunnableSeq(*writers) elif writers: - return RunnableSequence(self.bound, *writers) + return RunnableSeq(self.bound, *writers) else: return self.bound - def __init__( - self, - *, - channels: Union[list[str], Mapping[str, str]], - triggers: Sequence[str], - mapper: Optional[Callable[[Any], Any]] = None, - writers: Optional[list[Runnable]] = None, - tags: Optional[list[str]] = None, - metadata: Optional[Mapping[str, Any]] = None, - bound: Optional[Runnable[Any, Any]] = None, - kwargs: Optional[Mapping[str, Any]] = None, - config: Optional[RunnableConfig] = None, - retry_policy: Optional[RetryPolicy] = None, - **other_kwargs: Any, - ) -> None: - super().__init__( - channels=channels, - triggers=triggers, - mapper=mapper, - writers=writers or [], - bound=bound or DEFAULT_BOUND, - kwargs=kwargs or {}, - retry_policy=retry_policy, - config=merge_configs( - config, {"tags": tags or [], "metadata": metadata or {}} - ), - **other_kwargs, - ) - - def __repr_args__(self) -> Any: - return [(k, v) for k, v in super().__repr_args__() if k != "bound"] - def join(self, channels: Sequence[str]) -> PregelNode: assert isinstance(channels, list) or isinstance( channels, tuple @@ -207,7 +211,7 @@ class PregelNode(RunnableBindingBase): elif self.bound is DEFAULT_BOUND: return self.copy(update=dict(bound=coerce_to_runnable(other))) else: - return self.copy(update=dict(bound=self.bound | other)) + return self.copy(update=dict(bound=RunnableSeq(self.bound, other))) def pipe( self, @@ -227,3 +231,42 @@ class PregelNode(RunnableBindingBase): ], ) -> RunnableSerializable: raise NotImplementedError() + + def invoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + return self.bound.invoke(input, merge_configs(self.config, config), **kwargs) + + async def ainvoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + return await self.bound.ainvoke( + input, merge_configs(self.config, config), **kwargs + ) + + def stream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Iterator[Output]: + yield from self.bound.stream( + input, merge_configs(self.config, config), **kwargs + ) + + async def astream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[Output]: + async for item in self.bound.astream( + input, merge_configs(self.config, config), **kwargs + ): + yield item diff --git a/libs/langgraph/langgraph/pregel/retry.py b/libs/langgraph/langgraph/pregel/retry.py index 88940b584..486584809 100644 --- a/libs/langgraph/langgraph/pregel/retry.py +++ b/libs/langgraph/langgraph/pregel/retry.py @@ -4,6 +4,7 @@ import random import time from typing import Optional +from langgraph.errors import GraphInterrupt from langgraph.pregel.types import PregelExecutableTask, RetryPolicy logger = logging.getLogger(__name__) @@ -25,6 +26,9 @@ def run_with_retry( task.proc.invoke(task.input, task.config) # if successful, end break + except GraphInterrupt: + # if interrupted, end + raise except Exception as exc: if retry_policy is None: raise @@ -75,6 +79,9 @@ async def arun_with_retry( await task.proc.ainvoke(task.input, task.config) # if successful, end break + except GraphInterrupt: + # if interrupted, end + raise except Exception as exc: if retry_policy is None: raise diff --git a/libs/langgraph/langgraph/pregel/runner.py b/libs/langgraph/langgraph/pregel/runner.py new file mode 100644 index 000000000..c895bf9e9 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/runner.py @@ -0,0 +1,197 @@ +import asyncio +import concurrent.futures +import time +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + Optional, + Sequence, + Type, + Union, +) + +from langgraph.constants import ERROR, INTERRUPT +from langgraph.errors import GraphInterrupt +from langgraph.pregel.executor import Submit +from langgraph.pregel.retry import arun_with_retry, run_with_retry +from langgraph.pregel.types import PregelExecutableTask, RetryPolicy + + +class PregelRunner: + def __init__( + self, + *, + submit: Submit, + put_writes: Callable[[str, Sequence[tuple[str, Any]]], None], + use_astream: bool = False, + ) -> None: + self.submit = submit + self.put_writes = put_writes + self.use_astream = use_astream + + def tick( + self, + tasks: list[PregelExecutableTask], + *, + timeout: Optional[float] = None, + retry_policy: Optional[RetryPolicy] = None, + ) -> Iterator[None]: + # give control back to the caller + yield + # execute tasks, and wait for one to fail or all to finish. + # each task is independent from all other concurrent tasks + # yield updates/debug output as each task finishes + futures = { + self.submit( + run_with_retry, + task, + retry_policy, + ): task + for task in tasks + if not task.writes + } + all_futures = futures.copy() + end_time = timeout + time.monotonic() if timeout else None + while futures: + done, _ = concurrent.futures.wait( + futures, + return_when=concurrent.futures.FIRST_COMPLETED, + timeout=(max(0, end_time - time.monotonic()) if end_time else None), + ) + if not done: + break # timed out + for fut in done: + task = futures.pop(fut) + if exc := _exception(fut): + if isinstance(exc, GraphInterrupt): + # save interrupt to checkpointer + self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]]) + else: + # save error to checkpointer + self.put_writes(task.id, [(ERROR, exc)]) + + else: + # save task writes to checkpointer + self.put_writes(task.id, task.writes) + else: + # remove references to loop vars + del fut, task + # maybe stop other tasks + if _should_stop_others(done): + break + # give control back to the caller + yield + # panic on failure or timeout + _panic_or_proceed(all_futures) + + async def atick( + self, + tasks: list[PregelExecutableTask], + *, + timeout: Optional[float] = None, + retry_policy: Optional[RetryPolicy] = None, + ) -> AsyncIterator[None]: + loop = asyncio.get_event_loop() + # give control back to the caller + yield + # execute tasks, and wait for one to fail or all to finish. + # each task is independent from all other concurrent tasks + # yield updates/debug output as each task finishes + futures = { + self.submit( + arun_with_retry, + task, + retry_policy, + stream=self.use_astream, + __name__=task.name, + __cancel_on_exit__=True, + ): task + for task in tasks + if not task.writes + } + all_futures = futures.copy() + end_time = timeout + loop.time() if timeout else None + while futures: + done, _ = await asyncio.wait( + futures, + return_when=asyncio.FIRST_COMPLETED, + timeout=(max(0, end_time - loop.time()) if end_time else None), + ) + if not done: + break # timed out + for fut in done: + task = futures.pop(fut) + if exc := _exception(fut): + if isinstance(exc, GraphInterrupt): + # save interrupt to checkpointer + self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]]) + else: + # save error to checkpointer + self.put_writes(task.id, [(ERROR, exc)]) + else: + # save task writes to checkpointer + self.put_writes(task.id, task.writes) + else: + # remove references to loop vars + del fut, task + # maybe stop other tasks + if _should_stop_others(done): + break + # give control back to the caller + yield + # panic on failure or timeout + _panic_or_proceed(all_futures, asyncio.TimeoutError) + + +def _should_stop_others( + done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], +) -> bool: + for fut in done: + if fut.cancelled(): + return True + if exc := fut.exception(): + return not isinstance(exc, GraphInterrupt) + else: + return False + + +def _exception( + fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]], +) -> Optional[BaseException]: + if fut.cancelled(): + if isinstance(fut, asyncio.Task): + return asyncio.CancelledError() + else: + return concurrent.futures.CancelledError() + else: + return fut.exception() + + +def _panic_or_proceed( + futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], + timeout_exc_cls: Type[Exception] = TimeoutError, +) -> None: + done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + for fut in futs: + if fut.done(): + done.add(fut) + else: + inflight.add(fut) + while done: + # if any task failed + if exc := _exception(done.pop()): + # cancel all pending tasks + while inflight: + inflight.pop().cancel() + # raise the exception + raise exc + if inflight: + # if we got here means we timed out + while inflight: + # cancel all pending tasks + inflight.pop().cancel() + # raise timeout error + raise timeout_exc_cls("Timed out") diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 8343329a4..80d0d8a23 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -4,6 +4,7 @@ from typing import Any, Callable, Literal, NamedTuple, Optional, Type, Union from langchain_core.runnables import Runnable, RunnableConfig from langgraph.checkpoint.base import CheckpointMetadata +from langgraph.constants import Interrupt def default_retry_on(exc: Exception) -> bool: @@ -56,10 +57,18 @@ class RetryPolicy(NamedTuple): """List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry.""" +class CachePolicy(NamedTuple): + """Configuration for caching nodes.""" + + pass + + class PregelTask(NamedTuple): id: str name: str error: Optional[Exception] = None + interrupts: tuple[Interrupt, ...] = () + state: Union[None, RunnableConfig, "StateSnapshot"] = None class PregelExecutableTask(NamedTuple): @@ -70,7 +79,10 @@ class PregelExecutableTask(NamedTuple): config: RunnableConfig triggers: list[str] retry_policy: Optional[RetryPolicy] + cache_policy: Optional[CachePolicy] id: str + path: tuple[str, ...] + scheduled: bool = False class StateSnapshot(NamedTuple): diff --git a/libs/langgraph/langgraph/pregel/write.py b/libs/langgraph/langgraph/pregel/write.py index e85885cc9..fd732966a 100644 --- a/libs/langgraph/langgraph/pregel/write.py +++ b/libs/langgraph/langgraph/pregel/write.py @@ -4,11 +4,9 @@ import asyncio from typing import ( Any, Callable, - List, NamedTuple, Optional, Sequence, - Tuple, TypeVar, Union, ) @@ -18,7 +16,7 @@ from langchain_core.runnables.utils import ConfigurableFieldSpec from langgraph.constants import CONFIG_KEY_SEND, TASKS, Send from langgraph.errors import InvalidUpdateError -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None] R = TypeVar("R", bound=Runnable) @@ -32,7 +30,7 @@ class ChannelWriteEntry(NamedTuple): channel: str value: Any = PASSTHROUGH skip_none: bool = False - mapper: Optional[Runnable] = None + mapper: Optional[Callable] = None class ChannelWrite(RunnableCallable): @@ -59,9 +57,6 @@ class ChannelWrite(RunnableCallable): self.writes = writes self.require_at_least_one_of = require_at_least_one_of - def __repr_args__(self) -> Any: - return [("writes", self.writes)] - def get_name( self, suffix: Optional[str] = None, *, name: Optional[str] = None ) -> str: @@ -82,65 +77,29 @@ class ChannelWrite(RunnableCallable): ] def _write(self, input: Any, config: RunnableConfig) -> None: - # split packets and entries - writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)] - entries = [ - write for write in self.writes if isinstance(write, ChannelWriteEntry) + writes = [ + ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper) + if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH + else write + for write in self.writes ] - for entry in entries: - if entry.channel == TASKS: - raise InvalidUpdateError("Cannot write to the reserved channel TASKS") - # process entries into values - values = [ - input if write.value is PASSTHROUGH else write.value for write in entries - ] - values = [ - val if write.mapper is None else write.mapper.invoke(val, config) - for val, write in zip(values, entries) - ] - values = [ - (write.channel, val) - for val, write in zip(values, entries) - if not write.skip_none or val is not None - ] - # write packets and values self.do_write( config, - writes + values, + writes, self.require_at_least_one_of if input is not None else None, ) return input async def _awrite(self, input: Any, config: RunnableConfig) -> None: - # split packets and entries - writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)] - entries = [ - write for write in self.writes if isinstance(write, ChannelWriteEntry) + writes = [ + ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper) + if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH + else write + for write in self.writes ] - for entry in entries: - if entry.channel == TASKS: - raise InvalidUpdateError("Cannot write to the reserved channel TASKS") - # process entries into values - values = [ - input if write.value is PASSTHROUGH else write.value for write in entries - ] - values = await asyncio.gather( - *( - _mk_future(val) - if write.mapper is None - else write.mapper.ainvoke(val, config) - for val, write in zip(values, entries) - ) - ) - values = [ - (write.channel, val) - for val, write in zip(values, entries) - if not write.skip_none or val is not None - ] - # write packets and values self.do_write( config, - writes + values, + writes, self.require_at_least_one_of if input is not None else None, ) return input @@ -148,9 +107,32 @@ class ChannelWrite(RunnableCallable): @staticmethod def do_write( config: RunnableConfig, - values: List[Tuple[str, Any]], + writes: Sequence[Union[ChannelWriteEntry, Send]], require_at_least_one_of: Optional[Sequence[str]] = None, ) -> None: + # validate + for w in writes: + if isinstance(w, ChannelWriteEntry): + if w.channel == TASKS: + raise InvalidUpdateError( + "Cannot write to the reserved channel TASKS" + ) + if w.value is PASSTHROUGH: + raise InvalidUpdateError("PASSTHROUGH value must be replaced") + # split packets and entries + sends = [(TASKS, packet) for packet in writes if isinstance(packet, Send)] + entries = [write for write in writes if isinstance(write, ChannelWriteEntry)] + # process entries into values + values = [ + write.mapper(write.value) if write.mapper is not None else write.value + for write in entries + ] + values = [ + (write.channel, val) + for val, write in zip(values, entries) + if not write.skip_none or val is not None + ] + # filter out SKIP_WRITE values filtered = [(chan, val) for chan, val in values if val is not SKIP_WRITE] if require_at_least_one_of is not None: if not {chan for chan, _ in filtered} & set(require_at_least_one_of): @@ -158,7 +140,7 @@ class ChannelWrite(RunnableCallable): f"Must write to at least one of {require_at_least_one_of}" ) write: TYPE_SEND = config["configurable"][CONFIG_KEY_SEND] - write(filtered) + write(sends + filtered) @staticmethod def is_writer(runnable: Runnable) -> bool: diff --git a/libs/langgraph/langgraph/store/__init__.py b/libs/langgraph/langgraph/store/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/langgraph/langgraph/store/base.py b/libs/langgraph/langgraph/store/base.py new file mode 100644 index 000000000..7f0030f56 --- /dev/null +++ b/libs/langgraph/langgraph/store/base.py @@ -0,0 +1,21 @@ +from typing import Any, List, Optional + +V = dict[str, Any] + + +class BaseStore: + def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + # list[namespace] -> dict[namespace, list[value]] + raise NotImplementedError + + def put(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + # list[(namespace, key, value | none)] -> None + raise NotImplementedError + + async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + # list[namespace] -> dict[namespace, list[value]] + raise NotImplementedError + + async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + # list[(namespace, key, value | none)] -> None + raise NotImplementedError diff --git a/libs/langgraph/langgraph/store/batch.py b/libs/langgraph/langgraph/store/batch.py new file mode 100644 index 000000000..54eb20d47 --- /dev/null +++ b/libs/langgraph/langgraph/store/batch.py @@ -0,0 +1,65 @@ +import asyncio +from typing import NamedTuple, Optional, Union + +from langgraph.store.base import BaseStore, V + + +class ListOp(NamedTuple): + prefixes: list[str] + + +class PutOp(NamedTuple): + writes: list[tuple[str, str, Optional[V]]] + + +class AsyncBatchedStore(BaseStore): + def __init__(self, store: BaseStore) -> None: + self.store = store + self.aqueue: dict[asyncio.Future, Union[ListOp, PutOp]] = {} + self.task = asyncio.create_task(_run(self.aqueue, self.store)) + + def __del__(self) -> None: + self.task.cancel() + + async def alist(self, prefixes: list[str]) -> dict[str, dict[str, V]]: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = ListOp(prefixes) + return await fut + + async def aput(self, writes: list[tuple[str, str, Optional[V]]]) -> None: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = PutOp(writes) + return await fut + + +async def _run( + aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], store: BaseStore +) -> None: + while True: + await asyncio.sleep(0) + if not aqueue: + continue + # this could use a lock, if we want thread safety + taken = aqueue.copy() + aqueue.clear() + # action each operation + lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} + if lists: + try: + results = await store.alist( + [p for op in lists.values() for p in op.prefixes] + ) + for fut, op in lists.items(): + fut.set_result({k: results.get(k) for k in op.prefixes}) + except Exception as e: + for fut in lists: + fut.set_exception(e) + puts = {f: o for f, o in taken.items() if isinstance(o, PutOp)} + if puts: + try: + await store.aput([w for op in puts.values() for w in op.writes]) + for fut in puts: + fut.set_result(None) + except Exception as e: + for fut in puts: + fut.set_exception(e) diff --git a/libs/langgraph/langgraph/store/memory.py b/libs/langgraph/langgraph/store/memory.py new file mode 100644 index 000000000..48fa2884f --- /dev/null +++ b/libs/langgraph/langgraph/store/memory.py @@ -0,0 +1,25 @@ +from collections import defaultdict +from typing import List, Optional + +from langgraph.store.base import BaseStore, V + + +class MemoryStore(BaseStore): + def __init__(self) -> None: + self.data: dict[str, dict[str, V]] = defaultdict(dict) + + def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + return {prefix: self.data[prefix] for prefix in prefixes} + + async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + return self.list(prefixes) + + def put(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + for namespace, key, value in writes: + if value is None: + self.data[namespace].pop(key, None) + else: + self.data[namespace][key] = value + + async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + return self.put(writes) diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils.py deleted file mode 100644 index 64cb21128..000000000 --- a/libs/langgraph/langgraph/utils.py +++ /dev/null @@ -1,185 +0,0 @@ -import asyncio -import enum -import inspect -import sys -from contextvars import copy_context -from functools import partial, wraps -from typing import Any, AsyncIterator, Awaitable, Callable, Optional - -from langchain_core.runnables.base import ( - Runnable, - RunnableConfig, - RunnableLambda, - RunnableLike, - RunnableParallel, -) -from langchain_core.runnables.config import ( - merge_configs, - run_in_executor, - var_child_runnable_config, -) -from langchain_core.runnables.utils import accepts_config -from typing_extensions import TypeGuard - -try: - from langchain_core.runnables.config import _set_config_context -except ImportError: - # For forwards compatibility - def _set_config_context(context: RunnableConfig) -> None: # type: ignore - """Set the context for the current thread.""" - var_child_runnable_config.set(context) - - -# Before Python 3.11 native StrEnum is not available -class StrEnum(str, enum.Enum): - """A string enum.""" - - pass - - -class RunnableCallable(Runnable): - """A much simpler version of RunnableLambda that requires sync and async functions.""" - - def __init__( - self, - func: Callable[..., Optional[Runnable]], - afunc: Optional[Callable[..., Awaitable[Optional[Runnable]]]] = None, - *, - name: Optional[str] = None, - tags: Optional[list[str]] = None, - trace: bool = True, - recurse: bool = True, - **kwargs: Any, - ) -> None: - if name is not None: - self.name = name - elif func: - try: - if func.__name__ != "": - self.name = func.__name__ - except AttributeError: - pass - elif afunc: - try: - self.name = afunc.__name__ - except AttributeError: - pass - self.func = func - self.afunc = afunc - self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None - self.kwargs = kwargs - self.trace = trace - self.recurse = recurse - - def __repr__(self) -> str: - repr_args = { - k: v - for k, v in self.__dict__.items() - if k not in {"name", "func", "afunc", "config", "kwargs", "trace"} - } - return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})" - - def invoke( - self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any - ) -> Any: - if self.func is None: - raise TypeError( - f'No synchronous function provided to "{self.name}".' - "\nEither initialize with a synchronous function or invoke" - " via the async API (ainvoke, astream, etc.)" - ) - kwargs = {**self.kwargs, **kwargs} - if self.trace: - ret = self._call_with_config( - self.func, input, merge_configs(self.config, config), **kwargs - ) - else: - config = merge_configs(self.config, config) - context = copy_context() - context.run(_set_config_context, config) - if accepts_config(self.func): - kwargs["config"] = config - ret = context.run(self.func, input, **kwargs) - if isinstance(ret, Runnable) and self.recurse: - return ret.invoke(input, config) - return ret - - async def ainvoke( - self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any - ) -> Any: - if not self.afunc: - return self.invoke(input, config) - kwargs = {**self.kwargs, **kwargs} - if self.trace: - ret = await self._acall_with_config( - self.afunc, input, merge_configs(self.config, config), **kwargs - ) - else: - config = merge_configs(self.config, config) - context = copy_context() - context.run(_set_config_context, config) - if accepts_config(self.afunc): - kwargs["config"] = config - if sys.version_info >= (3, 11): - ret = await asyncio.create_task( - self.afunc(input, **kwargs), context=context - ) - else: - ret = await self.afunc(input, **kwargs) - if isinstance(ret, Runnable) and self.recurse: - return await ret.ainvoke(input, config) - return ret - - -def is_async_callable( - func: Any, -) -> TypeGuard[Callable[..., Awaitable]]: - """Check if a function is async.""" - return ( - asyncio.iscoroutinefunction(func) - or hasattr(func, "__call__") - and asyncio.iscoroutinefunction(func.__call__) - ) - - -def is_async_generator( - func: Any, -) -> TypeGuard[Callable[..., AsyncIterator]]: - """Check if a function is an async generator.""" - return ( - inspect.isasyncgenfunction(func) - or hasattr(func, "__call__") - and inspect.isasyncgenfunction(func.__call__) - ) - - -def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnable: - """Coerce a runnable-like object into a Runnable. - - Args: - thing: A runnable-like object. - - Returns: - A Runnable. - """ - if isinstance(thing, Runnable): - return thing - elif is_async_generator(thing) or inspect.isgeneratorfunction(thing): - return RunnableLambda(thing, name=name) - elif callable(thing): - if is_async_callable(thing): - return RunnableCallable(None, thing, name=name, trace=trace) - else: - return RunnableCallable( - thing, - wraps(thing)(partial(run_in_executor, None, thing)), - name=name, - trace=trace, - ) - elif isinstance(thing, dict): - return RunnableParallel(thing) - else: - raise TypeError( - f"Expected a Runnable, callable or dict." - f"Instead got an unsupported type: {type(thing)}" - ) diff --git a/libs/langgraph/langgraph/utils/__init__.py b/libs/langgraph/langgraph/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/utils/config.py new file mode 100644 index 000000000..461c89024 --- /dev/null +++ b/libs/langgraph/langgraph/utils/config.py @@ -0,0 +1,152 @@ +from typing import Any, Optional + +from langchain_core.callbacks import Callbacks +from langchain_core.runnables import RunnableConfig +from langchain_core.runnables.config import COPIABLE_KEYS, DEFAULT_RECURSION_LIMIT + +from langgraph.checkpoint.base import CheckpointMetadata +from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP + + +def patch_configurable( + config: Optional[RunnableConfig], patch: dict[str, Any] +) -> RunnableConfig: + if config is None: + return {"configurable": patch} + elif "configurable" not in config: + return {**config, "configurable": patch} + else: + return {**config, "configurable": {**config["configurable"], **patch}} + + +def patch_checkpoint_map( + config: RunnableConfig, metadata: Optional[CheckpointMetadata] +) -> RunnableConfig: + if parents := (metadata.get("parents") if metadata else None): + return patch_configurable( + config, + { + CONFIG_KEY_CHECKPOINT_MAP: { + **parents, + config["configurable"]["checkpoint_ns"]: config["configurable"][ + "checkpoint_id" + ], + }, + }, + ) + else: + return config + + +def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig: + """Merge multiple configs into one. + + Args: + *configs (Optional[RunnableConfig]): The configs to merge. + + Returns: + RunnableConfig: The merged config. + """ + base: RunnableConfig = {} + # Even though the keys aren't literals, this is correct + # because both dicts are the same type + for config in configs: + if config is None: + continue + for key in config: + if key == "metadata": + base[key] = { # type: ignore + **base.get(key, {}), # type: ignore + **(config.get(key) or {}), # type: ignore + } + elif key == "tags": + base[key] = sorted( # type: ignore + set(base.get(key, []) + (config.get(key) or [])), # type: ignore + ) + elif key == "configurable": + base[key] = { # type: ignore + **base.get(key, {}), # type: ignore + **(config.get(key) or {}), # type: ignore + } + elif key == "callbacks": + base_callbacks = base.get("callbacks") + these_callbacks = config["callbacks"] + # callbacks can be either None, list[handler] or manager + # so merging two callbacks values has 6 cases + if isinstance(these_callbacks, list): + if base_callbacks is None: + base["callbacks"] = these_callbacks.copy() + elif isinstance(base_callbacks, list): + base["callbacks"] = base_callbacks + these_callbacks + else: + # base_callbacks is a manager + mngr = base_callbacks.copy() + for callback in these_callbacks: + mngr.add_handler(callback, inherit=True) + base["callbacks"] = mngr + elif these_callbacks is not None: + # these_callbacks is a manager + if base_callbacks is None: + base["callbacks"] = these_callbacks.copy() + elif isinstance(base_callbacks, list): + mngr = these_callbacks.copy() + for callback in base_callbacks: + mngr.add_handler(callback, inherit=True) + base["callbacks"] = mngr + else: + # base_callbacks is also a manager + base["callbacks"] = base_callbacks.merge(these_callbacks) + elif key == "recursion_limit": + if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT: + base["recursion_limit"] = config["recursion_limit"] + elif key in COPIABLE_KEYS and config[key] is not None: # type: ignore[literal-required] + base[key] = config[key].copy() # type: ignore[literal-required] + else: + base[key] = config[key] or base.get(key) # type: ignore + return base + + +def patch_config( + config: Optional[RunnableConfig], + *, + callbacks: Optional[Callbacks] = None, + recursion_limit: Optional[int] = None, + max_concurrency: Optional[int] = None, + run_name: Optional[str] = None, + configurable: Optional[dict[str, Any]] = None, +) -> RunnableConfig: + """Patch a config with new values. + + Args: + config (Optional[RunnableConfig]): The config to patch. + callbacks (Optional[BaseCallbackManager], optional): The callbacks to set. + Defaults to None. + recursion_limit (Optional[int], optional): The recursion limit to set. + Defaults to None. + max_concurrency (Optional[int], optional): The max concurrency to set. + Defaults to None. + run_name (Optional[str], optional): The run name to set. Defaults to None. + configurable (Optional[Dict[str, Any]], optional): The configurable to set. + Defaults to None. + + Returns: + RunnableConfig: The patched config. + """ + config = config.copy() or {} + if callbacks is not None: + # If we're replacing callbacks, we need to unset run_name + # As that should apply only to the same run as the original callbacks + config["callbacks"] = callbacks + if "run_name" in config: + del config["run_name"] + if "run_id" in config: + del config["run_id"] + if recursion_limit is not None: + config["recursion_limit"] = recursion_limit + if max_concurrency is not None: + config["max_concurrency"] = max_concurrency + if run_name is not None: + config["run_name"] = run_name + if configurable is not None: + config["configurable"] = {**config.get("configurable", {}), **configurable} + return config diff --git a/libs/langgraph/langgraph/utils/fields.py b/libs/langgraph/langgraph/utils/fields.py new file mode 100644 index 000000000..a4c29a9ec --- /dev/null +++ b/libs/langgraph/langgraph/utils/fields.py @@ -0,0 +1,108 @@ +import dataclasses +from typing import Any, Optional, Type, Union + +from typing_extensions import Annotated, NotRequired, ReadOnly, Required, get_origin + + +def _is_optional_type(type_: Any) -> bool: + """Check if a type is Optional.""" + + if hasattr(type_, "__origin__") and hasattr(type_, "__args__"): + origin = get_origin(type_) + if origin is Optional: + return True + if origin is Union: + return any( + arg is type(None) or _is_optional_type(arg) for arg in type_.__args__ + ) + if origin is Annotated: + return _is_optional_type(type_.__args__[0]) + return origin is None + if hasattr(type_, "__bound__") and type_.__bound__ is not None: + return _is_optional_type(type_.__bound__) + return type_ is None + + +def _is_required_type(type_: Any) -> Optional[bool]: + """Check if an annotation is marked as Required/NotRequired. + + Returns: + - True if required + - False if not required + - None if not annotated with either + """ + origin = get_origin(type_) + if origin is Required: + return True + if origin is NotRequired: + return False + if origin is Annotated or getattr(origin, "__args__", None): + # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated + return _is_required_type(type_.__args__[0]) + return None + + +def _is_readonly_type(type_: Any) -> bool: + """Check if an annotation is marked as ReadOnly. + + Returns: + - True if is read only + - False if not read only + """ + + # See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier + origin = get_origin(type_) + if origin is Annotated: + return _is_readonly_type(type_.__args__[0]) + if origin is ReadOnly: + return True + return False + + +_DEFAULT_KEYS = frozenset() + + +def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any: + """Determine the default value for a field in a state schema. + + This is based on: + If TypedDict: + - Required/NotRequired + - total=False -> everything optional + - Type annotation (Optional/Union[None]) + """ + optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS) + irq = _is_required_type(type_) + if name in optional_keys: + # Either total=False or explicit NotRequired. + # No type annotation trumps this. + if irq: + # Unless it's earlier versions of python & explicit Required + return ... + return None + if irq is not None: + if irq: + # Handle Required[] + # (we already handled NotRequired and total=False) + return ... + # Handle NotRequired[] for earlier versions of python + return None + if dataclasses.is_dataclass(schema): + field_info = next( + (f for f in dataclasses.fields(schema) if f.name == name), None + ) + if field_info: + if ( + field_info.default is not dataclasses.MISSING + and field_info.default is not ... + ): + return field_info.default + elif field_info.default_factory is not dataclasses.MISSING: + return field_info.default_factory() + # Note, we ignore ReadOnly attributes, + # as they don't make much sense. (we don't care if you mutate the state in your node) + # and mutating state in your node has no effect on our graph state. + # Base case is the annotation + if _is_optional_type(type_): + return None + return ... diff --git a/libs/langgraph/langgraph/utils/runnable.py b/libs/langgraph/langgraph/utils/runnable.py new file mode 100644 index 000000000..c59bd0b18 --- /dev/null +++ b/libs/langgraph/langgraph/utils/runnable.py @@ -0,0 +1,519 @@ +import asyncio +import enum +import inspect +import sys +from contextlib import AsyncExitStack +from contextvars import copy_context +from functools import partial, wraps +from typing import Any, AsyncIterator, Awaitable, Callable, Iterator, Optional + +from langchain_core.runnables.base import ( + Runnable, + RunnableConfig, + RunnableLambda, + RunnableLike, + RunnableParallel, + RunnableSequence, +) +from langchain_core.runnables.config import ( + ensure_config, + get_async_callback_manager_for_config, + get_callback_manager_for_config, + run_in_executor, + var_child_runnable_config, +) +from langchain_core.runnables.utils import Input, Output, accepts_config +from langchain_core.tracers._streaming import _StreamingCallbackHandler +from typing_extensions import TypeGuard + +from langgraph.utils.config import merge_configs, patch_config + +try: + from langchain_core.runnables.config import _set_config_context +except ImportError: + # For forwards compatibility + def _set_config_context(context: RunnableConfig) -> None: # type: ignore + """Set the context for the current thread.""" + var_child_runnable_config.set(context) + + +# Before Python 3.11 native StrEnum is not available +class StrEnum(str, enum.Enum): + """A string enum.""" + + +ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11) + + +class RunnableCallable(Runnable): + """A much simpler version of RunnableLambda that requires sync and async functions.""" + + def __init__( + self, + func: Callable[..., Optional[Runnable]], + afunc: Optional[Callable[..., Awaitable[Optional[Runnable]]]] = None, + *, + name: Optional[str] = None, + tags: Optional[list[str]] = None, + trace: bool = True, + recurse: bool = True, + **kwargs: Any, + ) -> None: + self.name = name + if self.name is None: + if func: + try: + if func.__name__ != "": + self.name = func.__name__ + except AttributeError: + pass + elif afunc: + try: + self.name = afunc.__name__ + except AttributeError: + pass + self.func = func + if func is not None: + self.func_accepts_config = accepts_config(func) + self.afunc = afunc + if afunc is not None: + self.afunc_accepts_config = accepts_config(afunc) + self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None + self.kwargs = kwargs + self.trace = trace + self.recurse = recurse + + def __repr__(self) -> str: + repr_args = { + k: v + for k, v in self.__dict__.items() + if k not in {"name", "func", "afunc", "config", "kwargs", "trace"} + } + return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})" + + def invoke( + self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any + ) -> Any: + if self.func is None: + raise TypeError( + f'No synchronous function provided to "{self.name}".' + "\nEither initialize with a synchronous function or invoke" + " via the async API (ainvoke, astream, etc.)" + ) + kwargs = {**self.kwargs, **kwargs} + if self.func_accepts_config: + kwargs["config"] = config + config = ensure_config(merge_configs(self.config, config)) + context = copy_context() + if self.trace: + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + try: + child_config = patch_config(config, callbacks=run_manager.get_child()) + context = copy_context() + context.run(_set_config_context, child_config) + ret = context.run(self.func, input, **kwargs) + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(ret) + else: + context.run(_set_config_context, config) + ret = context.run(self.func, input, **kwargs) + if isinstance(ret, Runnable) and self.recurse: + return ret.invoke(input, config) + return ret + + async def ainvoke( + self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any + ) -> Any: + if not self.afunc: + return self.invoke(input, config) + kwargs = {**self.kwargs, **kwargs} + if self.afunc_accepts_config: + kwargs["config"] = config + config = ensure_config(merge_configs(self.config, config)) + context = copy_context() + if self.trace: + callback_manager = get_async_callback_manager_for_config(config) + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.name, + run_id=config.pop("run_id", None), + ) + try: + child_config = patch_config(config, callbacks=run_manager.get_child()) + context.run(_set_config_context, child_config) + coro = self.afunc(input, **kwargs) + if ASYNCIO_ACCEPTS_CONTEXT: + ret = await asyncio.create_task(coro, context=context) + else: + ret = await coro + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(ret) + else: + context.run(_set_config_context, config) + if ASYNCIO_ACCEPTS_CONTEXT: + ret = await asyncio.create_task( + self.afunc(input, **kwargs), context=context + ) + else: + ret = await self.afunc(input, **kwargs) + if isinstance(ret, Runnable) and self.recurse: + return await ret.ainvoke(input, config) + return ret + + +def is_async_callable( + func: Any, +) -> TypeGuard[Callable[..., Awaitable]]: + """Check if a function is async.""" + return ( + asyncio.iscoroutinefunction(func) + or hasattr(func, "__call__") + and asyncio.iscoroutinefunction(func.__call__) + ) + + +def is_async_generator( + func: Any, +) -> TypeGuard[Callable[..., AsyncIterator]]: + """Check if a function is an async generator.""" + return ( + inspect.isasyncgenfunction(func) + or hasattr(func, "__call__") + and inspect.isasyncgenfunction(func.__call__) + ) + + +def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnable: + """Coerce a runnable-like object into a Runnable. + + Args: + thing: A runnable-like object. + + Returns: + A Runnable. + """ + if isinstance(thing, Runnable): + return thing + elif is_async_generator(thing) or inspect.isgeneratorfunction(thing): + return RunnableLambda(thing, name=name) + elif callable(thing): + if is_async_callable(thing): + return RunnableCallable(None, thing, name=name, trace=trace) + else: + return RunnableCallable( + thing, + wraps(thing)(partial(run_in_executor, None, thing)), + name=name, + trace=trace, + ) + elif isinstance(thing, dict): + return RunnableParallel(thing) + else: + raise TypeError( + f"Expected a Runnable, callable or dict." + f"Instead got an unsupported type: {type(thing)}" + ) + + +class RunnableSeq(Runnable): + """A simpler version of RunnableSequence.""" + + def __init__( + self, + *steps: RunnableLike, + name: Optional[str] = None, + ) -> None: + """Create a new RunnableSequence. + + Args: + steps: The steps to include in the sequence. + name: The name of the Runnable. Defaults to None. + first: The first Runnable in the sequence. Defaults to None. + middle: The middle Runnables in the sequence. Defaults to None. + last: The last Runnable in the sequence. Defaults to None. + + Raises: + ValueError: If the sequence has less than 2 steps. + """ + steps_flat: list[Runnable] = [] + for step in steps: + if isinstance(step, RunnableSequence): + steps_flat.extend(step.steps) + elif isinstance(step, RunnableSeq): + steps_flat.extend(step.steps) + else: + steps_flat.append(coerce_to_runnable(step, name=None, trace=True)) + if len(steps_flat) < 2: + raise ValueError( + f"RunnableSeq must have at least 2 steps, got {len(steps_flat)}" + ) + self.steps = steps_flat + self.name = name + + def __or__( + self, + other: Any, + ) -> Runnable: + if isinstance(other, RunnableSequence): + return RunnableSeq( + *self.steps, + other.first, + *other.middle, + other.last, + name=self.name or other.name, + ) + elif isinstance(other, RunnableSeq): + return RunnableSeq( + *self.steps, + *other.steps, + name=self.name or other.name, + ) + else: + return RunnableSeq( + *self.steps, + coerce_to_runnable(other), + name=self.name, + ) + + def __ror__( + self, + other: Any, + ) -> Runnable: + if isinstance(other, RunnableSequence): + return RunnableSequence( + other.first, + *other.middle, + other.last, + *self.steps, + name=other.name or self.name, + ) + elif isinstance(other, RunnableSeq): + return RunnableSeq( + *other.steps, + *self.steps, + name=other.name or self.name, + ) + else: + return RunnableSequence( + coerce_to_runnable(other), + *self.steps, + name=self.name, + ) + + def invoke( + self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any + ) -> Output: + # setup callbacks and context + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + # start the root run + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + # invoke all steps in sequence + try: + for i, step in enumerate(self.steps): + # mark each step as a child run + config = patch_config( + config, callbacks=run_manager.get_child(f"seq:step:{i+1}") + ) + context = copy_context() + context.run(_set_config_context, config) + if i == 0: + input = context.run(step.invoke, input, config, **kwargs) + else: + input = context.run(step.invoke, input, config) + # finish the root run + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(input) + return input + + async def ainvoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + # setup callbacks + config = ensure_config(config) + callback_manager = get_async_callback_manager_for_config(config) + # start the root run + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + # invoke all steps in sequence + try: + for i, step in enumerate(self.steps): + # mark each step as a child run + config = patch_config( + config, callbacks=run_manager.get_child(f"seq:step:{i+1}") + ) + context = copy_context() + context.run(_set_config_context, config) + if i == 0: + coro = step.ainvoke(input, config, **kwargs) + else: + coro = step.ainvoke(input, config) + if ASYNCIO_ACCEPTS_CONTEXT: + input = await asyncio.create_task(coro, context=context) + else: + input = await asyncio.create_task(coro) + # finish the root run + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(input) + return input + + def stream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Iterator[Output]: + # setup callbacks + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + # start the root run + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + try: + # stream the last steps + # transform the input stream of each step with the next + # steps that don't natively support transforming an input stream will + # buffer input in memory until all available, and then start emitting output + for idx, step in enumerate(self.steps): + config = patch_config( + config, + callbacks=run_manager.get_child(f"seq:step:{idx+1}"), + ) + if idx == 0: + iterator = step.stream(input, config, **kwargs) + else: + iterator = step.transform(iterator, config) + if stream_handler := next( + ( + h + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + ), + None, + ): + # populates streamed_output in astream_log() output if needed + iterator = stream_handler.tap_output_iter(run_manager.run_id, iterator) + output: Output = None + add_supported = False + for chunk in iterator: + yield chunk + # collect final output + if output is None: + output = chunk + elif add_supported: + try: + output = output + chunk + except TypeError: + output = chunk + add_supported = False + else: + output = chunk + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(output) + + async def astream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[Output]: + # setup callbacks + config = ensure_config(config) + callback_manager = get_async_callback_manager_for_config(config) + # start the root run + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + try: + async with AsyncExitStack() as stack: + # stream the last steps + # transform the input stream of each step with the next + # steps that don't natively support transforming an input stream will + # buffer input in memory until all available, and then start emitting output + for idx, step in enumerate(self.steps): + config = patch_config( + config, + callbacks=run_manager.get_child(f"seq:step:{idx+1}"), + ) + if idx == 0: + aiterator = step.astream(input, config, **kwargs) + else: + aiterator = step.atransform(aiterator, config) + if hasattr(aiterator, "aclose"): + stack.push_async_callback(aiterator.aclose) + if stream_handler := next( + ( + h + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + ), + None, + ): + # populates streamed_output in astream_log() output if needed + aiterator = stream_handler.tap_output_aiter( + run_manager.run_id, aiterator + ) + output: Output = None + add_supported = False + async for chunk in aiterator: + yield chunk + # collect final output + if add_supported: + try: + output = output + chunk + except TypeError: + output = chunk + add_supported = False + else: + output = chunk + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(output) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index aa0054283..c9bf1c7fe 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1,126 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.3.5" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, - {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, -] - -[[package]] -name = "aiohttp" -version = "3.10.2" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95213b3d79c7e387144e9cb7b9d2809092d6ff2c044cb59033aedc612f38fb6d"}, - {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1aa005f060aff7124cfadaa2493f00a4e28ed41b232add5869e129a2e395935a"}, - {file = "aiohttp-3.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eabe6bf4c199687592f5de4ccd383945f485779c7ffb62a9b9f1f8a3f9756df8"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e010736fc16d21125c7e2dc5c350cd43c528b85085c04bf73a77be328fe944"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99f81f9c1529fd8e03be4a7bd7df32d14b4f856e90ef6e9cbad3415dbfa9166c"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d611d1a01c25277bcdea06879afbc11472e33ce842322496b211319aa95441bb"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00191d38156e09e8c81ef3d75c0d70d4f209b8381e71622165f22ef7da6f101"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74c091a5ded6cb81785de2d7a8ab703731f26de910dbe0f3934eabef4ae417cc"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:18186a80ec5a701816adbf1d779926e1069392cf18504528d6e52e14b5920525"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5a7ceb2a0d2280f23a02c64cd0afdc922079bb950400c3dd13a1ab2988428aac"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8bd7be6ff6c162a60cb8fce65ee879a684fbb63d5466aba3fa5b9288eb04aefa"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fae962b62944eaebff4f4fddcf1a69de919e7b967136a318533d82d93c3c6bd1"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0fde16d284efcacbe15fb0c1013f0967b6c3e379649239d783868230bf1db42"}, - {file = "aiohttp-3.10.2-cp310-cp310-win32.whl", hash = "sha256:f81cd85a0e76ec7b8e2b6636fe02952d35befda4196b8c88f3cec5b4fb512839"}, - {file = "aiohttp-3.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:54ba10eb5a3481c28282eb6afb5f709aedf53cf9c3a31875ffbdc9fc719ffd67"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87fab7f948e407444c2f57088286e00e2ed0003ceaf3d8f8cc0f60544ba61d91"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6ad66ed660d46503243cbec7b2b3d8ddfa020f984209b3b8ef7d98ce69c3f2"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4be88807283bd96ae7b8e401abde4ca0bab597ba73b5e9a2d98f36d451e9aac"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c98041f90927c2cbd72c22a164bb816fa3010a047d264969cf82e1d4bcf8d1"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54e36c67e1a9273ecafab18d6693da0fb5ac48fd48417e4548ac24a918c20998"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7de3ddb6f424af54535424082a1b5d1ae8caf8256ebd445be68c31c662354720"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd9c7db94b4692b827ce51dcee597d61a0e4f4661162424faf65106775b40e7"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e57e21e1167705f8482ca29cc5d02702208d8bf4aff58f766d94bcd6ead838cd"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1a50e59b720060c29e2951fd9f13c01e1ea9492e5a527b92cfe04dd64453c16"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:686c87782481fda5ee6ba572d912a5c26d9f98cc5c243ebd03f95222af3f1b0f"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:dafb4abb257c0ed56dc36f4e928a7341b34b1379bd87e5a15ce5d883c2c90574"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:494a6f77560e02bd7d1ab579fdf8192390567fc96a603f21370f6e63690b7f3d"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6fe8503b1b917508cc68bf44dae28823ac05e9f091021e0c41f806ebbb23f92f"}, - {file = "aiohttp-3.10.2-cp311-cp311-win32.whl", hash = "sha256:4ddb43d06ce786221c0dfd3c91b4892c318eaa36b903f7c4278e7e2fa0dd5102"}, - {file = "aiohttp-3.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:ca2f5abcb0a9a47e56bac173c01e9f6c6e7f27534d91451c5f22e6a35a5a2093"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:14eb6b17f6246959fb0b035d4f4ae52caa870c4edfb6170aad14c0de5bfbf478"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:465e445ec348d4e4bd349edd8b22db75f025da9d7b6dc1369c48e7935b85581e"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:341f8ece0276a828d95b70cd265d20e257f5132b46bf77d759d7f4e0443f2906"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01fbb87b5426381cd9418b3ddcf4fc107e296fa2d3446c18ce6c76642f340a3"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c474af073e1a6763e1c5522bbb2d85ff8318197e4c6c919b8d7886e16213345"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9076810a5621236e29b2204e67a68e1fe317c8727ee4c9abbfbb1083b442c38"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f515d6859e673940e08de3922b9c4a2249653b0ac181169313bd6e4b1978ac"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:655e583afc639bef06f3b2446972c1726007a21003cd0ef57116a123e44601bc"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8da9449a575133828cc99985536552ea2dcd690e848f9d41b48d8853a149a959"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19073d57d0feb1865d12361e2a1f5a49cb764bf81a4024a3b608ab521568093a"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8e98e1845805f184d91fda6f9ab93d7c7b0dddf1c07e0255924bfdb151a8d05"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:377220a5efde6f9497c5b74649b8c261d3cce8a84cb661be2ed8099a2196400a"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92f7f4a4dc9cdb5980973a74d43cdbb16286dacf8d1896b6c3023b8ba8436f8e"}, - {file = "aiohttp-3.10.2-cp312-cp312-win32.whl", hash = "sha256:9bb2834a6f11d65374ce97d366d6311a9155ef92c4f0cee543b2155d06dc921f"}, - {file = "aiohttp-3.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:518dc3cb37365255708283d1c1c54485bbacccd84f0a0fb87ed8917ba45eda5b"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f98e70bbbf693086efe4b86d381efad8edac040b8ad02821453083d15ec315f"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f6f0b252a009e98fe84028a4ec48396a948e7a65b8be06ccfc6ef68cf1f614d"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9360e3ffc7b23565600e729e8c639c3c50d5520e05fdf94aa2bd859eef12c407"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3988044d1635c7821dd44f0edfbe47e9875427464e59d548aece447f8c22800a"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a9d59da1543a6f1478c3436fd49ec59be3868bca561a33778b4391005e499d"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f49bdb94809ac56e09a310a62f33e5f22973d6fd351aac72a39cd551e98194"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfd2dca3f11c365d6857a07e7d12985afc59798458a2fdb2ffa4a0332a3fd43"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c1508ec97b2cd3e120bfe309a4ff8e852e8a7460f1ef1de00c2c0ed01e33c"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:49904f38667c44c041a0b44c474b3ae36948d16a0398a8f8cd84e2bb3c42a069"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:352f3a4e5f11f3241a49b6a48bc5b935fabc35d1165fa0d87f3ca99c1fcca98b"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:fc61f39b534c5d5903490478a0dd349df397d2284a939aa3cbaa2fb7a19b8397"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ad2274e707be37420d0b6c3d26a8115295fe9d8e6e530fa6a42487a8ca3ad052"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c836bf3c7512100219fe1123743fd8dd9a2b50dd7cfb0c3bb10d041309acab4b"}, - {file = "aiohttp-3.10.2-cp38-cp38-win32.whl", hash = "sha256:53e8898adda402be03ff164b0878abe2d884e3ea03a4701e6ad55399d84b92dc"}, - {file = "aiohttp-3.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:7cc8f65f5b22304693de05a245b6736b14cb5bc9c8a03da6e2ae9ef15f8b458f"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dfc906d656e14004c5bc672399c1cccc10db38df2b62a13fb2b6e165a81c316"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:91b10208b222ddf655c3a3d5b727879d7163db12b634492df41a9182a76edaae"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fd16b5e1a7bdd14668cd6bde60a2a29b49147a535c74f50d8177d11b38433a7"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bfdda4971bd79201f59adbad24ec2728875237e1c83bba5221284dbbf57bda"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69d73f869cf29e8a373127fc378014e2b17bcfbe8d89134bc6fb06a2f67f3cb3"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df59f8486507c421c0620a2c3dce81fbf1d54018dc20ff4fecdb2c106d6e6abc"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df930015db36b460aa9badbf35eccbc383f00d52d4b6f3de2ccb57d064a6ade"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:562b1153ab7f766ee6b8b357ec777a302770ad017cf18505d34f1c088fccc448"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d984db6d855de58e0fde1ef908d48fe9a634cadb3cf715962722b4da1c40619d"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:14dc3fcb0d877911d775d511eb617a486a8c48afca0a887276e63db04d3ee920"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b52a27a5c97275e254704e1049f4b96a81e67d6205f52fa37a4777d55b0e98ef"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cd33d9de8cfd006a0d0fe85f49b4183c57e91d18ffb7e9004ce855e81928f704"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1238fc979160bc03a92fff9ad021375ff1c8799c6aacb0d8ea1b357ea40932bb"}, - {file = "aiohttp-3.10.2-cp39-cp39-win32.whl", hash = "sha256:e2f43d238eae4f0b04f58d4c0df4615697d4ca3e9f9b1963d49555a94f0f5a04"}, - {file = "aiohttp-3.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:947847f07a8f81d7b39b2d0202fd73e61962ebe17ac2d8566f260679e467da7b"}, - {file = "aiohttp-3.10.2.tar.gz", hash = "sha256:4d1f694b5d6e459352e5e925a42e05bac66655bfde44d81c59992463d2897014"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -151,31 +29,6 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "anthropic" -version = "0.28.1" -description = "The official Python library for the anthropic API" -optional = false -python-versions = ">=3.7" -files = [ - {file = "anthropic-0.28.1-py3-none-any.whl", hash = "sha256:c4773ae2b42951a6b747bed328b0d03fa412938c95c3a8b9dce70d69badb710b"}, - {file = "anthropic-0.28.1.tar.gz", hash = "sha256:e3a6d595bde241141bdc685edc393903ec95c7fa378013a71186cfb8f32b1793"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tokenizers = ">=0.13.0" -typing-extensions = ">=4.7,<5" - -[package.extras] -bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] -vertex = ["google-auth (>=2,<3)"] - [[package]] name = "anyio" version = "4.4.0" @@ -317,17 +170,6 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - [[package]] name = "attrs" version = "23.2.0" @@ -722,17 +564,6 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -789,22 +620,6 @@ files = [ [package.extras] devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] -[[package]] -name = "filelock" -version = "3.15.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, - {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - [[package]] name = "fqdn" version = "1.5.1" @@ -816,131 +631,6 @@ files = [ {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, ] -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "fsspec" -version = "2024.6.0" -description = "File-system specification" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fsspec-2024.6.0-py3-none-any.whl", hash = "sha256:58d7122eb8a1a46f7f13453187bfea4972d66bf01618d37366521b1998034cee"}, - {file = "fsspec-2024.6.0.tar.gz", hash = "sha256:f579960a56e6d8038a9efc8f9c77279ec12e6299aa86b0769a7e9c46b94527c2"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] -tqdm = ["tqdm"] - [[package]] name = "grandalf" version = "0.8" @@ -958,77 +648,6 @@ pyparsing = "*" [package.extras] full = ["numpy", "ply"] -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - [[package]] name = "h11" version = "0.14.0" @@ -1085,40 +704,6 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -[[package]] -name = "huggingface-hub" -version = "0.23.4" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "huggingface_hub-0.23.4-py3-none-any.whl", hash = "sha256:3a0b957aa87150addf0cc7bd71b4d954b78e749850e1e7fb29ebbd2db64ca037"}, - {file = "huggingface_hub-0.23.4.tar.gz", hash = "sha256:35d99016433900e44ae7efe1c209164a5a81dbbcd53a52f99c281dcd7ce22431"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -inference = ["aiohttp", "minijinja (>=1.0)"] -quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - [[package]] name = "idna" version = "3.7" @@ -1301,76 +886,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jiter" -version = "0.4.2" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jiter-0.4.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2b003ff58d14f5e182b875acd5177b2367245c19a03be9a2230535d296f7550"}, - {file = "jiter-0.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b48c77c25f094707731cd5bad6b776046846b60a27ee20efc8fadfb10a89415f"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f50ad6b172bde4d45f4d4ea10c49282a337b8bb735afc99763dfa55ea84a743"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f6001e86f525fbbc9706db2078dc22be078b0950de55b92d37041930f5f940"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16646ef23b62b007de80460d303ebb2d81e355dac9389c787cec87cdd7ffef2f"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b4e847c13b0bf1255c711a92330e7a8cb8b5cdd1e37d7db309627bcdd3367ff"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c536589be60e4c5f2b20fadc4db7e9f55d4c9df3551f29ddf1c4a18dcc9dd54"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3b2763996167830889a854b4ded30bb90897f9b76be78069c50c3ec4540950e"}, - {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:675e8ab98c99495091af6b6e9bf2b6353bcf81f25ab6ce27d36127e315b4505d"}, - {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e48e43d9d999aaf55f53406b8846ff8cbe3e47ee4b9dc37e5a10a65ce760809f"}, - {file = "jiter-0.4.2-cp310-none-win32.whl", hash = "sha256:881b6e67c50bc36acb3570eda693763c8cd77d590940e06fa6d325d0da52ec1b"}, - {file = "jiter-0.4.2-cp310-none-win_amd64.whl", hash = "sha256:bb8f7b43259efc6add0d721ade2953e064b24e2026d26d979bc09ec080844cef"}, - {file = "jiter-0.4.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:24ad336ac47f274fa83f6fbedcabff9d3387c80f67c66b992688e6a8ba2c47e9"}, - {file = "jiter-0.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc392a220095730afe365ce1516f2f88bb085a2fd29ea191be9c6e3c71713d9a"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1fdc408de36c81460896de0176f2f7b9f3574dcd35693a0b2c00f4ca34c98e4"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10ad76722ee6a8c820b0db06a793c08b7d679e5201b9563015bd1e06c959a09"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb46d1e9c82bba87f0cbda38413e49448a7df35b1e55917124bff9f38974a23"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:194e28ef4b5f3b61408cb2ee6b6dcbcdb0c9063d01b92b01345b7605692849f5"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0a447533eccd62748a727e058efa10a8d7cf1de8ffe1a4d705ecb41dad9090"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5f7704d7260bbb88cca3453951af739589132b26e896a3144fa2dae2263716d7"}, - {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01427458bc9550f2eda09d425755330e7d0eb09adce099577433bebf05d28d59"}, - {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:159b8416879c0053b17c352f70b67b749ef5b2924c6154318ecf71918aab0905"}, - {file = "jiter-0.4.2-cp311-none-win32.whl", hash = "sha256:f2445234acfb79048ce1a0d5d0e181abb9afd9e4a29d8d9988fe26cc5773a81a"}, - {file = "jiter-0.4.2-cp311-none-win_amd64.whl", hash = "sha256:e15a65f233b6b0e5ac10ddf3b97ceb18aa9ffba096259961641d78b4ee321bd5"}, - {file = "jiter-0.4.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d61d59521aea9745447ce50f74d39a16ef74ec9d6477d9350d77e75a3d774ad2"}, - {file = "jiter-0.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eef607dc0acc251923427808dbd017f1998ae3c1a0430a261527aa5cbb3a942"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af6bf39954646e374fc47429c656372ac731a6a26b644158a5a84bcdbed33a47"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f509d23606e476852ee46a2b65b5c4ad3905f17424d9cc19c1dffa1c94ba3c6"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59672774daa44ee140aada0c781c82bee4d9ac5e522966186cfb6b3c217d8a51"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a0458efac5afeca254cf557b8a654e17013075a69905c78f88d557f129d871"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8860766d1c293e75c1bb4e25b74fa987e3adf199cac3f5f9e6e49c2bebf092f"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a109f3281b72bbf4921fe43db1005c004a38559ca0b6c4985add81777dfe0a44"}, - {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:faa7e667454b77ad2f0ef87db39f4944de759617aadf210ea2b73f26bb24755f"}, - {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3512f8b00cafb6780b427cb6282800d2bf8277161d9c917830661bd4ed1d3528"}, - {file = "jiter-0.4.2-cp312-none-win32.whl", hash = "sha256:853b35d508ee5b66d06630473c1c0b7bb5e29bf4785c9d2202437116c94f7e21"}, - {file = "jiter-0.4.2-cp312-none-win_amd64.whl", hash = "sha256:4a3a8197784278eb8b24cb02c45e1cad67c2ce5b5b758adfb19b87f74bbdff9c"}, - {file = "jiter-0.4.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ca2a4d750aed3154b89f2efb148609fc985fad8db739460797aaf9b478acedda"}, - {file = "jiter-0.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e6c304b3cc6896256727e1fb8991c7179a345eca8224e201795e9cacf4683b0"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cc34ac708ae1750d077e490321761ec4b9a055b994cbdd1d6fbd37099e4aa7b"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c93383875ab8d2e4f760aaff335b4a12ff32d4f9cf49c4498d657734f611466"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce197ee044add576afca0955b42142dd0312639adb6ebadbdbe4277f2855614f"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a427716813ff65480ca5b5117cfa099f49b49cd38051f8609bd0d5493013ca0"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479990218353356234669e70fac53e5eb6f739a10db25316171aede2c97d9364"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d35a91ec5ac74cf33234c431505299fa91c0a197c2dbafd47400aca7c69489d4"}, - {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b27189847193708c94ad10ca0d891309342ae882725d2187cf5d2db02bde8d1b"}, - {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76c255308cd1093fb411a03756b7bb220e48d4a98c30cbc79ed448bf3978e27d"}, - {file = "jiter-0.4.2-cp38-none-win32.whl", hash = "sha256:bb77438060bad49cc251941e6701b31138365c8a0ddaf10cdded2fcc6dd30701"}, - {file = "jiter-0.4.2-cp38-none-win_amd64.whl", hash = "sha256:ce858af19f7ce0d4b51c9f6c0c9d08f1e9dcef1986c5875efd0674a7054292ca"}, - {file = "jiter-0.4.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6128838a2f357b3921b2a3242d5dc002ae4255ecc8f9f05c20d56d7d2d79c5ad"}, - {file = "jiter-0.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2420cebb9ba856cb57dcab1d2d8def949b464b0db09c22a4e4dbd52fff7b200"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d13d8128e853b320e00bb18bd4bb8b136cc0936091dc87633648fc688eb705"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eba5d6e54f149c508ba88677f97d3dc7dd75e9980d234bbac8027ac6db0763a3"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fad5d64af0bc0545237419bf4150d8de56f0bd217434bdd1a59730327252bef"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d179e7bca89cf5719bd761dd37a341ff0f98199ecaa9c14af09792e47e977cc"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36353caee9f103d8ee7bda077f6400505b0f370e27eabcab33a33d21de12a2a6"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dd146c25bce576ca5db64fc7eccb8862af00f1f0e30108796953f12a53660e4c"}, - {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:14b7c08cadbcd703041c66dc30e24e17de2f340281cac0e69374223ecf153aa4"}, - {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a90f1a8b3d29aea198f8ea2b01148276ced8056e5103f32525266b3d880e65c9"}, - {file = "jiter-0.4.2-cp39-none-win32.whl", hash = "sha256:25b174997c780337b61ae57b1723455eecae9a17a9659044fd3c3b369190063f"}, - {file = "jiter-0.4.2-cp39-none-win_amd64.whl", hash = "sha256:bef62cea18521c5b99368147040c7e560c55098a35c93456f110678a2d34189a"}, - {file = "jiter-0.4.2.tar.gz", hash = "sha256:29b9d44f23f0c05f46d482f4ebf03213ee290d77999525d0975a17f875bf1eea"}, -] - [[package]] name = "json5" version = "0.9.25" @@ -1634,13 +1149,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.2" +version = "4.2.5" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"}, - {file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"}, + {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, + {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, ] [package.dependencies] @@ -1665,7 +1180,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -1715,58 +1230,15 @@ files = [ {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, ] -[[package]] -name = "langchain" -version = "0.2.5" -description = "Building applications with LLMs through composability" -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langchain-0.2.5-py3-none-any.whl", hash = "sha256:9aded9a65348254e1c93dcdaacffe4d1b6a5e7f74ef80c160c88ff78ad299228"}, - {file = "langchain-0.2.5.tar.gz", hash = "sha256:ffdbf4fcea46a10d461bcbda2402220fcfd72a0c70e9f4161ae0510067b9b3bd"}, -] - -[package.dependencies] -aiohttp = ">=3.8.3,<4.0.0" -async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} -langchain-core = ">=0.2.7,<0.3.0" -langchain-text-splitters = ">=0.2.0,<0.3.0" -langsmith = ">=0.1.17,<0.2.0" -numpy = [ - {version = ">=1,<2", markers = "python_version < \"3.12\""}, - {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""}, -] -pydantic = ">=1,<3" -PyYAML = ">=5.3" -requests = ">=2,<3" -SQLAlchemy = ">=1.4,<3" -tenacity = ">=8.1.0,<9.0.0" - -[[package]] -name = "langchain-anthropic" -version = "0.1.15" -description = "An integration package connecting AnthropicMessages and LangChain" -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langchain_anthropic-0.1.15-py3-none-any.whl", hash = "sha256:7cceea526f473e4d514f39295dc128eec57da628a4bbb54850d11dda7aa959fc"}, - {file = "langchain_anthropic-0.1.15.tar.gz", hash = "sha256:c5c3c6eaccb11ed99a63886e50873ac21eaf8e9441e0f75c7ae7cd8cdef65155"}, -] - -[package.dependencies] -anthropic = ">=0.28.0,<1" -defusedxml = ">=0.7.1,<0.8.0" -langchain-core = ">=0.2.2rc1,<0.3" - [[package]] name = "langchain-core" -version = "0.2.27" +version = "0.2.38" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.2.27-py3-none-any.whl", hash = "sha256:b12f58d4e3590e8e0b4b727acb0457b00e80862d1bf8b9d1ae36128adb08a7d0"}, - {file = "langchain_core-0.2.27.tar.gz", hash = "sha256:5d2e4b9bc84285bbfe19864363b1a21dd04c2eed12d4a6b39f239ee02237ce40"}, + {file = "langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a"}, + {file = "langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3"}, ] [package.dependencies] @@ -1781,58 +1253,9 @@ PyYAML = ">=5.3" tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" typing-extensions = ">=4.7" -[[package]] -name = "langchain-openai" -version = "0.1.8" -description = "An integration package connecting OpenAI and LangChain" -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langchain_openai-0.1.8-py3-none-any.whl", hash = "sha256:8125c84223e9f43b05defbca64eedbcf362fd78a680de6c25e64f973b34a8063"}, - {file = "langchain_openai-0.1.8.tar.gz", hash = "sha256:a11fcce15def7917c44232abda6baaa63dfc79fe44be1531eea650d39a44cd95"}, -] - -[package.dependencies] -langchain-core = ">=0.2.2,<0.3" -openai = ">=1.26.0,<2.0.0" -tiktoken = ">=0.7,<1" - -[[package]] -name = "langchain-text-splitters" -version = "0.2.1" -description = "LangChain text splitting utilities" -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langchain_text_splitters-0.2.1-py3-none-any.whl", hash = "sha256:c2774a85f17189eaca50339629d2316d13130d4a8d9f1a1a96f3a03670c4a138"}, - {file = "langchain_text_splitters-0.2.1.tar.gz", hash = "sha256:06853d17d7241ecf5c97c7b6ef01f600f9b0fb953dd997838142a527a4f32ea4"}, -] - -[package.dependencies] -langchain-core = ">=0.2.0,<0.3.0" - -[package.extras] -extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"] - -[[package]] -name = "langchainhub" -version = "0.1.20" -description = "The LangChain Hub API client" -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langchainhub-0.1.20-py3-none-any.whl", hash = "sha256:b3cbb5b2d7d6f9c3f89748bcc74424d8030ed4ebca58b5f44e0b6d9f111e33eb"}, - {file = "langchainhub-0.1.20.tar.gz", hash = "sha256:499fa8958233071f35750987f325005d16241bebd455163955b607c195c37f25"}, -] - -[package.dependencies] -packaging = ">=23.2,<25" -requests = ">=2,<3" -types-requests = ">=2.31.0.2,<3.0.0.0" - [[package]] name = "langgraph-checkpoint" -version = "1.0.2" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -1840,7 +1263,7 @@ files = [] develop = true [package.dependencies] -langchain-core = ">=0.2.22,<0.3" +langchain-core = ">=0.2.38,<0.4" [package.source] type = "directory" @@ -1848,7 +1271,7 @@ url = "../checkpoint" [[package]] name = "langgraph-checkpoint-postgres" -version = "1.0.0" +version = "1.0.6" description = "Library with a Postgres implementation of LangGraph checkpoint saver." optional = false python-versions = "^3.9.0,<4.0" @@ -1856,9 +1279,10 @@ files = [] develop = true [package.dependencies] -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" orjson = ">=3.10.1" -psycopg = {version = ">=3.1.19", extras = ["binary"]} +psycopg = "^3.0.0" +psycopg-pool = "^3.0.0" [package.source] type = "directory" @@ -1866,7 +1290,7 @@ url = "../checkpoint-postgres" [[package]] name = "langgraph-checkpoint-sqlite" -version = "1.0.0" +version = "1.0.3" description = "Library with a SQLite implementation of LangGraph checkpoint saver." optional = false python-versions = "^3.9.0" @@ -1875,7 +1299,7 @@ develop = true [package.dependencies] aiosqlite = "^0.20.0" -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" [package.source] type = "directory" @@ -1883,18 +1307,22 @@ url = "../checkpoint-sqlite" [[package]] name = "langsmith" -version = "0.1.79" +version = "0.1.112" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langsmith-0.1.79-py3-none-any.whl", hash = "sha256:c7f2c23981917713b5515b773f37c84ff68a7adf803476e2ebb5adcb36a04202"}, - {file = "langsmith-0.1.79.tar.gz", hash = "sha256:d215718cfdcdf4a011126b7a3d4a37eee96d887e59ac1e628a57e24b2bfa3163"}, + {file = "langsmith-0.1.112-py3-none-any.whl", hash = "sha256:38ecc62df35e55e5aa9a05ecf1f1483d5a4db3275b9da4876ae27364714b3717"}, + {file = "langsmith-0.1.112.tar.gz", hash = "sha256:08c47a9497261c332bc4b15e2930d8e751310e63652ecdc78382a5dd07cb42e2"}, ] [package.dependencies] +httpx = ">=0.23.0,<1" orjson = ">=3.9.14,<4.0.0" -pydantic = ">=1,<3" +pydantic = [ + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, + {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, +] requests = ">=2,<3" [[package]] @@ -1991,105 +1419,6 @@ files = [ {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, ] -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - [[package]] name = "mypy" version = "1.10.0" @@ -2242,13 +1571,13 @@ files = [ [[package]] name = "notebook" -version = "7.2.1" +version = "7.2.2" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, - {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, + {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, + {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, ] [package.dependencies] @@ -2280,127 +1609,70 @@ jupyter-server = ">=1.8,<3" [package.extras] test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, -] - -[[package]] -name = "openai" -version = "1.34.0" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.7.1" -files = [ - {file = "openai-1.34.0-py3-none-any.whl", hash = "sha256:018623c2f795424044675c6230fa3bfbf98d9e0aab45d8fd116f2efb2cfb6b7e"}, - {file = "openai-1.34.0.tar.gz", hash = "sha256:95c8e2da4acd6958e626186957d656597613587195abd0fb2527566a93e76770"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.7,<5" - -[package.extras] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] - [[package]] name = "orjson" -version = "3.10.5" +version = "3.10.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, - {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, - {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, - {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, - {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, - {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, - {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, - {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, - {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, - {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, - {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, - {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, - {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, - {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, - {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, - {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, + {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, + {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, + {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, + {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, + {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, + {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, + {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, + {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, + {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, + {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, + {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, + {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, + {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, + {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, + {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, + {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, + {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, + {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, + {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, + {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, ] [[package]] @@ -2638,6 +1910,20 @@ files = [ {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, ] +[[package]] +name = "psycopg-pool" +version = "3.2.2" +description = "Connection Pool for Psycopg" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, + {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, +] + +[package.dependencies] +typing-extensions = ">=4.4" + [[package]] name = "ptyprocess" version = "0.7.0" @@ -2814,13 +2100,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -2828,29 +2114,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.20.3" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, -] - -[package.dependencies] -pytest = ">=6.1.0" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -3246,94 +2514,6 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" -[[package]] -name = "regex" -version = "2024.5.15" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, - {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, - {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, - {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, - {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, - {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, - {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, - {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, - {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, - {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, - {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, - {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, -] - [[package]] name = "requests" version = "2.32.3" @@ -3490,28 +2670,29 @@ files = [ [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -3578,93 +2759,6 @@ files = [ {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] -[[package]] -name = "sqlalchemy" -version = "2.0.30" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:955991a09f0992c68a499791a753523f50f71a6885531568404fa0f231832aa0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f69e4c756ee2686767eb80f94c0125c8b0a0b87ede03eacc5c8ae3b54b99dc46"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c9db1ce00e59e8dd09d7bae852a9add716efdc070a3e2068377e6ff0d6fdaa"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1429a4b0f709f19ff3b0cf13675b2b9bfa8a7e79990003207a011c0db880a13"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:efedba7e13aa9a6c8407c48facfdfa108a5a4128e35f4c68f20c3407e4376aa9"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16863e2b132b761891d6c49f0a0f70030e0bcac4fd208117f6b7e053e68668d0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win32.whl", hash = "sha256:2ecabd9ccaa6e914e3dbb2aa46b76dede7eadc8cbf1b8083c94d936bcd5ffb49"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win_amd64.whl", hash = "sha256:0b3f4c438e37d22b83e640f825ef0f37b95db9aa2d68203f2c9549375d0b2260"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a79d65395ac5e6b0c2890935bad892eabb911c4aa8e8015067ddb37eea3d56c"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a5baf9267b752390252889f0c802ea13b52dfee5e369527da229189b8bd592e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5a646930c5123f8461f6468901573f334c2c63c795b9af350063a736d0134"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296230899df0b77dec4eb799bcea6fbe39a43707ce7bb166519c97b583cfcab3"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c62d401223f468eb4da32627bffc0c78ed516b03bb8a34a58be54d618b74d472"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3b69e934f0f2b677ec111b4d83f92dc1a3210a779f69bf905273192cf4ed433e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win32.whl", hash = "sha256:77d2edb1f54aff37e3318f611637171e8ec71472f1fdc7348b41dcb226f93d90"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win_amd64.whl", hash = "sha256:b6c7ec2b1f4969fc19b65b7059ed00497e25f54069407a8701091beb69e591a5"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a8e3b0a7e09e94be7510d1661339d6b52daf202ed2f5b1f9f48ea34ee6f2d57"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b60203c63e8f984df92035610c5fb76d941254cf5d19751faab7d33b21e5ddc0"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1dc3eabd8c0232ee8387fbe03e0a62220a6f089e278b1f0aaf5e2d6210741ad"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:40ad017c672c00b9b663fcfcd5f0864a0a97828e2ee7ab0c140dc84058d194cf"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e42203d8d20dc704604862977b1470a122e4892791fe3ed165f041e4bf447a1b"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win32.whl", hash = "sha256:2a4f4da89c74435f2bc61878cd08f3646b699e7d2eba97144030d1be44e27584"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win_amd64.whl", hash = "sha256:b6bf767d14b77f6a18b6982cbbf29d71bede087edae495d11ab358280f304d8e"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc0c53579650a891f9b83fa3cecd4e00218e071d0ba00c4890f5be0c34887ed3"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:311710f9a2ee235f1403537b10c7687214bb1f2b9ebb52702c5aa4a77f0b3af7"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:408f8b0e2c04677e9c93f40eef3ab22f550fecb3011b187f66a096395ff3d9fd"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a4b4fb0dd4d2669070fb05b8b8824afd0af57587393015baee1cf9890242d9"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a943d297126c9230719c27fcbbeab57ecd5d15b0bd6bfd26e91bfcfe64220621"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a089e218654e740a41388893e090d2e2c22c29028c9d1353feb38638820bbeb"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win32.whl", hash = "sha256:fa561138a64f949f3e889eb9ab8c58e1504ab351d6cf55259dc4c248eaa19da6"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win_amd64.whl", hash = "sha256:7d74336c65705b986d12a7e337ba27ab2b9d819993851b140efdf029248e818e"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8c62fe2480dd61c532ccafdbce9b29dacc126fe8be0d9a927ca3e699b9491a"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2383146973a15435e4717f94c7509982770e3e54974c71f76500a0136f22810b"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8409de825f2c3b62ab15788635ccaec0c881c3f12a8af2b12ae4910a0a9aeef6"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0094c5dc698a5f78d3d1539853e8ecec02516b62b8223c970c86d44e7a80f6c7"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:edc16a50f5e1b7a06a2dcc1f2205b0b961074c123ed17ebda726f376a5ab0953"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f7703c2010355dd28f53deb644a05fc30f796bd8598b43f0ba678878780b6e4c"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win32.whl", hash = "sha256:1f9a727312ff6ad5248a4367358e2cf7e625e98b1028b1d7ab7b806b7d757513"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win_amd64.whl", hash = "sha256:a0ef36b28534f2a5771191be6edb44cc2673c7b2edf6deac6562400288664221"}, - {file = "SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a"}, - {file = "SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - [[package]] name = "stack-data" version = "0.6.3" @@ -3734,58 +2828,6 @@ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] -[[package]] -name = "tiktoken" -version = "0.7.0" -description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tiktoken-0.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485f3cc6aba7c6b6ce388ba634fbba656d9ee27f766216f45146beb4ac18b25f"}, - {file = "tiktoken-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e54be9a2cd2f6d6ffa3517b064983fb695c9a9d8aa7d574d1ef3c3f931a99225"}, - {file = "tiktoken-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79383a6e2c654c6040e5f8506f3750db9ddd71b550c724e673203b4f6b4b4590"}, - {file = "tiktoken-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d4511c52caacf3c4981d1ae2df85908bd31853f33d30b345c8b6830763f769c"}, - {file = "tiktoken-0.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13c94efacdd3de9aff824a788353aa5749c0faee1fbe3816df365ea450b82311"}, - {file = "tiktoken-0.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8e58c7eb29d2ab35a7a8929cbeea60216a4ccdf42efa8974d8e176d50c9a3df5"}, - {file = "tiktoken-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:21a20c3bd1dd3e55b91c1331bf25f4af522c525e771691adbc9a69336fa7f702"}, - {file = "tiktoken-0.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:10c7674f81e6e350fcbed7c09a65bca9356eaab27fb2dac65a1e440f2bcfe30f"}, - {file = "tiktoken-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:084cec29713bc9d4189a937f8a35dbdfa785bd1235a34c1124fe2323821ee93f"}, - {file = "tiktoken-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811229fde1652fedcca7c6dfe76724d0908775b353556d8a71ed74d866f73f7b"}, - {file = "tiktoken-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b6e7dc2e7ad1b3757e8a24597415bafcfb454cebf9a33a01f2e6ba2e663992"}, - {file = "tiktoken-0.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1063c5748be36344c7e18c7913c53e2cca116764c2080177e57d62c7ad4576d1"}, - {file = "tiktoken-0.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20295d21419bfcca092644f7e2f2138ff947a6eb8cfc732c09cc7d76988d4a89"}, - {file = "tiktoken-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:959d993749b083acc57a317cbc643fb85c014d055b2119b739487288f4e5d1cb"}, - {file = "tiktoken-0.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:71c55d066388c55a9c00f61d2c456a6086673ab7dec22dd739c23f77195b1908"}, - {file = "tiktoken-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09ed925bccaa8043e34c519fbb2f99110bd07c6fd67714793c21ac298e449410"}, - {file = "tiktoken-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03c6c40ff1db0f48a7b4d2dafeae73a5607aacb472fa11f125e7baf9dce73704"}, - {file = "tiktoken-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d20b5c6af30e621b4aca094ee61777a44118f52d886dbe4f02b70dfe05c15350"}, - {file = "tiktoken-0.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d427614c3e074004efa2f2411e16c826f9df427d3c70a54725cae860f09e4bf4"}, - {file = "tiktoken-0.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c46d7af7b8c6987fac9b9f61041b452afe92eb087d29c9ce54951280f899a97"}, - {file = "tiktoken-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bc603c30b9e371e7c4c7935aba02af5994a909fc3c0fe66e7004070858d3f8f"}, - {file = "tiktoken-0.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2398fecd38c921bcd68418675a6d155fad5f5e14c2e92fcf5fe566fa5485a858"}, - {file = "tiktoken-0.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f5f6afb52fb8a7ea1c811e435e4188f2bef81b5e0f7a8635cc79b0eef0193d6"}, - {file = "tiktoken-0.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:861f9ee616766d736be4147abac500732b505bf7013cfaf019b85892637f235e"}, - {file = "tiktoken-0.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54031f95c6939f6b78122c0aa03a93273a96365103793a22e1793ee86da31685"}, - {file = "tiktoken-0.7.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fffdcb319b614cf14f04d02a52e26b1d1ae14a570f90e9b55461a72672f7b13d"}, - {file = "tiktoken-0.7.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c72baaeaefa03ff9ba9688624143c858d1f6b755bb85d456d59e529e17234769"}, - {file = "tiktoken-0.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:131b8aeb043a8f112aad9f46011dced25d62629091e51d9dc1adbf4a1cc6aa98"}, - {file = "tiktoken-0.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cabc6dc77460df44ec5b879e68692c63551ae4fae7460dd4ff17181df75f1db7"}, - {file = "tiktoken-0.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8d57f29171255f74c0aeacd0651e29aa47dff6f070cb9f35ebc14c82278f3b25"}, - {file = "tiktoken-0.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ee92776fdbb3efa02a83f968c19d4997a55c8e9ce7be821ceee04a1d1ee149c"}, - {file = "tiktoken-0.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e215292e99cb41fbc96988ef62ea63bb0ce1e15f2c147a61acc319f8b4cbe5bf"}, - {file = "tiktoken-0.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a81bac94769cab437dd3ab0b8a4bc4e0f9cf6835bcaa88de71f39af1791727a"}, - {file = "tiktoken-0.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d6d73ea93e91d5ca771256dfc9d1d29f5a554b83821a1dc0891987636e0ae226"}, - {file = "tiktoken-0.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:2bcb28ddf79ffa424f171dfeef9a4daff61a94c631ca6813f43967cb263b83b9"}, - {file = "tiktoken-0.7.0.tar.gz", hash = "sha256:1077266e949c24e0291f6c350433c6f0971365ece2b173a23bc3b9f9defef6b6"}, -] - -[package.dependencies] -regex = ">=2022.1.18" -requests = ">=2.26.0" - -[package.extras] -blobfile = ["blobfile (>=2)"] - [[package]] name = "tinycss2" version = "1.3.0" @@ -3804,123 +2846,6 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["pytest", "ruff"] -[[package]] -name = "tokenizers" -version = "0.19.1" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, - {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, - {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, - {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, - {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, - {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, - {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, - {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, - {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, - {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, - {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, - {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, - {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, - {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, - {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<1.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] - [[package]] name = "tomli" version = "2.0.1" @@ -3952,26 +2877,6 @@ files = [ {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] -[[package]] -name = "tqdm" -version = "4.66.4" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, - {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - [[package]] name = "traitlets" version = "5.14.3" @@ -3998,20 +2903,6 @@ files = [ {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, ] -[[package]] -name = "types-requests" -version = "2.32.0.20240602" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"}, - {file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "typing-extensions" version = "4.12.2" @@ -4173,109 +3064,6 @@ files = [ {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, ] -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [[package]] name = "zipp" version = "3.19.2" @@ -4294,4 +3082,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "4ef9e25016072ce08554c8ff8d091104fb59da7cdab9a128312bd787e6f35146" +content-hash = "3c3d4b9ce6b0609d0399ca9aece50495d0a29b978042908dd981ea267f541934" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 3aac6ed84..c32442040 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.4" +version = "0.2.19" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" @@ -9,35 +9,28 @@ repository = "https://www.github.com/langchain-ai/langgraph" [tool.poetry.dependencies] python = ">=3.9.0,<4.0" -langchain-core = ">=0.2.27,<0.3" +langchain-core = ">=0.2.38,<0.4" langgraph-checkpoint = "^1.0.2" [tool.poetry.group.dev.dependencies] -pytest = "^7.3.0" +pytest = "^8.3.2" pytest-cov = "^4.0.0" pytest-dotenv = "^0.5.2" -pytest-asyncio = "^0.20.3" pytest-mock = "^3.10.0" syrupy = "^4.0.2" httpx = "^0.26.0" pytest-watcher = "^0.4.1" -langchain = ">=0.1.0" grandalf = "^0.8" mypy = "^1.6.0" -ruff = "^0.1.4" +ruff = "^0.6.2" jupyter = "^1.0.0" -langchainhub = "^0.1.14" -langchain-openai = ">=0.1.2" -langchain-anthropic = ">=0.1.8" pytest-xdist = {extras = ["psutil"], version = "^3.6.1"} pytest-repeat = "^0.9.3" langgraph-checkpoint = {path = "../checkpoint", develop = true} langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true} langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true} - -[tool.poetry.group.dev] -optional = true +psycopg = {extras = ["binary"], version = ">=3.0.0"} [tool.ruff] lint.select = [ "E", "F", "I" ] @@ -65,7 +58,6 @@ omit = ["tests/*"] [tool.pytest-watcher] now = true delay = 0.1 -runner_args = ["--ff", "-v", "-n", "auto", "--dist", "worksteal", "--snapshot-update", "--tb", "short"] patterns = ["*.py"] [build-system] @@ -73,7 +65,6 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] -asyncio_mode = "auto" # --strict-markers will raise errors on unknown marks. # https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks # diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 644c99b95..bbf8a06cb 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -33,6 +33,176 @@ ''' # --- +# name: test_branch_then[memory] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[memory].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[postgres] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[postgres_pipe] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres_pipe].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[postgres_pool] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres_pool].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[sqlite] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[sqlite].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_conditional_entrypoint_graph '{"title": "LangGraphInput"}' # --- @@ -55,6 +225,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "left" @@ -67,6 +238,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "right" @@ -115,10 +287,10 @@ ''' # --- # name: test_conditional_entrypoint_graph_state - '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "output": {"title": "Output", "type": "string"}, "steps": {"title": "Steps", "type": "array", "items": {"type": "string"}}}}' + '{"properties": {"input": {"default": null, "title": "Input", "type": "string"}, "output": {"default": null, "title": "Output", "type": "string"}, "steps": {"default": null, "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' # --- # name: test_conditional_entrypoint_graph_state.1 - '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "output": {"title": "Output", "type": "string"}, "steps": {"title": "Steps", "type": "array", "items": {"type": "string"}}}}' + '{"properties": {"input": {"default": null, "title": "Input", "type": "string"}, "output": {"default": null, "title": "Output", "type": "string"}, "steps": {"default": null, "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' # --- # name: test_conditional_entrypoint_graph_state.2 ''' @@ -136,6 +308,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "left" @@ -148,6 +321,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "right" @@ -196,10 +370,10 @@ ''' # --- # name: test_conditional_entrypoint_to_multiple_state_graph - '{"title": "LangGraphInput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}' + '{"properties": {"locations": {"items": {"type": "string"}, "title": "Locations", "type": "array"}, "results": {"items": {"type": "string"}, "title": "Results", "type": "array"}}, "required": ["locations", "results"], "title": "LangGraphInput", "type": "object"}' # --- # name: test_conditional_entrypoint_to_multiple_state_graph.1 - '{"title": "LangGraphOutput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}' + '{"properties": {"locations": {"default": null, "items": {"type": "string"}, "title": "Locations", "type": "array"}, "results": {"default": null, "items": {"type": "string"}, "title": "Results", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' # --- # name: test_conditional_entrypoint_to_multiple_state_graph.2 ''' @@ -217,6 +391,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "get_weather" @@ -601,8 +776,1775 @@ ''' # --- -# name: test_conditional_state_graph - '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# name: test_conditional_graph[memory] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[memory].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[memory].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[memory].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[memory].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[memory].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'runnable', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'parents': dict({ + }), + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[memory].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'runnable', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'parents': dict({ + }), + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pipe] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pipe].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pipe].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'runnable', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'parents': dict({ + }), + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres_pipe].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pool] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pool].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pool].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pool].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pool].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pool].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'runnable', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'parents': dict({ + }), + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres_pool].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[sqlite] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[sqlite].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[sqlite].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[sqlite].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "parents": {}, + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[sqlite].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[sqlite].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'runnable', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'parents': dict({ + }), + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[sqlite].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
parents = {} + version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' # --- # name: test_conditional_state_graph.1 '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' @@ -682,6 +2624,506 @@ ''' # --- +# name: test_conditional_state_graph[memory] + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' +# --- +# name: test_conditional_state_graph[memory].1 + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_conditional_state_graph[memory].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[memory].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[postgres] + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres].1 + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[postgres_pipe] + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres_pipe].1 + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres_pipe].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres_pipe].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[postgres_pool] + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres_pool].1 + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_conditional_state_graph[postgres_pool].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres_pool].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[sqlite] + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphInput", "type": "object"}' +# --- +# name: test_conditional_state_graph[sqlite].1 + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_conditional_state_graph[sqlite].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[sqlite].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph_with_list_edge_inputs + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "A", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "A" + } + }, + { + "id": "B", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "runnable", + "RunnableCallable" + ], + "name": "B" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "A", + "target": "__end__" + }, + { + "source": "B", + "target": "__end__" + }, + { + "source": "__start__", + "target": "A" + }, + { + "source": "__start__", + "target": "B" + } + ] + } + ''' +# --- +# name: test_conditional_state_graph_with_list_edge_inputs.1 + ''' + graph TD; + A --> __end__; + B --> __end__; + __start__ --> A; + __start__ --> B; + + ''' +# --- +# name: test_dynamic_interrupt + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge ''' graph TD; @@ -695,6 +3137,71 @@ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class ''' graph TD; @@ -778,6 +3285,356 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2 ''' graph TD; @@ -848,6 +3705,356 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch ''' graph TD; @@ -861,8 +4068,70 @@ ''' # --- -# name: test_message_graph - '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' # --- # name: test_message_graph.1 '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' @@ -942,6 +4211,411 @@ ''' # --- +# name: test_message_graph[memory] + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}' +# --- +# name: test_message_graph[memory].1 + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}' +# --- +# name: test_message_graph[memory].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[memory].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[postgres] + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}' +# --- +# name: test_message_graph[postgres].1 + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}' +# --- +# name: test_message_graph[postgres].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[postgres_pipe] + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}' +# --- +# name: test_message_graph[postgres_pipe].1 + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}' +# --- +# name: test_message_graph[postgres_pipe].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres_pipe].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[postgres_pool] + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}' +# --- +# name: test_message_graph[postgres_pool].1 + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}' +# --- +# name: test_message_graph[postgres_pool].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres_pool].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[sqlite] + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}' +# --- +# name: test_message_graph[sqlite].1 + '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "enum": ["ai"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "enum": ["AIMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "enum": ["chat"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "enum": ["ChatMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "enum": ["function"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "enum": ["FunctionMessageChunk"], "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "enum": ["human"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "enum": ["HumanMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InvalidToolCall": {"description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "type": {"const": "invalid_tool_call", "enum": ["invalid_tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "error"], "title": "InvalidToolCall", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "enum": ["system"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "enum": ["SystemMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"description": "Represents a request to call a tool.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"name\\": \\"foo\\",\\n \\"args\\": {\\"a\\": 1},\\n \\"id\\": \\"123\\"\\n }\\n\\n This represents a request to call the tool named \\"foo\\" with arguments {\\"a\\": 1}\\n and an identifier of \\"123\\".", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "enum": ["tool_call"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"description": "A chunk of a tool call (e.g., as part of a stream).\\n\\nWhen merging ToolCallChunks (e.g., via AIMessageChunk.__add__),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n\\n.. code-block:: python\\n\\n left_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\n right_chunks = [ToolCallChunk(name=None, args=\'1}\', index=0)]\\n\\n (\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n ).tool_call_chunks == [ToolCallChunk(name=\'foo\', args=\'{\\"a\\":1}\', index=0)]", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "enum": ["tool_call_chunk"], "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "enum": ["tool"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "enum": ["ToolMessageChunk"], "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n {\\n \\"input_tokens\\": 10,\\n \\"output_tokens\\": 20,\\n \\"total_tokens\\": 30\\n }", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "items": {"anyOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}' +# --- +# name: test_message_graph[sqlite].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[sqlite].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- # name: test_nested_graph ''' graph TD; @@ -1028,6 +4702,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_one', @@ -1045,6 +4720,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_slow', @@ -1057,6 +4733,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_fast', @@ -1074,6 +4751,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_three', @@ -1101,18 +4779,18 @@ tool_two___end__(__end__) tool_three(tool_three) __end__([__end__]):::last - subgraph tool_two - tool_two___start__ -.-> tool_two_tool_two_slow; - tool_two_tool_two_slow --> tool_two___end__; - tool_two___start__ -.-> tool_two_tool_two_fast; - tool_two_tool_two_fast --> tool_two___end__; - end __start__ -.-> tool_one; tool_one --> __end__; __start__ -.-> tool_two___start__; tool_two___end__ --> __end__; __start__ -.-> tool_three; tool_three --> __end__; + subgraph tool_two + tool_two___start__ -.-> tool_two_tool_two_slow; + tool_two_tool_two_slow --> tool_two___end__; + tool_two___start__ -.-> tool_two_tool_two_fast; + tool_two_tool_two_fast --> tool_two___end__; + end classDef default fill:#f2f0ff,line-height:1.2 classDef first fill-opacity:0 classDef last fill:#bfb6fc @@ -1202,10 +4880,10 @@ ''' # --- # name: test_prebuilt_tool_chat - '{"title": "LangGraphInput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}' + '{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphInput", "type": "object"}' # --- # name: test_prebuilt_tool_chat.1 - '{"title": "LangGraphOutput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}' + '{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"default": null, "items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' # --- # name: test_prebuilt_tool_chat.2 ''' @@ -1287,7 +4965,6 @@ ''' graph TD; __start__ --> Researcher; - Researcher -.  redo  .-> Researcher; Researcher -.  continue  .-> Chart_Generator; Researcher -.  call_tool  .-> Call_Tool; Researcher -.  end  .-> __end__; @@ -1296,6 +4973,7 @@ Chart_Generator -.  end  .-> __end__; Call_Tool -.-> Researcher; Call_Tool -.-> Chart_Generator; + Researcher -.  redo  .-> Researcher; ''' # --- @@ -1329,6 +5007,96 @@ ''' # --- +# name: test_start_branch_then[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[postgres] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[postgres_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[postgres_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[sqlite] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_state_graph_w_config '{"title": "LangGraphConfig", "type": "object", "properties": {"configurable": {"$ref": "#/definitions/Configurable"}}, "definitions": {"Configurable": {"title": "Configurable", "type": "object", "properties": {"tools": {"title": "Tools", "type": "array", "items": {"type": "string"}}}}}}' # --- @@ -1348,13 +5116,138 @@ '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- # name: test_state_graph_w_config_inherited_state_keys - '{"title": "LangGraphConfig", "type": "object", "properties": {"configurable": {"$ref": "#/definitions/Configurable"}}, "definitions": {"Configurable": {"title": "Configurable", "type": "object", "properties": {"tools": {"title": "Tools", "type": "array", "items": {"type": "string"}}}}}}' + '{"$defs": {"Configurable": {"properties": {"tools": {"default": null, "items": {"type": "string"}, "title": "Tools", "type": "array"}}, "title": "Configurable", "type": "object"}}, "properties": {"configurable": {"allOf": [{"$ref": "#/$defs/Configurable"}], "default": null}}, "title": "LangGraphConfig", "type": "object"}' # --- # name: test_state_graph_w_config_inherited_state_keys.1 - '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "required": ["input"], "title": "LangGraphInput", "type": "object"}' # --- # name: test_state_graph_w_config_inherited_state_keys.2 - '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' + '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "enum": ["AgentAction"], "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "enum": ["AgentFinish"], "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"default": null, "title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "default": null, "title": "Agent Outcome"}, "intermediate_steps": {"default": null, "items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "LangGraphOutput", "type": "object"}' +# --- +# name: test_weather_subgraph[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[sqlite] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' # --- # name: test_xray_issue ''' @@ -1367,16 +5260,16 @@ p_two_c_two(c_two) p_two___end__(__end__) __end__([__end__]):::last + __start__ --> p_one; + p_two___end__ --> p_one; + p_one -.  0  .-> p_two___start__; + p_one -.  1  .-> __end__; subgraph p_two p_two___start__ --> p_two_c_one; p_two_c_two --> p_two_c_one; p_two_c_one -.  0  .-> p_two_c_two; p_two_c_one -.  1  .-> p_two___end__; end - __start__ --> p_one; - p_two___end__ --> p_one; - p_one -.  0  .-> p_two___start__; - p_one -.  1  .-> __end__; classDef default fill:#f2f0ff,line-height:1.2 classDef first fill-opacity:0 classDef last fill:#bfb6fc @@ -1416,6 +5309,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'ask_question', @@ -1428,6 +5322,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'answer_question', @@ -1475,6 +5370,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -1500,6 +5396,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_sections', @@ -1565,6 +5462,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -1582,6 +5480,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:ask_question', @@ -1594,6 +5493,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:answer_question', @@ -1611,6 +5511,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_sections', diff --git a/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr b/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr index c0d2e782c..1b0f6c41d 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr @@ -36,6 +36,191 @@ +---------+ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[memory] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio_pipe] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio_pool] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[sqlite_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2 ''' graph TD; @@ -157,6 +342,611 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch ''' +-----------+ @@ -194,6 +984,191 @@ +---------+ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[memory] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio_pipe] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio_pool] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[sqlite_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- # name: test_nested_graph ''' +-----------+ @@ -219,3 +1194,128 @@ +---------+ ''' # --- +# name: test_weather_subgraph[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[sqlite_aio] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- diff --git a/libs/langgraph/tests/any_int.py b/libs/langgraph/tests/any_int.py new file mode 100644 index 000000000..2fb2dba55 --- /dev/null +++ b/libs/langgraph/tests/any_int.py @@ -0,0 +1,6 @@ +class AnyInt(int): + def __init__(self) -> None: + super().__init__() + + def __eq__(self, other: object) -> bool: + return isinstance(other, int) diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 9aa030d3b..9a1977a8c 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -1,24 +1,64 @@ +import re +from typing import Any, Sequence, Union + + class AnyStr(str): + def __init__(self, prefix: Union[str, re.Pattern] = "") -> None: + super().__init__() + self.prefix = prefix + + def __eq__(self, other: object) -> bool: + return isinstance(other, str) and ( + other.startswith(self.prefix) + if isinstance(self.prefix, str) + else self.prefix.match(other) + ) + + def __hash__(self) -> int: + return hash((str(self), self.prefix)) + + +class AnyDict(dict): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, dict) or len(self) != len(other): + return False + for k, v in self.items(): + if kk := next((kk for kk in other if kk == k), None): + if v == other[kk]: + continue + else: + return False + else: + return True + + +class AnyVersion: def __init__(self) -> None: super().__init__() def __eq__(self, other: object) -> bool: - return isinstance(other, str) + return isinstance(other, (str, int, float)) def __hash__(self) -> int: return hash(str(self)) -class ExceptionLike: - def __init__(self, exc: Exception) -> None: - self.exc = exc +class UnsortedSequence: + def __init__(self, *values: Any) -> None: + self.seq = values def __eq__(self, value: object) -> bool: return ( - isinstance(value, Exception) - and self.exc.__class__ == value.__class__ - and str(self.exc) == str(value) + isinstance(value, Sequence) + and len(self.seq) == len(value) + and all(a in value for a in self.seq) ) def __hash__(self) -> int: - return hash((self.exc.__class__, str(self.exc))) + return hash(frozenset(self.seq)) + + def __repr__(self) -> str: + return repr(self.seq) diff --git a/libs/langgraph/tests/conftest.py b/libs/langgraph/tests/conftest.py index 1219ff0f1..5ec5ebafd 100644 --- a/libs/langgraph/tests/conftest.py +++ b/libs/langgraph/tests/conftest.py @@ -1,14 +1,16 @@ -import asyncio import sys -from concurrent.futures import ThreadPoolExecutor -from contextlib import asynccontextmanager, contextmanager -from typing import AsyncIterator, Iterator, TypeVar +from contextlib import asynccontextmanager +from typing import AsyncIterator, Optional from uuid import UUID, uuid4 import pytest +from langchain_core import __version__ as core_version +from packaging import version from psycopg import AsyncConnection, Connection +from psycopg_pool import AsyncConnectionPool, ConnectionPool from pytest_mock import MockerFixture +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.checkpoint.postgres import PostgresSaver from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from langgraph.checkpoint.sqlite import SqliteSaver @@ -16,6 +18,13 @@ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver from tests.memory_assert import MemorySaverAssertImmutable DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/" +# TODO: fix this once core is released +SHOULD_CHECK_SNAPSHOTS = version.parse(core_version) >= version.parse("0.3.0.dev0") + + +@pytest.fixture +def anyio_backend(): + return "asyncio" @pytest.fixture() @@ -26,35 +35,6 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture: return mocker.patch("uuid.uuid4", side_effect=side_effect) -""" -pytest-asyncio doesn't support calling async fixtures with getfixturevalue -so we need to use ThreadPoolExecutor to run the async fixture in a thread -https://github.com/pytest-dev/pytest-asyncio/issues/112#issuecomment-462062890 -""" -T = TypeVar("T") - - -def close_loop(loop: asyncio.AbstractEventLoop) -> None: - loop.run_until_complete(loop.shutdown_asyncgens()) - loop.run_until_complete(loop.shutdown_default_executor()) - asyncio.set_event_loop(None) - loop.close() - - -@contextmanager -def agen_to_gen(agen: AsyncIterator[T]) -> Iterator[T]: - with ThreadPoolExecutor(1) as bg: - loop = asyncio.new_event_loop() - bg.submit(asyncio.set_event_loop, loop).result() - try: - yield bg.submit(loop.run_until_complete, agen.__aenter__()).result() - finally: - bg.submit( - loop.run_until_complete, agen.__aexit__(None, None, None) - ).result() - bg.submit(close_loop, loop).result() - - # checkpointer fixtures @@ -69,12 +49,6 @@ def checkpointer_sqlite(): yield checkpointer -@pytest.fixture(scope="function") -def checkpointer_sqlite_aio(): - with agen_to_gen(_checkpointer_sqlite_aio()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_sqlite_aio(): async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: @@ -123,15 +97,29 @@ def checkpointer_postgres_pipe(): @pytest.fixture(scope="function") -def checkpointer_postgres_aio(): - if sys.version_info < (3, 10): - pytest.skip("Async Postgres tests require Python 3.10+") - with agen_to_gen(_checkpointer_postgres_aio()) as checkpointer: - yield checkpointer +def checkpointer_postgres_pool(): + database = f"test_{uuid4().hex[:16]}" + # create unique db + with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn: + conn.execute(f"CREATE DATABASE {database}") + try: + # yield checkpointer + with ConnectionPool( + DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True} + ) as pool: + checkpointer = PostgresSaver(pool) + checkpointer.setup() + yield checkpointer + finally: + # drop unique db + with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn: + conn.execute(f"DROP DATABASE {database}") @asynccontextmanager async def _checkpointer_postgres_aio(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") database = f"test_{uuid4().hex[:16]}" # create unique db async with await AsyncConnection.connect( @@ -153,16 +141,10 @@ async def _checkpointer_postgres_aio(): await conn.execute(f"DROP DATABASE {database}") -@pytest.fixture(scope="function") -def checkpointer_postgres_aio_pipe(): - if sys.version_info < (3, 10): - pytest.skip("Async Postgres tests require Python 3.10+") - with agen_to_gen(_checkpointer_postgres_aio_pipe()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_postgres_aio_pipe(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") database = f"test_{uuid4().hex[:16]}" # create unique db async with await AsyncConnection.connect( @@ -185,3 +167,73 @@ async def _checkpointer_postgres_aio_pipe(): DEFAULT_POSTGRES_URI, autocommit=True ) as conn: await conn.execute(f"DROP DATABASE {database}") + + +@asynccontextmanager +async def _checkpointer_postgres_aio_pool(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") + database = f"test_{uuid4().hex[:16]}" + # create unique db + async with await AsyncConnection.connect( + DEFAULT_POSTGRES_URI, autocommit=True + ) as conn: + await conn.execute(f"CREATE DATABASE {database}") + try: + # yield checkpointer + async with AsyncConnectionPool( + DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True} + ) as pool: + checkpointer = AsyncPostgresSaver(pool) + await checkpointer.setup() + yield checkpointer + finally: + # drop unique db + async with await AsyncConnection.connect( + DEFAULT_POSTGRES_URI, autocommit=True + ) as conn: + await conn.execute(f"DROP DATABASE {database}") + + +@asynccontextmanager +async def awith_checkpointer( + checkpointer_name: Optional[str], +) -> AsyncIterator[BaseCheckpointSaver]: + if checkpointer_name is None: + yield None + elif checkpointer_name == "memory": + yield MemorySaverAssertImmutable() + elif checkpointer_name == "sqlite_aio": + async with _checkpointer_sqlite_aio() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio": + async with _checkpointer_postgres_aio() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio_pipe": + async with _checkpointer_postgres_aio_pipe() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio_pool": + async with _checkpointer_postgres_aio_pool() as checkpointer: + yield checkpointer + else: + raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}") + + +ALL_CHECKPOINTERS_SYNC = [ + "memory", + "sqlite", + "postgres", + "postgres_pipe", + "postgres_pool", +] +ALL_CHECKPOINTERS_ASYNC = [ + "memory", + "sqlite_aio", + "postgres_aio", + "postgres_aio_pipe", + "postgres_aio_pool", +] +ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [ + *ALL_CHECKPOINTERS_ASYNC, + None, +] diff --git a/libs/langgraph/tests/fake_tracer.py b/libs/langgraph/tests/fake_tracer.py new file mode 100644 index 000000000..28ecc88db --- /dev/null +++ b/libs/langgraph/tests/fake_tracer.py @@ -0,0 +1,91 @@ +from typing import Any, Optional +from uuid import UUID + +from langchain_core.messages.base import BaseMessage +from langchain_core.outputs.chat_generation import ChatGeneration +from langchain_core.outputs.llm_result import LLMResult +from langchain_core.tracers import BaseTracer, Run + + +class FakeTracer(BaseTracer): + """Fake tracer that records LangChain execution. + It replaces run ids with deterministic UUIDs for snapshotting.""" + + def __init__(self) -> None: + """Initialize the tracer.""" + super().__init__() + self.runs: list[Run] = [] + self.uuids_map: dict[UUID, UUID] = {} + self.uuids_generator = ( + UUID(f"00000000-0000-4000-8000-{i:012}", version=4) for i in range(10000) + ) + + def _replace_uuid(self, uuid: UUID) -> UUID: + if uuid not in self.uuids_map: + self.uuids_map[uuid] = next(self.uuids_generator) + return self.uuids_map[uuid] + + def _replace_message_id(self, maybe_message: Any) -> Any: + if isinstance(maybe_message, BaseMessage): + maybe_message.id = str(next(self.uuids_generator)) + if isinstance(maybe_message, ChatGeneration): + maybe_message.message.id = str(next(self.uuids_generator)) + if isinstance(maybe_message, LLMResult): + for i, gen_list in enumerate(maybe_message.generations): + for j, gen in enumerate(gen_list): + maybe_message.generations[i][j] = self._replace_message_id(gen) + if isinstance(maybe_message, dict): + for k, v in maybe_message.items(): + maybe_message[k] = self._replace_message_id(v) + if isinstance(maybe_message, list): + for i, v in enumerate(maybe_message): + maybe_message[i] = self._replace_message_id(v) + + return maybe_message + + def _copy_run(self, run: Run) -> Run: + if run.dotted_order: + levels = run.dotted_order.split(".") + processed_levels = [] + for level in levels: + timestamp, run_id = level.split("Z") + new_run_id = self._replace_uuid(UUID(run_id)) + processed_level = f"{timestamp}Z{new_run_id}" + processed_levels.append(processed_level) + new_dotted_order = ".".join(processed_levels) + else: + new_dotted_order = None + return run.copy( + update={ + "id": self._replace_uuid(run.id), + "parent_run_id": ( + self.uuids_map[run.parent_run_id] if run.parent_run_id else None + ), + "child_runs": [self._copy_run(child) for child in run.child_runs], + "trace_id": self._replace_uuid(run.trace_id) if run.trace_id else None, + "dotted_order": new_dotted_order, + "inputs": self._replace_message_id(run.inputs), + "outputs": self._replace_message_id(run.outputs), + } + ) + + def _persist_run(self, run: Run) -> None: + """Persist a run.""" + + self.runs.append(self._copy_run(run)) + + def flattened_runs(self) -> list[Run]: + q = [] + self.runs + result = [] + while q: + parent = q.pop() + result.append(parent) + if parent.child_runs: + q.extend(parent.child_runs) + return result + + @property + def run_ids(self) -> list[Optional[UUID]]: + runs = self.flattened_runs() + uuids_map = {v: k for k, v in self.uuids_map.items()} + return [uuids_map.get(r.id) for r in runs] diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index b02cbb65b..6b44051f7 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -24,8 +24,6 @@ class NoopSerializer(SerializerProtocol): class MemorySaverAssertImmutable(MemorySaver): - serde = NoopSerializer() - storage_for_copies: defaultdict[str, dict[str, dict[str, Checkpoint]]] def __init__( @@ -59,9 +57,9 @@ class MemorySaverAssertImmutable(MemorySaver): ) == saved ) - self.storage_for_copies[thread_id][checkpoint_ns][ - checkpoint["id"] - ] = self.serde.dumps_typed(copy_checkpoint(checkpoint)) + self.storage_for_copies[thread_id][checkpoint_ns][checkpoint["id"]] = ( + self.serde.dumps_typed(copy_checkpoint(checkpoint)) + ) # call super to write checkpoint return super().put(config, checkpoint, metadata, new_versions) @@ -74,15 +72,6 @@ class MemorySaverAssertCheckpointMetadata(MemorySaver): should produce a side effect that can be asserted. """ - serde = NoopSerializer() - - def __init__( - self, - *, - serde: Optional[SerializerProtocol] = None, - ) -> None: - super().__init__(serde=serde) - def put( self, config: RunnableConfig, diff --git a/libs/langgraph/tests/messages.py b/libs/langgraph/tests/messages.py index be67db1f1..ecc657a36 100644 --- a/libs/langgraph/tests/messages.py +++ b/libs/langgraph/tests/messages.py @@ -6,10 +6,11 @@ Please note that the `id` field is assigned AFTER the model is created to workaround an issue with pydantic ignoring the __eq__ method on subclassed strings. """ + from typing import Any from langchain_core.documents import Document -from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage +from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage from tests.any_str import AnyStr @@ -36,7 +37,14 @@ def _AnyIdAIMessageChunk(**kwargs: Any) -> AIMessageChunk: def _AnyIdHumanMessage(**kwargs: Any) -> HumanMessage: - """Create a human with an any id field.""" + """Create a human message with an any id field.""" message = HumanMessage(**kwargs) message.id = AnyStr() return message + + +def _AnyIdToolMessage(**kwargs: Any) -> ToolMessage: + """Create a tool message with an any id field.""" + message = ToolMessage(**kwargs) + message.id = AnyStr() + return message diff --git a/libs/langgraph/tests/test_algo.py b/libs/langgraph/tests/test_algo.py index cdf179c24..203280488 100644 --- a/libs/langgraph/tests/test_algo.py +++ b/libs/langgraph/tests/test_algo.py @@ -1,7 +1,6 @@ -from langgraph.channels.manager import ChannelsManager from langgraph.checkpoint.base import empty_checkpoint -from langgraph.managed.base import ManagedValuesManager from langgraph.pregel.algo import prepare_next_tasks +from langgraph.pregel.manager import ChannelsManager def test_prepare_next_tasks() -> None: @@ -9,20 +8,18 @@ def test_prepare_next_tasks() -> None: processes = {} checkpoint = empty_checkpoint() - with ManagedValuesManager({}, config) as managed, ChannelsManager( - {}, checkpoint, config - ) as channels: + with ChannelsManager({}, checkpoint, config) as (channels, managed): assert ( prepare_next_tasks( checkpoint, processes, channels, managed, config, 0, for_execution=False ) - == [] + == {} ) assert ( prepare_next_tasks( checkpoint, processes, channels, managed, config, 0, for_execution=True ) - == [] + == {} ) # TODO: add more tests diff --git a/libs/langgraph/tests/test_channels.py b/libs/langgraph/tests/test_channels.py index b7d936878..69a624969 100644 --- a/libs/langgraph/tests/test_channels.py +++ b/libs/langgraph/tests/test_channels.py @@ -1,18 +1,15 @@ import operator -from contextlib import asynccontextmanager, contextmanager -from typing import AsyncGenerator, Generator, Sequence, Union +from typing import Sequence, Union -import httpx import pytest -from langchain_core.runnables import RunnableConfig -from pytest_mock import MockerFixture from langgraph.channels.binop import BinaryOperatorAggregate -from langgraph.channels.context import Context from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.errors import EmptyChannelError, InvalidUpdateError +pytestmark = pytest.mark.anyio + def test_last_value() -> None: with LastValue(int).from_checkpoint(None, {}) as channel: @@ -96,50 +93,6 @@ async def test_topic_async() -> None: assert channel.get() == ["e"] -def test_topic_unique() -> None: - with Topic(str, unique=True).from_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["c", "d"], "de-dupes from current and previous steps" - assert channel.update([]) - with pytest.raises(EmptyChannelError): - channel.get() - assert not channel.update([]), "channel already empty" - assert channel.update(["e"]) - assert channel.get() == ["e"] - checkpoint = channel.checkpoint() - with Topic(str, unique=True).from_checkpoint(checkpoint, {}) as channel: - assert channel.get() == ["e"] - assert channel.update(["d", "f"]) - assert channel.get() == ["f"], "de-dupes from checkpoint" - - -async def test_topic_unique_async() -> None: - async with Topic(str, unique=True).afrom_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["c", "d"], "de-dupes from current and previous steps" - assert channel.update([]) - with pytest.raises(EmptyChannelError): - channel.get() - assert not channel.update([]), "channel already empty" - assert channel.update(["e"]) - assert channel.get() == ["e"] - checkpoint = channel.checkpoint() - async with Topic(str, unique=True).afrom_checkpoint(checkpoint, {}) as channel: - assert channel.get() == ["e"] - assert channel.update(["d", "f"]) - assert channel.get() == ["f"], "de-dupes from checkpoint" - - def test_topic_accumulate() -> None: with Topic(str, accumulate=True).from_checkpoint(None, {}) as channel: assert channel.ValueType is Sequence[str] @@ -176,49 +129,6 @@ async def test_topic_accumulate_async() -> None: assert channel.get() == ["a", "b", "b", "c", "d", "d", "e"] -def test_topic_unique_accumulate() -> None: - with Topic(str, unique=True, accumulate=True).from_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["a", "b", "c", "d"] - assert not channel.update(["c"]), "no new values" - assert not channel.update([]) - assert channel.get() == ["a", "b", "c", "d"] - checkpoint = channel.checkpoint() - with Topic(str, unique=True, accumulate=True).from_checkpoint( - checkpoint, {} - ) as channel: - assert channel.get() == ["a", "b", "c", "d"] - assert channel.update(["d", "e"]) - assert channel.get() == ["a", "b", "c", "d", "e"] - - -async def test_topic_unique_accumulate_async() -> None: - async with Topic(str, unique=True, accumulate=True).afrom_checkpoint( - None, {} - ) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["a", "b", "c", "d"] - channel.update([]) - assert channel.get() == ["a", "b", "c", "d"] - checkpoint = channel.checkpoint() - async with Topic(str, unique=True, accumulate=True).afrom_checkpoint( - checkpoint, {} - ) as channel: - assert channel.get() == ["a", "b", "c", "d"] - channel.update(["d", "e"]) - assert channel.get() == ["a", "b", "c", "d", "e"] - - def test_binop() -> None: with BinaryOperatorAggregate(int, operator.add).from_checkpoint( None, {} @@ -257,80 +167,3 @@ async def test_binop_async() -> None: checkpoint, {} ) as channel: assert channel.get() == 10 - - -def test_ctx_manager(mocker: MockerFixture) -> None: - setup = mocker.Mock() - cleanup = mocker.Mock() - - @contextmanager - def an_int() -> Generator[int, None, None]: - setup() - try: - yield 5 - finally: - cleanup() - - with Context(an_int, None).from_checkpoint(None, {}) as channel: - assert setup.call_count == 1 - assert cleanup.call_count == 0 - - assert channel.ValueType is None - assert channel.UpdateType is None - - assert channel.get() == 5 - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - assert setup.call_count == 1 - assert cleanup.call_count == 1 - - -def test_ctx_manager_ctx(mocker: MockerFixture) -> None: - with Context(httpx.Client).from_checkpoint(None, {}) as channel: - assert channel.ValueType is None - assert channel.UpdateType is None - - assert isinstance(channel.get(), httpx.Client) - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - with pytest.raises(EmptyChannelError): - channel.checkpoint() - - -async def test_ctx_manager_async(mocker: MockerFixture) -> None: - setup = mocker.Mock() - cleanup = mocker.Mock() - - @contextmanager - def an_int_sync(config: RunnableConfig) -> Generator[int, None, None]: - try: - yield 5 - finally: - pass - - @asynccontextmanager - async def an_int() -> AsyncGenerator[int, None]: - setup() - try: - yield 5 - finally: - cleanup() - - async with Context(an_int_sync, an_int).afrom_checkpoint(None, {}) as channel: - assert setup.call_count == 1 - assert cleanup.call_count == 0 - - assert channel.ValueType is None - assert channel.UpdateType is None - - assert channel.get() == 5 - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - assert setup.call_count == 1 - assert cleanup.call_count == 1 diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index e6682e964..d4e618ddf 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -4,12 +4,16 @@ import pytest from pytest_mock import MockerFixture from langgraph.graph import END, START, StateGraph - - -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_SYNC, + awith_checkpointer, ) + +pytestmark = pytest.mark.anyio + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_interruption_without_state_updates( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -47,12 +51,9 @@ def test_interruption_without_state_updates( assert graph.get_state(thread).next == () -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_interruption_without_state_updates_async( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ): """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -72,17 +73,17 @@ async def test_interruption_without_state_updates_async( builder.add_edge("step_2", "step_3") builder.add_edge("step_3", END) - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - graph = builder.compile(checkpointer=checkpointer, interrupt_after="*") + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer, interrupt_after="*") - initial_input = {"input": "hello world"} - thread = {"configurable": {"thread_id": "1"}} + initial_input = {"input": "hello world"} + thread = {"configurable": {"thread_id": "1"}} - await graph.ainvoke(initial_input, thread, debug=True) - assert (await graph.aget_state(thread)).next == ("step_2",) + await graph.ainvoke(initial_input, thread, debug=True) + assert (await graph.aget_state(thread)).next == ("step_2",) - await graph.ainvoke(None, thread, debug=True) - assert (await graph.aget_state(thread)).next == ("step_3",) + await graph.ainvoke(None, thread, debug=True) + assert (await graph.aget_state(thread)).next == ("step_3",) - await graph.ainvoke(None, thread, debug=True) - assert (await graph.aget_state(thread)).next == () + await graph.ainvoke(None, thread, debug=True) + assert (await graph.aget_state(thread)).next == () diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index ba9c88afc..42dca7ce9 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,4 +1,17 @@ -from typing import Annotated, Any, Callable, Dict, List, Optional, Sequence, Type, Union +import dataclasses +import json +from typing import ( + Annotated, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Type, + TypeVar, + Union, +) import pytest from langchain_core.callbacks import CallbackManagerForLLMRun @@ -9,19 +22,29 @@ from langchain_core.messages import ( BaseMessage, HumanMessage, SystemMessage, + ToolCall, ToolMessage, ) from langchain_core.outputs import ChatGeneration, ChatResult -from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable, RunnableLambda from langchain_core.tools import BaseTool from langchain_core.tools import tool as dec_tool -from pydantic import BaseModel as BaseModelV2 +from pydantic import BaseModel +from pydantic.v1 import BaseModel as BaseModelV1 +from typing_extensions import TypedDict +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent from langgraph.prebuilt.tool_node import InjectedState +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_SYNC, + awith_checkpointer, +) from tests.messages import _AnyIdHumanMessage +pytestmark = pytest.mark.anyio + class FakeToolCallingModel(BaseChatModel): def _generate( @@ -50,12 +73,11 @@ class FakeToolCallingModel(BaseChatModel): return self -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + "checkpointer_" + checkpointer_name + ) model = FakeToolCallingModel() agent = create_react_agent(model, [], checkpointer=checkpointer) @@ -76,6 +98,7 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> "agent": "agent", } assert saved.metadata == { + "parents": {}, "source": "loop", "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, "step": 1, @@ -83,40 +106,35 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> assert saved.pending_writes == [] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_no_modifier_async( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_no_modifier_async(checkpointer_name: str) -> None: + async with awith_checkpointer(checkpointer_name) as checkpointer: + model = FakeToolCallingModel() - model = FakeToolCallingModel() + agent = create_react_agent(model, [], checkpointer=checkpointer) + inputs = [HumanMessage("hi?")] + thread = {"configurable": {"thread_id": "123"}} + response = await agent.ainvoke({"messages": inputs}, thread, debug=True) + expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} + assert response == expected_response - agent = create_react_agent(model, [], checkpointer=checkpointer) - inputs = [HumanMessage("hi?")] - thread = {"configurable": {"thread_id": "123"}} - response = await agent.ainvoke({"messages": inputs}, thread, debug=True) - expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} - assert response == expected_response - - if checkpointer: - saved = await checkpointer.aget_tuple(thread) - assert saved is not None - assert saved.checkpoint["channel_values"] == { - "messages": [ - _AnyIdHumanMessage(content="hi?"), - AIMessage(content="hi?", id="0"), - ], - "agent": "agent", - } - assert saved.metadata == { - "source": "loop", - "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, - "step": 1, - } - assert saved.pending_writes == [] + if checkpointer: + saved = await checkpointer.aget_tuple(thread) + assert saved is not None + assert saved.checkpoint["channel_values"] == { + "messages": [ + _AnyIdHumanMessage(content="hi?"), + AIMessage(content="hi?", id="0"), + ], + "agent": "agent", + } + assert saved.metadata == { + "parents": {}, + "source": "loop", + "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, + "step": 1, + } + assert saved.pending_writes == [] def test_passing_two_modifiers(): @@ -374,7 +392,7 @@ class MyModel(BaseModel): some_other_val: str -class MyModelV2(BaseModelV2): +class MyModelV1(BaseModelV1): some_val: int some_other_val: str @@ -390,7 +408,7 @@ def my_tool(some_val: int, some_other_val: str) -> str: [ my_function, MyModel, - MyModelV2, + MyModelV1, my_tool, ], ) @@ -435,14 +453,53 @@ async def test_validation_node(tool_schema: Any, use_message_key: bool): check_results(result_sync) -def test_tool_node_inject_state() -> None: - def tool1(some_val: int, state: Annotated[dict, InjectedState]) -> str: - """Tool 1 docstring.""" - return state["foo"] +class _InjectStateSchema(TypedDict): + messages: list + foo: str - def tool2(some_val: int, state: Annotated[dict, InjectedState()]) -> str: + +class _InjectedStatePydanticSchema(BaseModelV1): + messages: list + foo: str + + +class _InjectedStatePydanticV2Schema(BaseModel): + messages: list + foo: str + + +@dataclasses.dataclass +class _InjectedStateDataclassSchema: + messages: list + foo: str + + +T = TypeVar("T") + + +@pytest.mark.parametrize( + "schema_", + [ + _InjectStateSchema, + _InjectedStatePydanticSchema, + _InjectedStatePydanticV2Schema, + _InjectedStateDataclassSchema, + ], +) +def test_tool_node_inject_state(schema_: Type[T]) -> None: + def tool1(some_val: int, state: Annotated[T, InjectedState]) -> str: """Tool 1 docstring.""" - return state["foo"] + if isinstance(state, dict): + return state["foo"] + else: + return getattr(state, "foo") + + def tool2(some_val: int, state: Annotated[T, InjectedState()]) -> str: + """Tool 2 docstring.""" + if isinstance(state, dict): + return state["foo"] + else: + return getattr(state, "foo") def tool3( some_val: int, @@ -467,23 +524,36 @@ def test_tool_node_inject_state() -> None: "type": "tool_call", } msg = AIMessage("hi?", tool_calls=[tool_call]) - result = node.invoke({"messages": [msg], "foo": "bar"}) + result = node.invoke(schema_(**{"messages": [msg], "foo": "bar"})) tool_message = result["messages"][-1] - assert tool_message.content == "bar" + assert tool_message.content == "bar", f"Failed for tool={tool_name}" if tool_name == "tool3": - with pytest.raises(KeyError): - node.invoke({"messages": [msg], "notfoo": "bar"}) + failure_input = None + try: + failure_input = schema_(**{"messages": [msg], "notfoo": "bar"}) + except Exception: + pass + if failure_input is not None: + with pytest.raises(KeyError): + node.invoke(failure_input) - with pytest.raises(ValueError): - node.invoke([msg]) + with pytest.raises(ValueError): + node.invoke([msg]) else: - tool_message = node.invoke({"messages": [msg], "notfoo": "bar"})[ - "messages" - ][-1] - assert "KeyError" in tool_message.content - tool_message = node.invoke([msg])[-1] - assert "KeyError" in tool_message.content + failure_input = None + try: + failure_input = schema_(**{"messages": [msg], "notfoo": "bar"}) + except Exception: + # We'd get a validation error from pydantic state and wouldn't make it to the node + # anyway + pass + if failure_input is not None: + messages_ = node.invoke(failure_input) + tool_message = messages_["messages"][-1] + assert "KeyError" in tool_message.content + tool_message = node.invoke([msg])[-1] + assert "KeyError" in tool_message.content tool_call = { "name": "tool4", @@ -492,10 +562,25 @@ def test_tool_node_inject_state() -> None: "type": "tool_call", } msg = AIMessage("hi?", tool_calls=[tool_call]) - result = node.invoke({"messages": [msg]}) + result = node.invoke(schema_(**{"messages": [msg], "foo": ""})) tool_message = result["messages"][-1] assert tool_message.content == "hi?" result = node.invoke([msg]) tool_message = result[-1] assert tool_message.content == "hi?" + + +def test_tool_node_ensure_utf8() -> None: + @dec_tool + def get_day_list(days: list[str]) -> list[str]: + """choose days""" + return days + + data = ["星期一", "水曜日", "목요일", "Friday"] + tools = [get_day_list] + tool_calls = [ToolCall(name=get_day_list.name, args={"days": data}, id="test_id")] + outputs: list[ToolMessage] = ToolNode(tools).invoke( + [AIMessage(content="", tool_calls=tool_calls)] + ) + assert outputs[0].content == json.dumps(data, ensure_ascii=False) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 14a96b577..335ccc2a5 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1,5 +1,6 @@ import json import operator +import re import time import warnings from collections import Counter @@ -19,6 +20,7 @@ from typing import ( Tuple, TypedDict, Union, + cast, get_type_hints, ) @@ -32,6 +34,7 @@ from langchain_core.runnables import ( RunnablePick, ) from langsmith import traceable +from pydantic import BaseModel from pytest_mock import MockerFixture from syrupy import SnapshotAssertion @@ -49,30 +52,71 @@ from langgraph.checkpoint.base import ( CheckpointTuple, ) from langgraph.checkpoint.memory import MemorySaver -from langgraph.checkpoint.serde.base import SerializerProtocol -from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -from langgraph.checkpoint.sqlite import SqliteSaver -from langgraph.constants import ERROR, Send -from langgraph.errors import InvalidUpdateError +from langgraph.constants import ERROR, Interrupt, Send +from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph +from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) from langgraph.prebuilt.tool_node import ToolNode -from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot +from langgraph.pregel import ( + Channel, + GraphRecursionError, + Pregel, + StateSnapshot, +) from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask -from tests.any_str import AnyStr, ExceptionLike -from tests.memory_assert import ( - MemorySaverAssertCheckpointMetadata, - MemorySaverAssertImmutable, - MemorySaverNoPending, - NoopSerializer, -) -from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage +from langgraph.store.memory import MemoryStore +from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence +from tests.conftest import ALL_CHECKPOINTERS_SYNC, SHOULD_CHECK_SNAPSHOTS +from tests.fake_tracer import FakeTracer +from tests.memory_assert import MemorySaverAssertCheckpointMetadata +from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage, _AnyIdToolMessage + + +# define these objects to avoid importing langchain_core.agents +# and therefore avoid relying on core Pydantic version +class AgentAction(BaseModel): + tool: str + tool_input: Union[str, dict] + log: str + type: Literal["AgentAction"] = "AgentAction" + + model_config = { + "json_schema_extra": { + "description": ( + """Represents a request to execute an action by an agent. + +The action consists of the name of the tool to execute and the input to pass +to the tool. The log is used to pass along extra information about the action.""" + ) + } + } + + +class AgentFinish(BaseModel): + """Final return value of an ActionAgent. + + Agents return an AgentFinish when they have reached a stopping condition. + """ + + return_values: dict + log: str + type: Literal["AgentFinish"] = "AgentFinish" + model_config = { + "json_schema_extra": { + "description": ( + """Final return value of an ActionAgent. + +Agents return an AgentFinish when they have reached a stopping condition.""" + ) + } + } def test_graph_validation() -> None: @@ -186,8 +230,7 @@ def test_graph_validation() -> None: with pytest.raises(ValueError, match="Found edge starting at unknown node "): graph.compile() - def bad_reducer(a): - ... + def bad_reducer(a): ... class BadReducerState(TypedDict): hello: Annotated[str, bad_reducer] @@ -195,6 +238,21 @@ def test_graph_validation() -> None: with pytest.raises(ValueError, match="Invalid reducer"): StateGraph(BadReducerState) + def node_b(state: State) -> State: + return {"hello": "world"} + + builder = StateGraph(State) + builder.add_node("a", node_b) + builder.add_node("b", node_b) + builder.add_node("c", node_b) + builder.set_entry_point("a") + builder.add_edge("a", "b") + builder.add_edge("a", "c") + graph = builder.compile() + + with pytest.raises(InvalidUpdateError, match="At key 'hello'"): + graph.invoke({"hello": "there"}) + def test_checkpoint_errors() -> None: class FaultyGetCheckpointer(MemorySaver): @@ -274,7 +332,6 @@ def test_node_schemas_custom_output() -> None: def node_b(state: StateForB): assert state == { "bye": "world", - "now": None, } return { "now": 123, @@ -424,15 +481,23 @@ def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: graph.set_finish_point("add_one") gapp = graph.compile() - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} - with warnings.catch_warnings(): - warnings.simplefilter("error") # raise warnings as errors - assert app.config_schema().schema() == { - "properties": {}, - "title": "LangGraphConfig", - "type": "object", + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "integer", + } + with warnings.catch_warnings(): + warnings.simplefilter("error") # raise warnings as errors + assert app.config_schema().model_json_schema() == { + "properties": {}, + "title": "LangGraphConfig", + "type": "object", + } + assert app.invoke(2) == 3 assert app.invoke(2, output_keys=["output"]) == {"output": 3} assert repr(app), "does not raise recursion error" @@ -473,16 +538,24 @@ def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> None: input_channels="input", ) - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": { - "output": {"title": "Output", "type": "integer"}, - "fixed": {"title": "Fixed", "type": "integer"}, - "output_plus_one": {"title": "Output Plus One", "type": "integer"}, - }, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None}, + "fixed": {"title": "Fixed", "type": "integer", "default": None}, + "output_plus_one": { + "title": "Output Plus One", + "type": "integer", + "default": None, + }, + }, + } assert app.invoke(2) == {"output": 3, "fixed": 5, "output_plus_one": 4} @@ -497,12 +570,18 @@ def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: output_channels=["output"], ) - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": {"output": {"title": "Output", "type": "integer"}}, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None} + }, + } assert app.invoke(2) == {"output": 3} @@ -516,17 +595,21 @@ def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> None: input_channels=["input"], output_channels=["output"], ) - - assert app.input_schema.schema() == { - "title": "LangGraphInput", - "type": "object", - "properties": {"input": {"title": "Input", "type": "integer"}}, - } - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": {"output": {"title": "Output", "type": "integer"}}, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "object", + "properties": { + "input": {"title": "Input", "type": "integer", "default": None} + }, + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None} + }, + } assert app.invoke({"input": 2}) == {"output": 3} @@ -575,10 +658,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert step == 2 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_invoke_two_processes_in_out_interrupt( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -655,7 +735,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 6, "writes": 5}, + metadata={"parents": {}, "source": "loop", "step": 6, "writes": {"two": 5}}, created_at=AnyStr(), parent_config=history[1].config, ), @@ -670,7 +750,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": None}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -685,7 +770,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 4, "writes": 3}, + metadata={ + "parents": {}, + "source": "input", + "step": 4, + "writes": {"input": 3}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -700,7 +790,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": None}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -715,7 +810,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 2, "writes": 20}, + metadata={ + "parents": {}, + "source": "input", + "step": 2, + "writes": {"input": 20}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -730,7 +830,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": 4}, + metadata={"parents": {}, "source": "loop", "step": 1, "writes": {"two": 4}}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -745,7 +845,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[7].config, ), @@ -760,36 +865,28 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 2}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"input": 2}, + }, created_at=AnyStr(), parent_config=None, ), ] - # forking from any previous checkpoint w/out forking should do nothing + # re-running from any previous checkpoint should re-run nodes assert [c for c in app.stream(None, history[0].config, stream_mode="updates")] == [] - assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [] - assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = app.update_state(history[0].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [] - - fork_config = app.update_state(history[1].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [ - {"two": {"output": 5}} + assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [ + {"two": {"output": 5}}, ] - - fork_config = app.update_state(history[2].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [ - {"one": {"inbox": 4}} + assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [ + {"one": {"inbox": 4}}, ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_fork_always_re_runs_nodes( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -833,7 +930,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[1].config, ), @@ -848,7 +950,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 4, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -863,7 +970,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -878,7 +990,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 2, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -893,7 +1010,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -908,7 +1030,7 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -923,31 +1045,29 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 1}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": 1}, + }, created_at=AnyStr(), parent_config=None, ), ] - # forking from any previous checkpoint w/out forking should do nothing + # forking from any previous checkpoint should re-run nodes assert [ c for c in graph.stream(None, history[0].config, stream_mode="updates") ] == [] assert [ c for c in graph.stream(None, history[1].config, stream_mode="updates") - ] == [] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = graph.update_state(history[0].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [] - - fork_config = graph.update_state(history[1].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [ - {"add_one": 1} + ] == [ + {"add_one": 1}, ] - - fork_config = graph.update_state(history[2].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [ + assert [ + c for c in graph.stream(None, history[2].config, stream_mode="updates") + ] == [ {"add_one": 1}, {"add_one": 1}, ] @@ -1005,7 +1125,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "input": 2, "triggers": ["input"], @@ -1016,7 +1136,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "input": [12], "triggers": ["inbox"], @@ -1027,9 +1147,11 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "result": [("inbox", 3)], + "error": None, + "interrupts": [], }, }, { @@ -1037,9 +1159,11 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "result": [("output", 13)], + "error": None, + "interrupts": [], }, }, { @@ -1047,7 +1171,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "input": [3], "triggers": ["inbox"], @@ -1058,9 +1182,11 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "result": [("output", 4)], + "error": None, + "interrupts": [], }, }, ] @@ -1188,6 +1314,21 @@ def test_invoke_two_processes_two_in_two_out_invalid(mocker: MockerFixture) -> N # LastValue channels can only be updated once per iteration app.invoke(2) + class State(TypedDict): + hello: str + + def my_node(input: State) -> State: + return {"hello": "world"} + + builder = StateGraph(State) + builder.add_node("one", my_node) + builder.add_node("two", my_node) + builder.set_conditional_entry_point(lambda _: ["one", "two"]) + + graph = builder.compile() + with pytest.raises(InvalidUpdateError, match="At key 'hello'"): + graph.invoke({"hello": "there"}, debug=True) + def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) @@ -1209,7 +1350,13 @@ def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> Non assert app.invoke(2) == [3, 3] -def test_invoke_checkpoint(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_invoke_checkpoint_two( + mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) errored_once = False @@ -1232,8 +1379,6 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemorySaverAssertImmutable() - app = Pregel( nodes={"one": one}, channels={ @@ -1243,45 +1388,42 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: }, input_channels="input", output_channels="output", - checkpointer=memory, + checkpointer=checkpointer, retry_policy=RetryPolicy(), ) # total starts out as 0, so output is 0+2=2 assert app.invoke(2, {"configurable": {"thread_id": "1"}}) == 2 - checkpoint = memory.get({"configurable": {"thread_id": "1"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 2 # total is now 2, so output is 2+3=5 assert app.invoke(3, {"configurable": {"thread_id": "1"}}) == 5 assert errored_once, "errored and retried" - checkpoint_tup = memory.get_tuple({"configurable": {"thread_id": "1"}}) + checkpoint_tup = checkpointer.get_tuple({"configurable": {"thread_id": "1"}}) assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 # total is now 2+5=7, so output would be 7+4=11, but raises ValueError with pytest.raises(ValueError): app.invoke(4, {"configurable": {"thread_id": "1"}}) # checkpoint is not updated, error is recorded - checkpoint_tup = memory.get_tuple({"configurable": {"thread_id": "1"}}) + checkpoint_tup = checkpointer.get_tuple({"configurable": {"thread_id": "1"}}) assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 assert checkpoint_tup.pending_writes == [ - (AnyStr(), ERROR, ExceptionLike(ValueError("Input is too large"))) + (AnyStr(), ERROR, "ValueError('Input is too large')") ] # on a new thread, total starts out as 0, so output is 0+5=5 assert app.invoke(5, {"configurable": {"thread_id": "2"}}) == 5 - checkpoint = memory.get({"configurable": {"thread_id": "1"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 7 - checkpoint = memory.get({"configurable": {"thread_id": "2"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "2"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 5 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_pending_writes_resume( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -1309,8 +1451,8 @@ def test_pending_writes_resume( def reset(self): self.calls = 0 - one = AwhileMaker(0.2, {"value": 2}) - two = AwhileMaker(0.6, ConnectionError("I'm not good")) + one = AwhileMaker(0.1, {"value": 2}) + two = AwhileMaker(0.3, ConnectionError("I'm not good")) builder = StateGraph(State) builder.add_node("one", one) builder.add_node("two", two, retry=RetryPolicy(max_attempts=2)) @@ -1333,9 +1475,14 @@ def test_pending_writes_resume( assert state.next == ("one", "two") assert state.tasks == ( PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ConnectionError("I'm not good"))), + PregelTask(AnyStr(), "two", 'ConnectionError("I\'m not good")'), ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } # should contain pending write of "one" checkpoint = checkpointer.get_tuple(thread1) assert checkpoint is not None @@ -1343,7 +1490,7 @@ def test_pending_writes_resume( expected_writes = [ (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), ERROR, 'ConnectionError("I\'m not good")'), ] assert len(checkpoint.pending_writes) == 3 assert all(w in expected_writes for w in checkpoint.pending_writes) @@ -1372,6 +1519,150 @@ def test_pending_writes_resume( # both the pending write and the new write were applied, 1 + 2 + 3 = 6 assert graph.invoke(None, thread1) == {"value": 6} + # check all final checkpoints + checkpoints = [c for c in checkpointer.list(thread1)] + # we should have 3 + assert len(checkpoints) == 3 + # the last one not too interesting for this test + assert checkpoints[0] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "one": { + "start:one": AnyVersion(), + }, + "two": { + "start:two": AnyVersion(), + }, + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + "__interrupt__": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + }, + "channel_versions": { + "one": AnyVersion(), + "two": AnyVersion(), + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": {"one": "one", "two": "two", "value": 6}, + }, + metadata={ + "parents": {}, + "step": 1, + "source": "loop", + "writes": {"one": {"value": 2}, "two": {"value": 3}}, + }, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[1].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=[], + ) + # the previous one we assert that pending writes contains both + # - original error + # - successful writes from resuming after preventing error + assert checkpoints[1] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + }, + "channel_versions": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": { + "value": 1, + "start:one": "__start__", + "start:two": "__start__", + }, + }, + metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=UnsortedSequence( + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), + (AnyStr(), "two", "two"), + (AnyStr(), "value", 3), + ), + ) + assert checkpoints[2] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": {"__input__": {}}, + "channel_versions": { + "__start__": AnyVersion(), + }, + "channel_values": {"__start__": {"value": 1}}, + }, + metadata={ + "parents": {}, + "step": -1, + "source": "input", + "writes": {"__start__": {"value": 1}}, + }, + parent_config=None, + pending_writes=UnsortedSequence( + (AnyStr(), "value", 1), + (AnyStr(), "start:one", "__start__"), + (AnyStr(), "start:two", "__start__"), + ), + ) + def test_cond_edge_after_send() -> None: class Node: @@ -1399,30 +1690,11 @@ def test_cond_edge_after_send() -> None: assert graph.invoke(["0"]) == ["0", "1", "2", "2", "3"] -async def test_checkpointer_null_pending_writes() -> None: - class Node: - def __init__(self, name: str): - self.name = name - setattr(self, "__name__", name) - - def __call__(self, state): - return [self.name] - - builder = StateGraph(Annotated[list, operator.add]) - builder.add_node(Node("1")) - builder.add_edge(START, "1") - graph = builder.compile(checkpointer=MemorySaverNoPending()) - assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] - assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2 - assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ - "1" - ] * 3 - assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ - "1" - ] * 4 - - -def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_invoke_checkpoint_three( + mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") adder = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) def raise_if_above_10(input: int) -> int: @@ -1437,121 +1709,119 @@ def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None: | raise_if_above_10 ) - with SqliteSaver.from_conn_string(":memory:") as memory: - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=memory, - ) + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + ) - thread_1 = {"configurable": {"thread_id": "1"}} - # total starts out as 0, so output is 0+2=2 - assert app.invoke(2, thread_1, debug=1) == 2 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 2 - assert state.next == () - assert ( - state.config["configurable"]["checkpoint_id"] == memory.get(thread_1)["id"] - ) - # total is now 2, so output is 2+3=5 - assert app.invoke(3, thread_1) == 5 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert ( - state.config["configurable"]["checkpoint_id"] == memory.get(thread_1)["id"] - ) - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - app.invoke(4, thread_1) - # checkpoint is updated with new input - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert state.next == ("one",) - """we checkpoint inputs and it failed on "one", so the next node is one""" - # we can recover from error by sending new inputs - assert app.invoke(2, thread_1) == 9 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 16, "total is now 7+9=16" - assert state.next == () + thread_1 = {"configurable": {"thread_id": "1"}} + # total starts out as 0, so output is 0+2=2 + assert app.invoke(2, thread_1, debug=1) == 2 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 2 + assert state.next == () + assert ( + state.config["configurable"]["checkpoint_id"] + == checkpointer.get(thread_1)["id"] + ) + # total is now 2, so output is 2+3=5 + assert app.invoke(3, thread_1) == 5 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert ( + state.config["configurable"]["checkpoint_id"] + == checkpointer.get(thread_1)["id"] + ) + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + app.invoke(4, thread_1) + # checkpoint is updated with new input + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert state.next == ("one",) + """we checkpoint inputs and it failed on "one", so the next node is one""" + # we can recover from error by sending new inputs + assert app.invoke(2, thread_1) == 9 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 16, "total is now 7+9=16" + assert state.next == () - thread_2 = {"configurable": {"thread_id": "2"}} - # on a new thread, total starts out as 0, so output is 0+5=5 - assert app.invoke(5, thread_2, debug=True) == 5 - state = app.get_state({"configurable": {"thread_id": "1"}}) - assert state is not None - assert state.values.get("total") == 16 - assert state.next == (), "checkpoint of other thread not touched" - state = app.get_state(thread_2) - assert state is not None - assert state.values.get("total") == 5 - assert state.next == () + thread_2 = {"configurable": {"thread_id": "2"}} + # on a new thread, total starts out as 0, so output is 0+5=5 + assert app.invoke(5, thread_2, debug=True) == 5 + state = app.get_state({"configurable": {"thread_id": "1"}}) + assert state is not None + assert state.values.get("total") == 16 + assert state.next == (), "checkpoint of other thread not touched" + state = app.get_state(thread_2) + assert state is not None + assert state.values.get("total") == 5 + assert state.next == () - assert len(list(app.get_state_history(thread_1, limit=1))) == 1 - # list all checkpoints for thread 1 - thread_1_history = [c for c in app.get_state_history(thread_1)] - # there are 7 checkpoints - assert len(thread_1_history) == 7 - assert Counter(c.metadata["source"] for c in thread_1_history) == { - "input": 4, - "loop": 3, - } - # sorted descending - assert ( - thread_1_history[0].config["configurable"]["checkpoint_id"] - > thread_1_history[1].config["configurable"]["checkpoint_id"] - ) - # cursor pagination - cursored = list( - app.get_state_history(thread_1, limit=1, before=thread_1_history[0].config) - ) - assert len(cursored) == 1 - assert cursored[0].config == thread_1_history[1].config - # the last checkpoint - assert thread_1_history[0].values["total"] == 16 - # the first "loop" checkpoint - assert thread_1_history[-2].values["total"] == 2 - # can get each checkpoint using aget with config - assert ( - memory.get(thread_1_history[0].config)["id"] - == thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - assert ( - memory.get(thread_1_history[1].config)["id"] - == thread_1_history[1].config["configurable"]["checkpoint_id"] - ) + assert len(list(app.get_state_history(thread_1, limit=1))) == 1 + # list all checkpoints for thread 1 + thread_1_history = [c for c in app.get_state_history(thread_1)] + # there are 7 checkpoints + assert len(thread_1_history) == 7 + assert Counter(c.metadata["source"] for c in thread_1_history) == { + "input": 4, + "loop": 3, + } + # sorted descending + assert ( + thread_1_history[0].config["configurable"]["checkpoint_id"] + > thread_1_history[1].config["configurable"]["checkpoint_id"] + ) + # cursor pagination + cursored = list( + app.get_state_history(thread_1, limit=1, before=thread_1_history[0].config) + ) + assert len(cursored) == 1 + assert cursored[0].config == thread_1_history[1].config + # the last checkpoint + assert thread_1_history[0].values["total"] == 16 + # the first "loop" checkpoint + assert thread_1_history[-2].values["total"] == 2 + # can get each checkpoint using aget with config + assert ( + checkpointer.get(thread_1_history[0].config)["id"] + == thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + assert ( + checkpointer.get(thread_1_history[1].config)["id"] + == thread_1_history[1].config["configurable"]["checkpoint_id"] + ) - thread_1_next_config = app.update_state(thread_1_history[1].config, 10) - # update creates a new checkpoint - assert ( - thread_1_next_config["configurable"]["checkpoint_id"] - > thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - # update makes new checkpoint child of the previous one - assert ( - app.get_state(thread_1_next_config).parent_config - == thread_1_history[1].config - ) - # 1 more checkpoint in history - assert len(list(app.get_state_history(thread_1))) == 8 - assert Counter( - c.metadata["source"] for c in app.get_state_history(thread_1) - ) == { - "update": 1, - "input": 4, - "loop": 3, - } - # the latest checkpoint is the updated one - assert app.get_state(thread_1) == app.get_state(thread_1_next_config) + thread_1_next_config = app.update_state(thread_1_history[1].config, 10) + # update creates a new checkpoint + assert ( + thread_1_next_config["configurable"]["checkpoint_id"] + > thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + # update makes new checkpoint child of the previous one + assert ( + app.get_state(thread_1_next_config).parent_config == thread_1_history[1].config + ) + # 1 more checkpoint in history + assert len(list(app.get_state_history(thread_1))) == 8 + assert Counter(c.metadata["source"] for c in app.get_state_history(thread_1)) == { + "update": 1, + "input": 4, + "loop": 3, + } + # the latest checkpoint is the updated one + assert app.get_state(thread_1) == app.get_state(thread_1_next_config) def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None: @@ -1645,9 +1915,7 @@ def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) one = ( - Channel.subscribe_to("input") - | add_one - | Channel.write_to(output=RunnablePassthrough(), between=RunnablePassthrough()) + Channel.subscribe_to("input") | add_one | Channel.write_to("output", "between") ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") @@ -1700,7 +1968,7 @@ def test_invoke_two_processes_no_in(mocker: MockerFixture) -> None: one = Channel.subscribe_to("between") | add_one | Channel.write_to("output") two = Channel.subscribe_to("between") | add_one - with pytest.raises(ValueError): + with pytest.raises(TypeError): Pregel(nodes={"one": one, "two": two}) @@ -1741,7 +2009,6 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup.call_count == 0 for i, chunk in enumerate(app.stream(2)): assert setup.call_count == 1, "Expected setup to be called once" - assert cleanup.call_count == 0, "Expected cleanup to not be called yet" if i == 0: assert chunk == {"inbox": [3]} elif i == 1: @@ -1751,15 +2018,19 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup.call_count == 1, "Expected cleanup to be called once" -def test_conditional_graph(snapshot: SnapshotAssertion) -> None: - from copy import deepcopy - - from langchain_core.agents import AgentAction, AgentFinish +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_conditional_graph( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + # Assemble the tools @tool() def search_api(query: str) -> str: @@ -1791,13 +2062,16 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # Define tool execution logic def execute_tools(data: dict) -> dict: + data = data.copy() agent_action: AgentAction = data.pop("agent_outcome") observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] - data["intermediate_steps"].append((agent_action, observation)) + else: + data["intermediate_steps"] = data["intermediate_steps"].copy() + data["intermediate_steps"].append([agent_action, observation]) return data # Define decision-making logic @@ -1812,7 +2086,9 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: workflow = Graph() workflow.add_node("agent", agent) - workflow.add_node("tools", execute_tools, metadata={"version": 2, "variant": "b"}) + workflow.add_node( + "tools", execute_tools, metadata={"parents": {}, "version": 2, "variant": "b"} + ) workflow.set_entry_point("agent") @@ -1824,39 +2100,39 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: app = workflow.compile() - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot - assert app.get_graph().draw_mermaid() == snapshot - assert json.dumps(app.get_graph(xray=True).to_json(), indent=2) == snapshot - assert app.get_graph(xray=True).draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + assert app.get_graph().draw_mermaid() == snapshot + assert json.dumps(app.get_graph(xray=True).to_json(), indent=2) == snapshot + assert app.get_graph(xray=True).draw_mermaid(with_styles=False) == snapshot assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" ), } - # deepcopy because the nodes mutate the data - assert [deepcopy(c) for c in app.stream({"input": "what is weather in sf"})] == [ + assert [c for c in app.stream({"input": "what is weather in sf"})] == [ { "agent": { "input": "what is weather in sf", @@ -1869,14 +2145,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -1884,14 +2160,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -1904,22 +2180,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -1927,22 +2203,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -1954,13 +2230,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test state get/update methods with interrupt_after app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} - assert app_w_interrupt.get_graph().to_json() == snapshot - assert app_w_interrupt.get_graph().draw_mermaid() == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert app_w_interrupt.get_graph().to_json() == snapshot + assert app_w_interrupt.get_graph().draw_mermaid() == snapshot assert [ c for c in app_w_interrupt.stream({"input": "what is weather in sf"}, config) @@ -1989,16 +2266,19 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], config=app_w_interrupt.checkpointer.get_tuple(config).config, metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } }, }, }, @@ -2039,6 +2319,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2056,18 +2337,28 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + } + }, { "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2075,14 +2366,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2098,14 +2389,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2119,14 +2410,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2139,20 +2430,21 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2167,7 +2459,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test state get/update methods with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -2200,16 +2492,19 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, @@ -2244,6 +2539,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2261,18 +2557,28 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + }, + }, { "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2280,14 +2586,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2303,14 +2609,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2324,14 +2630,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2344,20 +2650,21 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2372,10 +2679,10 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test re-invoke to continue with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) - config = {"configurable": {"thread_id": "2"}} + config = {"configurable": {"thread_id": "3"}} llm.i = 0 # reset the llm assert [ @@ -2405,16 +2712,19 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, @@ -2422,18 +2732,26 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + }, + }, { "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2441,14 +2759,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2461,25 +2779,45 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: assert [c for c in app_w_interrupt.stream(None, config)] == [ { - "tools": { + "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2487,22 +2825,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2541,10 +2879,11 @@ def test_conditional_entrypoint_graph(snapshot: SnapshotAssertion) -> None: app = workflow.compile() - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert ( app.invoke("what is weather in sf", debug=True) @@ -2582,10 +2921,11 @@ def test_conditional_entrypoint_to_multiple_state_graph( app = workflow.compile() - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"locations": ["sf", "nyc"]}, debug=True) == { "locations": ["sf", "nyc"], @@ -2598,14 +2938,20 @@ def test_conditional_entrypoint_to_multiple_state_graph( } +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_conditional_state_graph( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: - from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) setup = mocker.Mock() teardown = mocker.Mock() @@ -2688,7 +3034,7 @@ def test_conditional_state_graph( observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) - return {"intermediate_steps": [(agent_action, observation)]} + return {"intermediate_steps": [[agent_action, observation]]} # Define decision-making logic def should_continue(data: AgentState) -> str: @@ -2716,31 +3062,32 @@ def test_conditional_state_graph( app = workflow.compile() - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot with assert_ctx_once(): assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2761,14 +3108,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2784,14 +3131,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2807,7 +3154,7 @@ def test_conditional_state_graph( # test state get/update methods with interrupt_after app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -2840,6 +3187,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -2881,6 +3229,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -2901,14 +3250,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2941,14 +3290,14 @@ def test_conditional_state_graph( log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -2956,6 +3305,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -2973,7 +3323,7 @@ def test_conditional_state_graph( # test state get/update methods with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], debug=True, ) @@ -3004,6 +3354,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3044,6 +3395,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -3063,14 +3415,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3102,14 +3454,14 @@ def test_conditional_state_graph( log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -3117,6 +3469,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -3133,7 +3486,7 @@ def test_conditional_state_graph( # test w interrupt before all app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before="*", debug=True, ) @@ -3152,7 +3505,7 @@ def test_conditional_state_graph( next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -3178,6 +3531,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3197,14 +3551,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3216,14 +3570,14 @@ def test_conditional_state_graph( tool="search_api", tool_input="query", log="tool:search_api:query" ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], }, tasks=(PregelTask(AnyStr(), "agent"),), @@ -3231,19 +3585,20 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3265,7 +3620,7 @@ def test_conditional_state_graph( # test w interrupt after all app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after="*", ) config = {"configurable": {"thread_id": "4"}} @@ -3295,6 +3650,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3314,14 +3670,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3333,14 +3689,14 @@ def test_conditional_state_graph( tool="search_api", tool_input="query", log="tool:search_api:query" ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], }, tasks=(PregelTask(AnyStr(), "agent"),), @@ -3348,19 +3704,20 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3381,8 +3738,25 @@ def test_conditional_state_graph( ] +def test_conditional_state_graph_with_list_edge_inputs(snapshot: SnapshotAssertion): + class State(TypedDict): + foo: Annotated[list[str], operator.add] + + graph_builder = StateGraph(State) + graph_builder.add_node("A", lambda x: {"foo": ["A"]}) + graph_builder.add_node("B", lambda x: {"foo": ["B"]}) + graph_builder.add_edge(START, "A") + graph_builder.add_edge(START, "B") + graph_builder.add_edge(["A", "B"], END) + + app = graph_builder.compile() + assert app.invoke({"foo": []}) == {"foo": ["A", "B"]} + + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + + def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) -> None: - from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate from langchain_core.tools import tool @@ -3472,9 +3846,10 @@ def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) app = builder.compile() - assert app.config_schema().schema_json() == snapshot - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.config_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot assert builder.channels.keys() == {"input", "agent_outcome", "intermediate_steps"} @@ -3537,10 +3912,11 @@ def test_conditional_entrypoint_graph_state(snapshot: SnapshotAssertion) -> None app = workflow.compile() - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", @@ -3557,7 +3933,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + from langchain_core.messages import AIMessage, HumanMessage from langchain_core.tools import tool class FakeFuntionChatModel(FakeMessagesListChatModel): @@ -3604,18 +3980,18 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: app = create_tool_calling_executor(model, tools) - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke( {"messages": [HumanMessage(content="what is weather in sf")]} ) == { "messages": [ _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3625,14 +4001,12 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3647,13 +4021,12 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call234", - id=AnyStr(), ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", tool_call_id="tool_call567", @@ -3683,7 +4056,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "agent": { "messages": [ - AIMessage( + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3692,7 +4065,6 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: "args": {"query": "query"}, }, ], - id=AnyStr(), ) ] } @@ -3700,11 +4072,10 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] } @@ -3712,7 +4083,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "agent": { "messages": [ - AIMessage( + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3726,7 +4097,6 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: "args": {"query": "a third one"}, }, ], - id=AnyStr(), ) ] } @@ -3734,17 +4104,15 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call234", - id=AnyStr(), ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", tool_call_id="tool_call567", - id=AnyStr(), ), ] } @@ -3758,8 +4126,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "agent": { "messages": [ - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3775,11 +4142,10 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] } @@ -3787,8 +4153,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "agent": { "messages": [ - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3809,17 +4174,15 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call234", - id=AnyStr(), ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", tool_call_id="tool_call567", - id=AnyStr(), ), ] } @@ -3828,8 +4191,10 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.parametrize("serde", [NoopSerializer(), JsonPlusSerializer()]) -def test_state_graph_packets(serde: SerializerProtocol) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_state_graph_packets( + request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture +) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) @@ -3842,8 +4207,13 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ) from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] + session: Annotated[httpx.Client, Context(httpx.Client)] @tool() def search_api(query: str) -> str: @@ -3887,6 +4257,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ) def agent(data: AgentState) -> AgentState: + assert isinstance(data["session"], httpx.Client) return { "messages": model.invoke(data["messages"]), "something_extra": "hi there", @@ -3894,16 +4265,26 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: # Define decision-making logic def should_continue(data: AgentState) -> str: + assert isinstance(data["session"], httpx.Client) assert ( data["something_extra"] == "hi there" ), "nodes can pass extra data to their cond edges, which isn't saved in state" # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: - return [Send("tools", tool_call) for tool_call in tool_calls] + return [ + Send("tools", {"call": tool_call, "my_session": data["session"]}) + for tool_call in tool_calls + ] else: return END - def tools_node(tool_call: ToolCall, config: RunnableConfig) -> AgentState: + class ToolInput(TypedDict): + call: ToolCall + my_session: httpx.Client + + def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + assert isinstance(input["my_session"], httpx.Client) + tool_call = input["call"] time.sleep(tool_call["args"].get("idx", 0) / 10) output = tools_by_name[tool_call["name"]].invoke(tool_call["args"], config) return { @@ -3949,10 +4330,9 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ), AIMessage( @@ -3971,16 +4351,14 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", - id=AnyStr(), tool_call_id="tool_call234", ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", - id=AnyStr(), tool_call_id="tool_call567", ), AIMessage(content="answer", id="ai3"), @@ -4010,10 +4388,9 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ) } @@ -4040,20 +4417,18 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for another", name="search_api", - id=AnyStr(), tool_call_id="tool_call234", ) }, }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for a third one", name="search_api", - id=AnyStr(), tool_call_id="tool_call567", ), }, @@ -4062,7 +4437,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(serde=serde), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4112,6 +4487,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: config=(app_w_interrupt.checkpointer.get_tuple(config)).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4163,6 +4539,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4188,10 +4565,9 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: assert [c for c in app_w_interrupt.stream(None, config)] == [ { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for a different query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ) } @@ -4233,10 +4609,9 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ), AIMessage( @@ -4262,6 +4637,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4312,10 +4688,9 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ), AIMessage(content="answer", id="ai2"), @@ -4326,6 +4701,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -4339,9 +4715,12 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_message_graph( snapshot: SnapshotAssertion, deterministic_uuids: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from copy import deepcopy @@ -4353,11 +4732,14 @@ def test_message_graph( AIMessage, BaseMessage, HumanMessage, - ToolMessage, ) from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -4463,15 +4845,15 @@ def test_message_graph( # meaning you can use it as you would any other runnable app = workflow.compile() - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert json.dumps(app.get_input_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_output_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke(HumanMessage(content="what is weather in sf")) == [ - HumanMessage( + _AnyIdHumanMessage( content="what is weather in sf", - id="00000000-0000-4000-8000-000000000002", # adds missing ids ), AIMessage( content="", @@ -4484,11 +4866,10 @@ def test_message_graph( ], id="ai1", # respects ids passed in ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000011", ), AIMessage( content="", @@ -4501,11 +4882,10 @@ def test_message_graph( ], id="ai2", ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000020", ), AIMessage(content="answer", id="ai3"), ] @@ -4526,11 +4906,10 @@ def test_message_graph( }, { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000036", ) ] }, @@ -4549,11 +4928,10 @@ def test_message_graph( }, { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000045", ) ] }, @@ -4561,7 +4939,7 @@ def test_message_graph( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4604,6 +4982,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4649,6 +5028,7 @@ def test_message_graph( config=next_config, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4671,11 +5051,10 @@ def test_message_graph( assert [c for c in app_w_interrupt.stream(None, config)] == [ { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] }, @@ -4708,11 +5087,10 @@ def test_message_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), AIMessage( content="", @@ -4731,6 +5109,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4770,11 +5149,10 @@ def test_message_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), AIMessage(content="answer", id="ai2"), ], @@ -4783,6 +5161,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -4791,7 +5170,7 @@ def test_message_graph( ) app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -4833,6 +5212,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4878,6 +5258,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4900,11 +5281,10 @@ def test_message_graph( assert [c for c in app_w_interrupt.stream(None, config)] == [ { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] }, @@ -4937,11 +5317,10 @@ def test_message_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), AIMessage( content="", @@ -4960,6 +5339,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4999,7 +5379,7 @@ def test_message_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5012,6 +5392,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5038,7 +5419,7 @@ def test_message_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5052,17 +5433,20 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_root_graph( - snapshot: SnapshotAssertion, deterministic_uuids: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from copy import deepcopy @@ -5079,6 +5463,10 @@ def test_root_graph( from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -5188,9 +5576,8 @@ def test_root_graph( app = workflow.compile() assert app.invoke(HumanMessage(content="what is weather in sf")) == [ - HumanMessage( + _AnyIdHumanMessage( content="what is weather in sf", - id="00000000-0000-4000-8000-000000000002", # adds missing ids ), AIMessage( content="", @@ -5203,11 +5590,10 @@ def test_root_graph( ], id="ai1", # respects ids passed in ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000011", ), AIMessage( content="", @@ -5220,11 +5606,10 @@ def test_root_graph( ], id="ai2", ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000020", ), AIMessage(content="answer", id="ai3"), ] @@ -5249,7 +5634,7 @@ def test_root_graph( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000036", + id="00000000-0000-4000-8000-000000000033", ) ] }, @@ -5272,7 +5657,7 @@ def test_root_graph( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000045", + id="00000000-0000-4000-8000-000000000041", ) ] }, @@ -5280,7 +5665,7 @@ def test_root_graph( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -5323,6 +5708,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -5368,6 +5754,7 @@ def test_root_graph( config=next_config, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -5390,11 +5777,10 @@ def test_root_graph( assert [c for c in app_w_interrupt.stream(None, config)] == [ { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] }, @@ -5427,7 +5813,7 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5450,6 +5836,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5489,7 +5876,7 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5502,6 +5889,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5510,7 +5898,7 @@ def test_root_graph( ) app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -5552,6 +5940,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -5597,6 +5986,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -5619,11 +6009,10 @@ def test_root_graph( assert [c for c in app_w_interrupt.stream(None, config)] == [ { "tools": [ - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] }, @@ -5656,7 +6045,7 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5679,6 +6068,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5718,11 +6108,10 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), AIMessage(content="answer", id="ai2"), ], @@ -5731,6 +6120,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5757,7 +6147,7 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", @@ -5771,9 +6161,10 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -5809,7 +6200,7 @@ def test_root_graph( }, ) new_workflow.add_edge("tools", "agent") - new_app = new_workflow.compile(checkpointer=app_w_interrupt.checkpointer) + new_app = new_workflow.compile(checkpointer=checkpointer) model.i = 0 # reset the llm # previous state is converted to new schema @@ -5828,11 +6219,10 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), AIMessage(content="answer", id="ai2"), _AnyIdAIMessage(content="an extra message"), @@ -5843,9 +6233,10 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -5862,7 +6253,7 @@ def test_root_graph( "__root__": [ HumanMessage( content="what is weather in sf", - id="00000000-0000-4000-8000-000000000077", + id="00000000-0000-4000-8000-000000000070", ), AIMessage( content="", @@ -5875,15 +6266,14 @@ def test_root_graph( } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for a different query", name="search_api", - id="00000000-0000-4000-8000-000000000091", tool_call_id="tool_call123", ), AIMessage(content="answer", id="ai2"), AIMessage( - content="an extra message", id="00000000-0000-4000-8000-000000000101" + content="an extra message", id="00000000-0000-4000-8000-000000000091" ), HumanMessage(content="what is weather in la"), ], @@ -5974,13 +6364,9 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", - "input": { - "query": "what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], }, }, @@ -5993,9 +6379,11 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", "result": [("query", "query: what is weather in sf")], + "error": None, + "interrupts": [], }, }, ), @@ -6007,13 +6395,9 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -6025,13 +6409,9 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -6047,9 +6427,11 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", "result": [("docs", ["doc3", "doc4"])], + "error": None, + "interrupts": [], }, }, ), @@ -6064,9 +6446,11 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", "result": [("docs", ["doc1", "doc2"])], + "error": None, + "interrupts": [], }, }, ), @@ -6084,11 +6468,10 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "input": { "query": "query: what is weather in sf", - "answer": None, "docs": ["doc1", "doc2", "doc3", "doc4"], }, "triggers": ["retriever_one", "retriever_two"], @@ -6103,9 +6486,11 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "result": [("answer", "doc1,doc2,doc3,doc4")], + "error": None, + "interrupts": [], }, }, ), @@ -6120,14 +6505,127 @@ def test_in_one_fan_out_out_one_graph_state() -> None: ] -def test_start_branch_then(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_dynamic_interrupt( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str + tool_two_node_count = 0 + + def tool_two_node(s: State) -> State: + nonlocal tool_two_node_count + tool_two_node_count += 1 + if s["market"] == "DE": + raise NodeInterrupt("Just because...") + return {"my_key": " all good"} + tool_two_graph = StateGraph(State) - tool_two_graph.add_node("tool_two_slow", lambda s: {"my_key": " slow"}) - tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"}) + tool_two_graph.add_node("tool_two", tool_two_node, retry=RetryPolicy()) + tool_two_graph.add_edge(START, "tool_two") + tool_two = tool_two_graph.compile() + + tracer = FakeTracer() + assert tool_two.invoke( + {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]} + ) == { + "my_key": "value", + "market": "DE", + } + assert tool_two_node_count == 1, "interrupts aren't retried" + assert len(tracer.runs) == 1 + run = tracer.runs[0] + assert run.end_time is not None + assert run.error is None + assert run.outputs == {"market": "DE", "my_key": "value"} + + assert tool_two.invoke({"my_key": "value", "market": "US"}) == { + "my_key": "value all good", + "market": "US", + } + + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, + }, + ] + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("Just because..."),), + ), + ), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_start_branch_then( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + + class State(TypedDict): + my_key: Annotated[str, operator.add] + market: str + shared: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] + + def assert_shared_value(data: State, config: RunnableConfig) -> State: + assert "shared" in data + if thread_id := config["configurable"].get("thread_id"): + if thread_id == "1": + # this is the first thread, so should not see a value + assert data["shared"] == {} + return {"shared": {"1": {"hello": "world"}}} + elif thread_id == "2": + # this should get value saved by thread 1 + assert data["shared"] == {"1": {"hello": "world"}} + elif thread_id == "3": + # this is a different assistant, so should not see previous value + assert data["shared"] == {} + return {} + + def tool_two_slow(data: State, config: RunnableConfig) -> State: + return {"my_key": " slow", **assert_shared_value(data, config)} + + def tool_two_fast(data: State, config: RunnableConfig) -> State: + return {"my_key": " fast", **assert_shared_value(data, config)} + + tool_two_graph = StateGraph(State) + tool_two_graph.add_node("tool_two_slow", tool_two_slow) + tool_two_graph.add_node("tool_two_fast", tool_two_fast) tool_two_graph.set_conditional_entry_point( lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", then=END ) @@ -6143,146 +6641,158 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: "market": "US", } - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] - ) + tool_two = tool_two_graph.compile( + store=MemoryStore(), + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], + ) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { - "my_key": "value ⛰️", - "market": "DE", - } - assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ - { - "source": "loop", - "step": 0, - "writes": None, - }, - { - "source": "input", - "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, - }, - ] - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value ⛰️ slow", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️ slow", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, + }, + ] + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value ⛰️ slow", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️ slow", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value fast", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value fast", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value fast", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value fast", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) - thread3 = {"configurable": {"thread_id": "3"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread3) == { - "my_key": "value", - "market": "US", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) - # update state - tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "update", - "step": 1, - "writes": {START: {"my_key": "key"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread3, debug=1) == { - "my_key": "valuekey fast", - "market": "US", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "valuekey fast", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 2, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread3) == { + "my_key": "value", + "market": "US", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) + # update state + tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "valuekey", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": {START: {"my_key": "key"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread3, debug=1) == { + "my_key": "valuekey fast", + "market": "US", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "valuekey fast", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) -def test_branch_then(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_branch_then( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -6312,496 +6822,524 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "market": "US", } - with SqliteSaver.from_conn_string(":memory:") as saver: - # test stream_mode=debug - tool_two = tool_two_graph.compile(checkpointer=saver) - thread10 = {"configurable": {"thread_id": "10"}} - assert [ - *tool_two.stream( - {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" - ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": {"my_key": ""}, - "metadata": { - "source": "input", - "step": -1, - "writes": {"my_key": "value", "market": "DE"}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__"}], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare"}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "result": [("my_key", " prepared")], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [{"id": AnyStr(), "name": "tool_two_slow"}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "input": {"my_key": "value prepared", "market": "DE"}, - "triggers": ["branch:prepare:condition:tool_two_slow"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "result": [("my_key", " slow")], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - "next": ["finish"], - "tasks": [{"id": AnyStr(), "name": "finish"}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "input": {"my_key": "value prepared slow", "market": "DE"}, - "triggers": ["branch:prepare:condition::then"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "result": [("my_key", " finished")], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow finished", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - "next": [], - "tasks": [], - }, - }, - ] - - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + thread10 = {"configurable": {"thread_id": "10"}} + assert [ + *tool_two.stream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": AnyStr(), + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": AnyStr(), + "name": "tool_two_slow", + "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + "next": ["finish"], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": AnyStr(), + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition::then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": AnyStr(), + "name": "finish", + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + "next": [], + "tasks": [], + }, + }, + ] - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["finish"] - ) + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_before=["tool_two_fast", "tool_two_slow"] + ) - thread1 = {"configurable": {"thread_id": "1"}} + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + thread2 = {"configurable": {"thread_id": "2"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_before=["finish"] + ) + + thread1 = {"configurable": {"thread_id": "11"}} + + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared slow", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={ "my_key": "value prepared slow", "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={ - "my_key": "value prepared slow", - "market": "DE", - }, - tasks=(PregelTask(AnyStr(), "finish"),), - next=("finish",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + }, + tasks=(PregelTask(AnyStr(), "finish"),), + next=("finish",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - # update state - tool_two.update_state(thread1, {"my_key": "er"}) - assert tool_two.get_state(thread1) == StateSnapshot( - values={ - "my_key": "value prepared slower", - "market": "DE", - }, - tasks=(PregelTask(AnyStr(), "finish"),), - next=("finish",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "update", - "step": 3, - "writes": {"tool_two_slow": {"my_key": "er"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_after=["prepare"] - ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", + # update state + tool_two.update_state(thread1, {"my_key": "er"}) + assert tool_two.get_state(thread1) == StateSnapshot( + values={ + "my_key": "value prepared slower", "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + }, + tasks=(PregelTask(AnyStr(), "finish"),), + next=("finish",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 3, + "writes": {"tool_two_slow": {"my_key": "er"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_after=["prepare"] + ) - thread3 = {"configurable": {"thread_id": "3"}} - # update an empty thread before first run - uconfig = tool_two.update_state(thread3, {"my_key": "key", "market": "DE"}) - # check current state - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "prepare"),), - next=("prepare",), - config=uconfig, - created_at=AnyStr(), - metadata={ - "source": "update", - "step": 0, - "writes": {START: {"my_key": "key", "market": "DE"}}, - }, - parent_config=None, - ) - # run from this point - assert tool_two.invoke(None, thread3) == { - "my_key": "key prepared", - "market": "DE", - } - # get state after first node - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=uconfig, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread3, debug=1) == { - "my_key": "key prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "21"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + thread2 = {"configurable": {"thread_id": "22"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + + thread3 = {"configurable": {"thread_id": "23"}} + # update an empty thread before first run + uconfig = tool_two.update_state(thread3, {"my_key": "key", "market": "DE"}) + # check current state + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "prepare"),), + next=("prepare",), + config=uconfig, + created_at=AnyStr(), + metadata={ + "parents": {}, + "source": "update", + "step": 0, + "writes": {START: {"my_key": "key", "market": "DE"}}, + }, + parent_config=None, + ) + # run from this point + assert tool_two.invoke(None, thread3) == { + "my_key": "key prepared", + "market": "DE", + } + # get state after first node + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=uconfig, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread3, debug=1) == { + "my_key": "key prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) -def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_in_one_fan_out_state_graph_waiting_edge( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -6866,7 +7404,7 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -6885,10 +7423,10 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["qa"], ) - config = {"configurable": {"thread_id": "1"}} + config = {"configurable": {"thread_id": "2"}} assert [ c for c in app_w_interrupt.stream({"query": "what is weather in sf"}, config) @@ -6910,6 +7448,7 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": {"retriever_one": {"docs": ["doc5"]}}, @@ -6922,9 +7461,14 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_via_branch( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -6992,7 +7536,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7011,11 +7555,16 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: - from langchain_core.pydantic_v1 import BaseModel, ValidationError + from pydantic.v1 import BaseModel, ValidationError + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") setup = mocker.Mock() teardown = mocker.Mock() @@ -7092,7 +7641,6 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( return {"answer": ",".join(data.docs)} def decider(data: State) -> str: - print("decider", data) assert isinstance(data, State) return "retriever_two" @@ -7115,6 +7663,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( app = workflow.compile() + # because it's a v1 pydantic, we're using .schema() here instead of the new methods assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.get_input_schema().schema() == snapshot assert app.get_output_schema().schema() == snapshot @@ -7140,7 +7689,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7175,11 +7724,16 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( } +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from pydantic import BaseModel, ConfigDict, ValidationError + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") setup = mocker.Mock() teardown = mocker.Mock() @@ -7277,9 +7831,10 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( app = workflow.compile() - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot - assert app.get_input_schema().schema() == snapshot - assert app.get_output_schema().schema() == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + assert app.get_input_schema().model_json_schema() == snapshot + assert app.get_output_schema().model_json_schema() == snapshot with pytest.raises(ValidationError), assert_ctx_once(): app.invoke({"query": {}}) @@ -7302,7 +7857,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7337,7 +7892,14 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( } -def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_in_one_fan_out_state_graph_waiting_edge_plus_regular( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -7406,7 +7968,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7801,1235 +8363,7 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) -def test_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - - class InnerState(TypedDict): - my_key: str - my_other_key: str - - def inner_1(state: InnerState): - return { - "my_key": state["my_key"] + " here", - "my_other_key": state["my_key"], - } - - def inner_2(state: InnerState): - return { - "my_key": state["my_key"] + " and there", - "my_other_key": state["my_key"], - } - - inner = StateGraph(InnerState) - inner.add_node("inner_1", inner_1) - inner.add_node("inner_2", inner_2) - inner.add_edge("inner_1", "inner_2") - inner.set_entry_point("inner_1") - inner.set_finish_point("inner_2") - - class State(TypedDict): - my_key: str - - def outer_1(state: State): - return {"my_key": "hi " + state["my_key"]} - - def outer_2(state: State): - return {"my_key": state["my_key"] + " and back again"} - - graph = StateGraph(State) - graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) - graph.add_node("outer_2", outer_2) - graph.set_entry_point("outer_1") - graph.add_edge("outer_1", "inner") - graph.add_edge("inner", "outer_2") - graph.set_finish_point("outer_2") - - app = graph.compile(checkpointer=checkpointer) - - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert app.invoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert app.invoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [*app.stream({"my_key": "my value"}, config)] == [ - {"outer_1": {"my_key": "hi my value"}}, - ] - assert [*app.stream(None, config)] == [ - {"inner": {"my_key": "hi my value here and there"}}, - {"outer_2": {"my_key": "hi my value here and there and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - - # test interrupts BEFORE the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_before=["inner"]) - config = {"configurable": {"thread_id": "4"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # while we're waiting for the node w/ interrupt inside to finish - assert [*app.stream(None, config, stream_mode="values")] == [] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test interrupts AFTER the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_after=["inner"]) - config = {"configurable": {"thread_id": "5"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - # interrupted after "inner" - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there and back again", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test restarting from checkpoint_id - config = {"configurable": {"thread_id": "6"}} - app = graph.compile(checkpointer=checkpointer) - assert app.invoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value" - } - state_history = [c for c in app.get_state_history(config)] - assert state_history == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - child_state_history = [ - c - for c in app.get_state_history( - {"configurable": {"thread_id": "6", "checkpoint_ns": "inner"}} - ) - ] - assert child_state_history == [ - StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - # there should be a single child checkpoint because we only keep - # one child checkpoint per parent checkpoint (in which child ran) - ] - - # check that child snapshot matches id of parent - child_snapshot = child_state_history[0] - assert ( - child_snapshot.config["configurable"]["checkpoint_id"] - == state_history[0].config["configurable"]["checkpoint_id"] - ) - # check resuming from interrupt w/ checkpoint_id - interrupt_state_snapshot, before_interrupt_state_snapshot = state_history[:2] - before_interrupt_config = before_interrupt_state_snapshot.config - # going to get to interrupt again here, so the output is None - assert app.invoke(None, before_interrupt_config, debug=True) == { - "my_key": "hi my value" - } - # one more "identical" snapshot than before, at top of list - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # going to restart from interrupt - interrupt_config = interrupt_state_snapshot.config - assert app.invoke(None, interrupt_config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -9092,11 +8426,13 @@ def test_nested_graph_interrupts_parallel( # - the writes of outer are persisted in 1st call and used in 2nd call, ie outer isn't called again (because we dont see outer_1 output again in 2nd stream) # test stream updates w/ nested interrupt config = {"configurable": {"thread_id": "2"}} - assert [*app.stream({"my_key": ""}, config)] == [ + assert [*app.stream({"my_key": ""}, config, subgraphs=True)] == [ # we got to parallel node first - {"outer_1": {"my_key": " and parallel"}}, + ((), {"outer_1": {"my_key": " and parallel"}}), + ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), ] assert [*app.stream(None, config)] == [ + {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, ] @@ -9104,17 +8440,12 @@ def test_nested_graph_interrupts_parallel( # test stream values w/ nested interrupt config = {"configurable": {"thread_id": "3"}} assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [ - { - "my_key": "", - }, + {"my_key": ""}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # test interrupts BEFORE the parallel node @@ -9124,14 +8455,13 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""} ] # while we're waiting for the node w/ interrupt inside to finish - assert [*app.stream(None, config, stream_mode="values")] == [] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": ""}, + ] + assert [*app.stream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # test interrupts AFTER the parallel node @@ -9141,24 +8471,20 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""} ] assert [*app.stream(None, config, stream_mode="values")] == [ + {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] -@pytest.mark.skip -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - checkpointer = request.getfixturevalue(checkpointer_name) + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) class State(TypedDict): my_key: str @@ -9185,7 +8511,10 @@ def test_doubly_nested_graph_interrupts( grandchild.set_finish_point("grandchild_2") child = StateGraph(ChildState) - child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.add_node( + "child_1", + grandchild.compile(interrupt_before=["grandchild_2"]), + ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -9229,20 +8558,1956 @@ def test_doubly_nested_graph_interrupts( # test stream values w/ nested interrupt config = {"configurable": {"thread_id": "3"}} assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, + {"my_key": "my value"}, + {"my_key": "hi my value"}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", + {"my_key": "hi my value"}, + {"my_key": "hi my value here and there"}, + {"my_key": "hi my value here and there and back again"}, + ] + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class InnerState(TypedDict): + my_key: str + my_other_key: str + + def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + inner = StateGraph(InnerState) + inner.add_node("inner_1", inner_1) + inner.add_node("inner_2", inner_2) + inner.add_edge("inner_1", "inner_2") + inner.set_entry_point("inner_1") + inner.set_finish_point("inner_2") + + class State(TypedDict): + my_key: str + other_parent_key: str + + def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"]), + ) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + app.invoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + # first get_state without subgraph state + assert app.get_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={"configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()}}, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } }, - { - "my_key": "hi my value here and there and back again", + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # now, get_state with subgraphs state + assert app.get_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": { + "": AnyStr(), + }, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # get_state_history returns outer graph checkpoints + history = list(app.get_state_history(config)) + assert history == [ + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + # get_state_history for a subgraph returns its checkpoints + child_history = [*app.get_state_history(history[0].tasks[0].state)] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # resume + app.invoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert app.get_state(config) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # test full history at the end + actual_history = list(app.get_state_history(config)) + expected_history = [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(PregelTask(AnyStr(), "outer_2"),), + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()} + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + # test looking up parent state by checkpoint ID + for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): + assert app.get_state(actual_snapshot.config) == expected_snapshot + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_doubly_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node( + "child_1", + grandchild.compile(interrupt_before=["grandchild_2"]), + ) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile()) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert [c for c in app.stream({"my_key": "my value"}, config, subgraphs=True)] == [ + ((), {"parent_1": {"my_key": "hi my value"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ), + ] + # get state without subgraphs + outer_state = app.get_state(config) + assert outer_state == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + child_state = app.get_state(outer_state.tasks[0].state) + assert ( + child_state.tasks[0] + == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ).tasks[0] + ) + grandchild_state = app.get_state(child_state.tasks[0].state) + assert grandchild_state == StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ) + # get state with subgraphs + assert app.get_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state=StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state=StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr( + re.compile(r"child:.+|child1:") + ): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": { + "grandchild_1": {"my_key": "hi my value here"} + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # resume + assert [c for c in app.stream(None, config, subgraphs=True)] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_2": {"my_key": "hi my value here and there"}}, + ), + ((AnyStr("child:"),), {"child_1": {"my_key": "hi my value here and there"}}), + ((), {"child": {"my_key": "hi my value here and there"}}), + ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), + ] + # get state with and without subgraphs + assert ( + app.get_state(config) + == app.get_state(config, subgraphs=True) + == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + ) + # get outer graph history + outer_history = list(app.get_state_history(config)) + assert outer_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0, "parents": {}}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get child graph history + child_history = list(app.get_state_history(outer_history[2].tasks[0].state)) + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + } + }, + ), + ), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get grandchild graph history + grandchild_history = list(app.get_state_history(child_history[1].tasks[0].state)) + assert grandchild_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, + "step": 2, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value here"}, + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("grandchild_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # replay grandchild checkpoint + assert [ + c for c in app.stream(None, grandchild_history[2].config, subgraphs=True) + ] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ) + ] + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_send_to_nested_graphs( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class OverallState(TypedDict): + subjects: list[str] + jokes: Annotated[list[str], operator.add] + + def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + + class JokeState(TypedDict): + subject: str + + def edit(state: JokeState): + subject = state["subject"] + return {"subject": f"{subject} - hohoho"} + + # subgraph + subgraph = StateGraph(input=JokeState, output=OverallState) + subgraph.add_node("edit", edit) + subgraph.add_node( + "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} + ) + subgraph.set_entry_point("edit") + subgraph.add_edge("edit", "generate") + subgraph.set_finish_point("generate") + + # parent graph + builder = StateGraph(OverallState) + builder.add_node( + "generate_joke", + subgraph.compile(interrupt_before=["generate"]), + ) + builder.add_conditional_edges(START, continue_to_jokes) + builder.add_edge("generate_joke", END) + + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + tracer = FakeTracer() + + # invoke and pause at nested interrupt + assert graph.invoke( + {"subjects": ["cats", "dogs"]}, config={**config, "callbacks": [tracer]} + ) == { + "subjects": ["cats", "dogs"], + "jokes": [], + } + assert len(tracer.runs) == 1, "Should produce exactly 1 root run" + + # check state + outer_state = graph.get_state(config) + assert outer_state == StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + ), + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # check state of each of the inner tasks + assert graph.get_state(outer_state.tasks[0].state) == StateSnapshot( + values={"subject": "cats - hohoho", "jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("generate_joke:"): AnyStr(), + } + ), + } + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"edit": None}, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(""), name="generate"),), + ) + assert graph.get_state(outer_state.tasks[1].state) == StateSnapshot( + values={"subject": "dogs - hohoho", "jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("generate_joke:"): AnyStr(), + } + ), + } + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"edit": None}, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(""), name="generate"),), + ) + # update state of dogs joke graph + graph.update_state(outer_state.tasks[1].state, {"subject": "turtles - hohoho"}) + + # continue past interrupt + assert sorted( + graph.stream(None, config=config), key=lambda d: d["generate_joke"]["jokes"][0] + ) == [ + {"generate_joke": {"jokes": ["Joke about cats - hohoho"]}}, + {"generate_joke": {"jokes": ["Joke about turtles - hohoho"]}}, + ] + + actual_snapshot = graph.get_state(config) + expected_snapshot = StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + assert actual_snapshot == expected_snapshot + + # test full history + actual_history = list(graph.get_state_history(config)) + + # get subgraph node state for expected history + expected_history = [ + StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + ), + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"jokes": []}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_weather_subgraph( + request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion +) -> None: + from langchain_core.language_models.fake_chat_models import ( + FakeMessagesListChatModel, + ) + from langchain_core.messages import AIMessage, ToolCall + from langchain_core.tools import tool + + from langgraph.graph import MessagesState + + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + # setup subgraph + + @tool + def get_weather(city: str): + """Get the weather for a specific city""" + return f"I'ts sunny in {city}!" + + weather_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="get_weather", + args={"city": "San Francisco"}, + ) + ], + ) + ] + ) + + class SubGraphState(MessagesState): + city: str + + def model_node(state: SubGraphState): + result = weather_model.invoke(state["messages"]) + return {"city": cast(AIMessage, result).tool_calls[0]["args"]["city"]} + + def weather_node(state: SubGraphState): + result = get_weather.invoke({"city": state["city"]}) + return {"messages": [{"role": "assistant", "content": result}]} + + subgraph = StateGraph(SubGraphState) + subgraph.add_node(model_node) + subgraph.add_node(weather_node) + subgraph.add_edge(START, "model_node") + subgraph.add_edge("model_node", "weather_node") + subgraph.add_edge("weather_node", END) + subgraph = subgraph.compile(interrupt_before=["weather_node"]) + + # setup main graph + + class RouterState(MessagesState): + route: Literal["weather", "other"] + + class Router(TypedDict): + route: Literal["weather", "other"] + + router_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="router", + args={"dest": "weather"}, + ) + ], + ) + ] + ) + + def router_node(state: RouterState): + system_message = "Classify the incoming query as either about weather or not." + messages = [{"role": "system", "content": system_message}] + state["messages"] + route = router_model.invoke(messages) + return {"route": cast(AIMessage, route).tool_calls[0]["args"]["dest"]} + + def normal_llm_node(state: RouterState): + return {"messages": [AIMessage("Hello!")]} + + def route_after_prediction(state: RouterState): + if state["route"] == "weather": + return "weather_graph" + else: + return "normal_llm_node" + + def weather_graph(state: RouterState): + return subgraph.invoke(state) + + graph = StateGraph(RouterState) + graph.add_node(router_node) + graph.add_node(normal_llm_node) + graph.add_node("weather_graph", weather_graph) + graph.add_edge(START, "router_node") + graph.add_conditional_edges("router_node", route_after_prediction) + graph.add_edge("normal_llm_node", END) + graph.add_edge("weather_graph", END) + graph = graph.compile(checkpointer=checkpointer) + + assert graph.get_graph(xray=1).draw_mermaid() == snapshot + + config = {"configurable": {"thread_id": "1"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + + # run until interrupt + assert [ + c + for c in graph.stream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + + # check current state + state = graph.get_state(config) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("weather_graph:"), + } + }, + ), + ), + ) + + # update + graph.update_state(state.tasks[0].state, {"city": "la"}) + + # run after update + assert [ + c + for c in graph.stream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (AnyStr("weather_graph:"),), + { + "weather_node": { + "messages": [{"role": "assistant", "content": "I'ts sunny in la!"}] + } + }, + ), + ( + (), + { + "weather_graph": { + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="I'ts sunny in la!"), + ] + } + }, + ), + ] + + # try updating acting as weather node + config = {"configurable": {"thread_id": "14"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + assert [ + c + for c in graph.stream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + state = graph.get_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf") + ], + "city": "San Francisco", + }, + next=("weather_node",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"model_node": {"city": "San Francisco"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="weather_node"),), + ), + ), + ), + ) + graph.update_state( + state.tasks[0].state.config, + {"messages": [{"role": "assistant", "content": "rainy"}]}, + as_node="weather_node", + ) + state = graph.get_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="rainy"), + ], + "city": "San Francisco", + }, + next=(), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "update", + "step": 2, + "writes": { + "weather_node": { + "messages": [{"role": "assistant", "content": "rainy"}] + } + }, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + ), + ), + ) + assert [ + c + for c in graph.stream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (), + { + "weather_graph": { + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="rainy"), + ] + } + }, + ), ] @@ -9298,7 +10563,7 @@ def test_checkpoint_metadata() -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) - from langchain_core.messages import AIMessage, AnyMessage, ToolMessage + from langchain_core.messages import AIMessage, AnyMessage from langchain_core.prompts import ChatPromptTemplate from langchain_core.tools import tool @@ -9385,9 +10650,8 @@ def test_checkpoint_metadata() -> None: ) == { "messages": [ _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( + _AnyIdAIMessage( content="", - id=AnyStr(), tool_calls=[ { "name": "search_api", @@ -9397,10 +10661,9 @@ def test_checkpoint_metadata() -> None: } ], ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ), _AnyIdAIMessage(content="answer"), @@ -9472,10 +10735,7 @@ def test_checkpoint_metadata() -> None: assert chkpnt_tuple.metadata["test_config_4"] == "bar" -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_remove_message_via_state_update( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -9534,7 +10794,7 @@ def test_remove_message_from_node(): def test_xray_lance(snapshot: SnapshotAssertion): from langchain_core.messages import AnyMessage, HumanMessage - from langchain_core.pydantic_v1 import BaseModel, Field + from pydantic import BaseModel, Field class Analyst(BaseModel): affiliation: str = Field( @@ -9659,10 +10919,7 @@ def test_xray_lance(snapshot: SnapshotAssertion): assert graph.get_graph(xray=1).to_json() == snapshot -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_channel_values(request: pytest.FixtureRequest, checkpointer_name: str) -> None: checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b50ecd36a..7697610cf 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1,6 +1,7 @@ import asyncio -import json import operator +import re +import sys from collections import Counter from contextlib import asynccontextmanager, contextmanager from typing import ( @@ -16,11 +17,15 @@ from typing import ( Tuple, TypedDict, Union, + cast, ) from uuid import UUID import httpx import pytest +from langchain_core.messages import ( + ToolCall, +) from langchain_core.runnables import ( RunnableConfig, RunnableLambda, @@ -39,33 +44,46 @@ from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.channels.untracked_value import UntrackedValue from langgraph.checkpoint.base import ( - BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointMetadata, CheckpointTuple, ) from langgraph.checkpoint.memory import MemorySaver -from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver -from langgraph.constants import ERROR, Send -from langgraph.errors import InvalidUpdateError +from langgraph.constants import ERROR, Interrupt, Send +from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages +from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) -from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.prebuilt.tool_node import ToolNode -from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot +from langgraph.pregel import ( + Channel, + GraphRecursionError, + Pregel, + StateSnapshot, +) from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask -from tests.any_str import AnyStr, ExceptionLike +from langgraph.store.memory import MemoryStore +from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_ASYNC_PLUS_NONE, + SHOULD_CHECK_SNAPSHOTS, + awith_checkpointer, +) +from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, - MemorySaverAssertImmutable, + MemorySaverNoPending, ) -from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage +from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage, _AnyIdToolMessage + +pytestmark = pytest.mark.anyio async def test_checkpoint_errors() -> None: @@ -204,6 +222,146 @@ async def test_node_cancellation_on_other_node_exception() -> None: assert inner_task_cancelled +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_dynamic_interrupt(checkpointer_name: str) -> None: + class State(TypedDict): + my_key: Annotated[str, operator.add] + market: str + + tool_two_node_count = 0 + + async def tool_two_node(s: State) -> State: + nonlocal tool_two_node_count + tool_two_node_count += 1 + if s["market"] == "DE": + raise NodeInterrupt("Just because...") + return {"my_key": " all good"} + + tool_two_graph = StateGraph(State) + tool_two_graph.add_node("tool_two", tool_two_node, retry=RetryPolicy()) + tool_two_graph.add_edge(START, "tool_two") + tool_two = tool_two_graph.compile() + + tracer = FakeTracer() + assert await tool_two.ainvoke( + {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]} + ) == { + "my_key": "value", + "market": "DE", + } + assert tool_two_node_count == 1, "interrupts aren't retried" + assert len(tracer.runs) == 1 + run = tracer.runs[0] + assert run.end_time is not None + assert run.error is None + assert run.outputs == {"market": "DE", "my_key": "value"} + + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}) == { + "my_key": "value all good", + "market": "US", + } + + async with awith_checkpointer(checkpointer_name) as checkpointer: + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert await tool_two.ainvoke( + {"my_key": "value ⛰️", "market": "DE"}, thread1 + ) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, + }, + ] + tup = await tool_two.checkpointer.aget_tuple(thread1) + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("Just because..."),), + ), + ), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + # TODO use aget_state_history + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_node_not_cancelled_on_other_node_interrupted( + checkpointer_name: str, +) -> None: + class State(TypedDict): + hello: str + + awhiles = 0 + inner_task_cancelled = False + + async def awhile(input: State) -> None: + nonlocal awhiles + + awhiles += 1 + try: + await asyncio.sleep(1) + return {"hello": "again"} + except asyncio.CancelledError: + nonlocal inner_task_cancelled + inner_task_cancelled = True + raise + + async def iambad(input: State) -> None: + if input["hello"] != "bye": + raise NodeInterrupt("I am bad") + + builder = StateGraph(State) + builder.add_node("agent", awhile) + builder.add_node("bad", iambad) + builder.set_conditional_entry_point(lambda _: ["agent", "bad"], then=END) + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + thread = {"configurable": {"thread_id": "1"}} + + assert await graph.ainvoke({"hello": "world"}, thread) == {"hello": "world"} + + assert not inner_task_cancelled + assert awhiles == 1 + + assert await graph.ainvoke(None, thread, debug=True) == {"hello": "world"} + + assert not inner_task_cancelled + assert awhiles == 1 + + assert await graph.ainvoke({"hello": "bye"}, thread) == {"hello": "again"} + + assert not inner_task_cancelled + assert awhiles == 2 + + async def test_step_timeout_on_stream_hang() -> None: inner_task_cancelled = False @@ -234,15 +392,8 @@ async def test_step_timeout_on_stream_hang() -> None: assert inner_task_cancelled -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_cancel_graph_astream( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC_PLUS_NONE) +async def test_cancel_graph_astream(checkpointer_name: str) -> None: class State(TypedDict): value: Annotated[int, operator.add] @@ -275,51 +426,47 @@ async def test_cancel_graph_astream( builder.add_edge(START, "alittlewhile") builder.add_edge(START, "aparallelwhile") builder.add_edge("alittlewhile", "awhile") - graph = builder.compile(checkpointer=checkpointer) - # test interrupting astream - got_event = False - thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} - async with aclosing(graph.astream({"value": 1}, thread1)) as stream: - async for chunk in stream: - assert chunk == {"alittlewhile": {"value": 2}} - got_event = True - break + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - assert got_event + # test interrupting astream + got_event = False + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} + async with aclosing(graph.astream({"value": 1}, thread1)) as stream: + async for chunk in stream: + assert chunk == {"alittlewhile": {"value": 2}} + got_event = True + break - # node aparallelwhile should start, but be cancelled - assert aparallelwhile.started is True - assert aparallelwhile.cancelled is True + assert got_event - # node "awhile" should never start - assert awhile.started is False + # node aparallelwhile should start, but be cancelled + assert aparallelwhile.started is True + assert aparallelwhile.cancelled is True - # checkpoint with output of "alittlewhile" should not be saved - if checkpointer is not None: - state = await graph.aget_state(thread1) - assert state is not None - assert state.values == {"value": 1} - assert state.next == ( - "aparallelwhile", - "alittlewhile", - ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} + # node "awhile" should never start + assert awhile.started is False + + # checkpoint with output of "alittlewhile" should not be saved + if checkpointer is not None: + state = await graph.aget_state(thread1) + assert state is not None + assert state.values == {"value": 1} + assert state.next == ( + "aparallelwhile", + "alittlewhile", + ) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe", None], -) -async def test_cancel_graph_astream_events_v2( - request: pytest.FixtureRequest, checkpointer_name: Optional[str] -) -> None: - checkpointer = ( - request.getfixturevalue(f"checkpointer_{checkpointer_name}") - if checkpointer_name - else None - ) - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC_PLUS_NONE) +async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) -> None: class State(TypedDict): value: int @@ -352,42 +499,46 @@ async def test_cancel_graph_astream_events_v2( builder.add_edge(START, "alittlewhile") builder.add_edge("alittlewhile", "awhile") builder.add_edge("awhile", "anotherwhile") - graph = builder.compile(checkpointer=checkpointer) - # test interrupting astream_events v2 - got_event = False - thread2: RunnableConfig = {"configurable": {"thread_id": "2"}} - async with aclosing( - graph.astream_events({"value": 1}, thread2, version="v2") - ) as stream: - async for chunk in stream: - if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: - got_event = True - assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} - break + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - # did break - assert got_event + # test interrupting astream_events v2 + got_event = False + thread2: RunnableConfig = {"configurable": {"thread_id": "2"}} + async with aclosing( + graph.astream_events({"value": 1}, thread2, version="v2") + ) as stream: + async for chunk in stream: + if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: + got_event = True + assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} + await asyncio.sleep(0.1) + break - # node "awhile" maybe starts (impl detail of astream_events) - # if it does start, it must be cancelled - if awhile.started: - assert awhile.cancelled is True + # did break + assert got_event - # node "anotherwhile" should never start - assert anotherwhile.started is False + # node "awhile" maybe starts (impl detail of astream_events) + # if it does start, it must be cancelled + if awhile.started: + assert awhile.cancelled is True - # checkpoint with output of "alittlewhile" should not be saved - if checkpointer is not None: - state = await graph.aget_state(thread2) - assert state is not None - assert state.values == {"value": 2} - assert state.next == ("awhile",) - assert state.metadata == { - "source": "loop", - "step": 1, - "writes": {"alittlewhile": {"value": 2}}, - } + # node "anotherwhile" should never start + assert anotherwhile.started is False + + # checkpoint with output of "alittlewhile" should not be saved + if checkpointer is not None: + state = await graph.aget_state(thread2) + assert state is not None + assert state.values == {"value": 2} + assert state.next == ("awhile",) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"alittlewhile": {"value": 2}}, + } async def test_node_schemas_custom_output() -> None: @@ -416,7 +567,6 @@ async def test_node_schemas_custom_output() -> None: async def node_b(state: StateForB): assert state == { "bye": "world", - "now": None, } return { "now": 123, @@ -506,8 +656,15 @@ async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: graph.set_finish_point("add_one") gapp = graph.compile() - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "integer", + } assert await app.ainvoke(2) == 3 assert await app.ainvoke(2, output_keys=["output"]) == {"output": 3} @@ -547,16 +704,24 @@ async def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> N input_channels="input", ) - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": { - "output": {"title": "Output", "type": "integer"}, - "fixed": {"title": "Fixed", "type": "integer"}, - "output_plus_one": {"title": "Output Plus One", "type": "integer"}, - }, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None}, + "fixed": {"title": "Fixed", "type": "integer", "default": None}, + "output_plus_one": { + "title": "Output Plus One", + "type": "integer", + "default": None, + }, + }, + } assert await app.ainvoke(2) == {"output": 3, "fixed": 5, "output_plus_one": 4} @@ -571,12 +736,18 @@ async def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: output_channels=["output"], ) - assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": {"output": {"title": "Output", "type": "integer"}}, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "integer", + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None} + }, + } assert await app.ainvoke(2) == {"output": 3} @@ -591,16 +762,21 @@ async def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> output_channels=["output"], ) - assert app.input_schema.schema() == { - "title": "LangGraphInput", - "type": "object", - "properties": {"input": {"title": "Input", "type": "integer"}}, - } - assert app.output_schema.schema() == { - "title": "LangGraphOutput", - "type": "object", - "properties": {"output": {"title": "Output", "type": "integer"}}, - } + if SHOULD_CHECK_SNAPSHOTS: + assert app.input_schema.model_json_schema() == { + "title": "LangGraphInput", + "type": "object", + "properties": { + "input": {"title": "Input", "type": "integer", "default": None} + }, + } + assert app.output_schema.model_json_schema() == { + "title": "LangGraphOutput", + "type": "object", + "properties": { + "output": {"title": "Output", "type": "integer", "default": None} + }, + } assert await app.ainvoke({"input": 2}) == {"output": 3} @@ -664,395 +840,440 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert step == 2 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_invoke_two_processes_in_out_interrupt( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") add_one = mocker.Mock(side_effect=lambda x: x + 1) one = Channel.subscribe_to("input") | add_one | Channel.write_to("inbox") two = Channel.subscribe_to("inbox") | add_one | Channel.write_to("output") - app = Pregel( - nodes={"one": one, "two": two}, - channels={ - "inbox": LastValue(int), - "output": LastValue(int), - "input": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=checkpointer, - interrupt_after_nodes=["one"], - ) - thread1 = {"configurable": {"thread_id": "1"}} - thread2 = {"configurable": {"thread_id": "2"}} - - # start execution, stop at inbox - assert await app.ainvoke(2, thread1) is None - - # inbox == 3 - checkpoint = await checkpointer.aget(thread1) - assert checkpoint is not None - assert checkpoint["channel_values"]["inbox"] == 3 - - # resume execution, finish - assert await app.ainvoke(None, thread1) == 4 - - # start execution again, stop at inbox - assert await app.ainvoke(20, thread1) is None - - # inbox == 21 - checkpoint = await checkpointer.aget(thread1) - assert checkpoint is not None - assert checkpoint["channel_values"]["inbox"] == 21 - - # send a new value in, interrupting the previous execution - assert await app.ainvoke(3, thread1) is None - assert await app.ainvoke(None, thread1) == 5 - - # start execution again, stopping at inbox - assert await app.ainvoke(20, thread2) is None - - # inbox == 21 - snapshot = await app.aget_state(thread2) - assert snapshot.values["inbox"] == 21 - assert snapshot.next == ("two",) - - # update the state, resume - await app.aupdate_state(thread2, 25, as_node="one") - assert await app.ainvoke(None, thread2) == 26 - - # no pending tasks - snapshot = await app.aget_state(thread2) - assert snapshot.next == () - - # list history - history = [c async for c in app.aget_state_history(thread1)] - assert history == [ - StateSnapshot( - values={"inbox": 4, "output": 5, "input": 3}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), }, - metadata={"source": "loop", "step": 6, "writes": 5}, - created_at=AnyStr(), - parent_config=history[1].config, - ), - StateSnapshot( - values={"inbox": 4, "output": 4, "input": 3}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 5, "writes": None}, - created_at=AnyStr(), - parent_config=history[2].config, - ), - StateSnapshot( - values={"inbox": 21, "output": 4, "input": 3}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "input", "step": 4, "writes": 3}, - created_at=AnyStr(), - parent_config=history[3].config, - ), - StateSnapshot( - values={"inbox": 21, "output": 4, "input": 20}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 3, "writes": None}, - created_at=AnyStr(), - parent_config=history[4].config, - ), - StateSnapshot( - values={"inbox": 3, "output": 4, "input": 20}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "input", "step": 2, "writes": 20}, - created_at=AnyStr(), - parent_config=history[5].config, - ), - StateSnapshot( - values={"inbox": 3, "output": 4, "input": 2}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 1, "writes": 4}, - created_at=AnyStr(), - parent_config=history[6].config, - ), - StateSnapshot( - values={"inbox": 3, "input": 2}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 0, "writes": None}, - created_at=AnyStr(), - parent_config=history[7].config, - ), - StateSnapshot( - values={"input": 2}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "input", "step": -1, "writes": 2}, - created_at=AnyStr(), - parent_config=None, - ), - ] + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + interrupt_after_nodes=["one"], + ) + thread1 = {"configurable": {"thread_id": "1"}} + thread2 = {"configurable": {"thread_id": "2"}} - # forking from any previous checkpoint w/out forking should do nothing - assert [ - c async for c in app.astream(None, history[0].config, stream_mode="updates") - ] == [] - assert [ - c async for c in app.astream(None, history[1].config, stream_mode="updates") - ] == [] - assert [ - c async for c in app.astream(None, history[2].config, stream_mode="updates") - ] == [] + # start execution, stop at inbox + assert await app.ainvoke(2, thread1) is None - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = await app.aupdate_state(history[0].config, None) - assert [ - c async for c in app.astream(None, fork_config, stream_mode="updates") - ] == [] + # inbox == 3 + checkpoint = await checkpointer.aget(thread1) + assert checkpoint is not None + assert checkpoint["channel_values"]["inbox"] == 3 - fork_config = await app.aupdate_state(history[1].config, None) - assert [c async for c in app.astream(None, fork_config, stream_mode="updates")] == [ - {"two": {"output": 5}} - ] + # resume execution, finish + assert await app.ainvoke(None, thread1) == 4 - fork_config = await app.aupdate_state(history[2].config, None) - assert [c async for c in app.astream(None, fork_config, stream_mode="updates")] == [ - {"one": {"inbox": 4}} - ] + # start execution again, stop at inbox + assert await app.ainvoke(20, thread1) is None + + # inbox == 21 + checkpoint = await checkpointer.aget(thread1) + assert checkpoint is not None + assert checkpoint["channel_values"]["inbox"] == 21 + + # send a new value in, interrupting the previous execution + assert await app.ainvoke(3, thread1) is None + assert await app.ainvoke(None, thread1) == 5 + + # start execution again, stopping at inbox + assert await app.ainvoke(20, thread2) is None + + # inbox == 21 + snapshot = await app.aget_state(thread2) + assert snapshot.values["inbox"] == 21 + assert snapshot.next == ("two",) + + # update the state, resume + await app.aupdate_state(thread2, 25, as_node="one") + assert await app.ainvoke(None, thread2) == 26 + + # no pending tasks + snapshot = await app.aget_state(thread2) + assert snapshot.next == () + + # list history + history = [c async for c in app.aget_state_history(thread1)] + assert history == [ + StateSnapshot( + values={"inbox": 4, "output": 5, "input": 3}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 6, + "writes": {"two": 5}, + }, + created_at=AnyStr(), + parent_config=history[1].config, + ), + StateSnapshot( + values={"inbox": 4, "output": 4, "input": 3}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[2].config, + ), + StateSnapshot( + values={"inbox": 21, "output": 4, "input": 3}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": 4, + "writes": {"input": 3}, + }, + created_at=AnyStr(), + parent_config=history[3].config, + ), + StateSnapshot( + values={"inbox": 21, "output": 4, "input": 20}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[4].config, + ), + StateSnapshot( + values={"inbox": 3, "output": 4, "input": 20}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": 2, + "writes": {"input": 20}, + }, + created_at=AnyStr(), + parent_config=history[5].config, + ), + StateSnapshot( + values={"inbox": 3, "output": 4, "input": 2}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"two": 4}, + }, + created_at=AnyStr(), + parent_config=history[6].config, + ), + StateSnapshot( + values={"inbox": 3, "input": 2}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[7].config, + ), + StateSnapshot( + values={"input": 2}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"input": 2}, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + + # forking from any previous checkpoint should re-run nodes + assert [ + c async for c in app.astream(None, history[0].config, stream_mode="updates") + ] == [] + assert [ + c async for c in app.astream(None, history[1].config, stream_mode="updates") + ] == [ + {"two": {"output": 5}}, + ] + assert [ + c async for c in app.astream(None, history[2].config, stream_mode="updates") + ] == [ + {"one": {"inbox": 4}}, + ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_fork_always_re_runs_nodes( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") add_one = mocker.Mock(side_effect=lambda _: 1) builder = StateGraph(Annotated[int, operator.add]) builder.add_node("add_one", add_one) builder.add_edge(START, "add_one") builder.add_conditional_edges("add_one", lambda cnt: "add_one" if cnt < 6 else END) - graph = builder.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "1"}} - # start execution, stop at inbox - assert [ - c async for c in graph.astream(1, thread1, stream_mode=["values", "updates"]) - ] == [ - ("values", 1), - ("updates", {"add_one": 1}), - ("values", 2), - ("updates", {"add_one": 1}), - ("values", 3), - ("updates", {"add_one": 1}), - ("values", 4), - ("updates", {"add_one": 1}), - ("values", 5), - ("updates", {"add_one": 1}), - ("values", 6), - ] + # start execution, stop at inbox + assert [ + c + async for c in graph.astream(1, thread1, stream_mode=["values", "updates"]) + ] == [ + ("values", 1), + ("updates", {"add_one": 1}), + ("values", 2), + ("updates", {"add_one": 1}), + ("values", 3), + ("updates", {"add_one": 1}), + ("values", 4), + ("updates", {"add_one": 1}), + ("values", 5), + ("updates", {"add_one": 1}), + ("values", 6), + ] - # list history - history = [c async for c in graph.aget_state_history(thread1)] - assert history == [ - StateSnapshot( - values=6, - next=(), - tasks=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 5, "writes": {"add_one": 1}}, - created_at=AnyStr(), - parent_config=history[1].config, - ), - StateSnapshot( - values=5, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 4, "writes": {"add_one": 1}}, - created_at=AnyStr(), - parent_config=history[2].config, - ), - StateSnapshot( - values=4, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 3, "writes": {"add_one": 1}}, - created_at=AnyStr(), - parent_config=history[3].config, - ), - StateSnapshot( - values=3, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 2, "writes": {"add_one": 1}}, - created_at=AnyStr(), - parent_config=history[4].config, - ), - StateSnapshot( - values=2, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 1, "writes": {"add_one": 1}}, - created_at=AnyStr(), - parent_config=history[5].config, - ), - StateSnapshot( - values=1, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "step": 0, "writes": None}, - created_at=AnyStr(), - parent_config=history[6].config, - ), - StateSnapshot( - values=0, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "input", "step": -1, "writes": 1}, - created_at=AnyStr(), - parent_config=None, - ), - ] + # list history + history = [c async for c in graph.aget_state_history(thread1)] + assert history == [ + StateSnapshot( + values=6, + next=(), + tasks=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[1].config, + ), + StateSnapshot( + values=5, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[2].config, + ), + StateSnapshot( + values=4, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[3].config, + ), + StateSnapshot( + values=3, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[4].config, + ), + StateSnapshot( + values=2, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[5].config, + ), + StateSnapshot( + values=1, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + created_at=AnyStr(), + parent_config=history[6].config, + ), + StateSnapshot( + values=0, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": 1}, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] - # forking from any previous checkpoint w/out forking should do nothing - assert [ - c async for c in graph.astream(None, history[0].config, stream_mode="updates") - ] == [] - assert [ - c async for c in graph.astream(None, history[1].config, stream_mode="updates") - ] == [] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = await graph.aupdate_state(history[0].config, None) - assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") - ] == [] - - fork_config = await graph.aupdate_state(history[1].config, None) - assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") - ] == [{"add_one": 1}] - - fork_config = await graph.aupdate_state(history[2].config, None) - assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") - ] == [ - {"add_one": 1}, - {"add_one": 1}, - ] + # forking from any previous checkpoint should re-run nodes + assert [ + c + async for c in graph.astream(None, history[0].config, stream_mode="updates") + ] == [] + assert [ + c + async for c in graph.astream(None, history[1].config, stream_mode="updates") + ] == [ + {"add_one": 1}, + ] + assert [ + c + async for c in graph.astream(None, history[2].config, stream_mode="updates") + ] == [ + {"add_one": 1}, + {"add_one": 1}, + ] async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: @@ -1110,7 +1331,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "input": 2, "triggers": ["input"], @@ -1121,7 +1342,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "input": [12], "triggers": ["inbox"], @@ -1132,9 +1353,11 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "result": [("inbox", 3)], + "error": None, + "interrupts": [], }, }, { @@ -1142,9 +1365,11 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "result": [("output", 13)], + "error": None, + "interrupts": [], }, }, { @@ -1152,7 +1377,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "input": [3], "triggers": ["inbox"], @@ -1163,9 +1388,11 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "result": [("output", 4)], + "error": None, + "interrupts": [], }, }, ] @@ -1318,7 +1545,8 @@ async def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) assert await app.ainvoke(2) == [3, 3] -async def test_invoke_checkpoint(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_invoke_checkpoint(mocker: MockerFixture, checkpointer_name: str) -> None: add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) errored_once = False @@ -1341,60 +1569,52 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemorySaverAssertImmutable() + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + retry_policy=RetryPolicy(), + ) - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=memory, - retry_policy=RetryPolicy(), - ) - - # total starts out as 0, so output is 0+2=2 - assert await app.ainvoke(2, {"configurable": {"thread_id": "1"}}) == 2 - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 2 - # total is now 2, so output is 2+3=5 - assert await app.ainvoke(3, {"configurable": {"thread_id": "1"}}) == 5 - assert errored_once, "errored and retried" - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - await app.ainvoke(4, {"configurable": {"thread_id": "1"}}) - # checkpoint is not updated - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - # on a new thread, total starts out as 0, so output is 0+5=5 - assert await app.ainvoke(5, {"configurable": {"thread_id": "2"}}) == 5 - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - checkpoint = await memory.aget({"configurable": {"thread_id": "2"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 5 + # total starts out as 0, so output is 0+2=2 + assert await app.ainvoke(2, {"configurable": {"thread_id": "1"}}) == 2 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 2 + # total is now 2, so output is 2+3=5 + assert await app.ainvoke(3, {"configurable": {"thread_id": "1"}}) == 5 + assert errored_once, "errored and retried" + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + await app.ainvoke(4, {"configurable": {"thread_id": "1"}}) + # checkpoint is not updated + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + # on a new thread, total starts out as 0, so output is 0+5=5 + assert await app.ainvoke(5, {"configurable": {"thread_id": "2"}}) == 5 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "2"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 5 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_pending_writes_resume( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - checkpointer: BaseCheckpointSaver = request.getfixturevalue( - f"checkpointer_{checkpointer_name}" - ) - class State(TypedDict): value: Annotated[int, operator.add] @@ -1415,68 +1635,222 @@ async def test_pending_writes_resume( def reset(self): self.calls = 0 - one = AwhileMaker(0.2, {"value": 2}) - two = AwhileMaker(0.6, ValueError("I'm not good")) + one = AwhileMaker(0.1, {"value": 2}) + two = AwhileMaker(0.3, ConnectionError("I'm not good")) builder = StateGraph(State) builder.add_node("one", one) - builder.add_node("two", two) + builder.add_node("two", two, retry=RetryPolicy(max_attempts=2)) builder.add_edge(START, "one") builder.add_edge(START, "two") - graph = builder.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} - with pytest.raises(ValueError, match="I'm not good"): - await graph.ainvoke({"value": 1}, thread1) + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} + with pytest.raises(ConnectionError, match="I'm not good"): + await graph.ainvoke({"value": 1}, thread1) - # both nodes should have been called once - assert one.calls == 1 - assert two.calls == 1 + # both nodes should have been called once + assert one.calls == 1 + assert two.calls == 2 - # latest checkpoint should be before nodes "one", "two" - state = await graph.aget_state(thread1) - assert state is not None - assert state.values == {"value": 1} - assert state.next == ("one", "two") - assert state.tasks == ( - PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ValueError("I'm not good"))), - ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} - # should contain pending write of "one" - checkpoint = await checkpointer.aget_tuple(thread1) - assert checkpoint is not None - # should contain error from "two" - expected_writes = [ - (AnyStr(), "one", "one"), - (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ValueError("I'm not good"))), - ] - assert len(checkpoint.pending_writes) == 3 - assert all(w in expected_writes for w in checkpoint.pending_writes) - # both non-error pending writes come from same task - non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR] - assert non_error_writes[0][0] == non_error_writes[1][0] - # error write is from the other task - error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR) - assert error_write[0] != non_error_writes[0][0] + # latest checkpoint should be before nodes "one", "two" + state = await graph.aget_state(thread1) + assert state is not None + assert state.values == {"value": 1} + assert state.next == ("one", "two") + assert state.tasks == ( + PregelTask(AnyStr(), "one"), + PregelTask(AnyStr(), "two", 'ConnectionError("I\'m not good")'), + ) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } + # should contain pending write of "one" + checkpoint = await checkpointer.aget_tuple(thread1) + assert checkpoint is not None + # should contain error from "two" + expected_writes = [ + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), ERROR, 'ConnectionError("I\'m not good")'), + ] + assert len(checkpoint.pending_writes) == 3 + assert all(w in expected_writes for w in checkpoint.pending_writes) + # both non-error pending writes come from same task + non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR] + assert non_error_writes[0][0] == non_error_writes[1][0] + # error write is from the other task + error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR) + assert error_write[0] != non_error_writes[0][0] - # resume execution - with pytest.raises(ValueError, match="I'm not good"): - await graph.ainvoke(None, thread1) + # resume execution + with pytest.raises(ConnectionError, match="I'm not good"): + await graph.ainvoke(None, thread1) - # node "one" succeeded previously, so shouldn't be called again - assert one.calls == 1 - # node "two" should have been called once again - assert two.calls == 2 + # node "one" succeeded previously, so shouldn't be called again + assert one.calls == 1 + # node "two" should have been called once again + assert two.calls == 4 - # confirm no new checkpoints saved - state_two = await graph.aget_state(thread1) - assert state_two.metadata == state.metadata + # confirm no new checkpoints saved + state_two = await graph.aget_state(thread1) + assert state_two.metadata == state.metadata - # resume execution, without exception - two.rtn = {"value": 3} - # both the pending write and the new write were applied, 1 + 2 + 3 = 6 - assert await graph.ainvoke(None, thread1) == {"value": 6} + # resume execution, without exception + two.rtn = {"value": 3} + # both the pending write and the new write were applied, 1 + 2 + 3 = 6 + assert await graph.ainvoke(None, thread1) == {"value": 6} + + # check all final checkpoints + checkpoints = [c async for c in checkpointer.alist(thread1)] + # we should have 3 + assert len(checkpoints) == 3 + # the last one not too interesting for this test + assert checkpoints[0] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "one": { + "start:one": AnyVersion(), + }, + "two": { + "start:two": AnyVersion(), + }, + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + "__interrupt__": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + }, + "channel_versions": { + "one": AnyVersion(), + "two": AnyVersion(), + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": {"one": "one", "two": "two", "value": 6}, + }, + metadata={ + "parents": {}, + "step": 1, + "source": "loop", + "writes": {"one": {"value": 2}, "two": {"value": 3}}, + }, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[1].config["configurable"][ + "checkpoint_id" + ], + } + }, + pending_writes=[], + ) + # the previous one we assert that pending writes contains both + # - original error + # - successful writes from resuming after preventing error + assert checkpoints[1] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + }, + "channel_versions": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": { + "value": 1, + "start:one": "__start__", + "start:two": "__start__", + }, + }, + metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[2].config["configurable"][ + "checkpoint_id" + ], + } + }, + pending_writes=UnsortedSequence( + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), + (AnyStr(), "two", "two"), + (AnyStr(), "value", 3), + ), + ) + assert checkpoints[2] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": {"__input__": {}}, + "channel_versions": { + "__start__": AnyVersion(), + }, + "channel_values": {"__start__": {"value": 1}}, + }, + metadata={ + "parents": {}, + "step": -1, + "source": "input", + "writes": {"__start__": {"value": 1}}, + }, + parent_config=None, + pending_writes=UnsortedSequence( + (AnyStr(), "value", 1), + (AnyStr(), "start:one", "__start__"), + (AnyStr(), "start:two", "__start__"), + ), + ) async def test_cond_edge_after_send() -> None: @@ -1506,7 +1880,10 @@ async def test_cond_edge_after_send() -> None: assert await graph.ainvoke(["0"]) == ["0", "1", "2", "2", "3"] -async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_invoke_checkpoint_three( + mocker: MockerFixture, checkpointer_name: str +) -> None: add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) def raise_if_above_10(input: int) -> int: @@ -1521,7 +1898,7 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: | raise_if_above_10 ) - async with AsyncSqliteSaver.from_conn_string(":memory:") as memory: + async with awith_checkpointer(checkpointer_name) as checkpointer: app = Pregel( nodes={"one": one}, channels={ @@ -1531,7 +1908,7 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: }, input_channels="input", output_channels="output", - checkpointer=memory, + checkpointer=checkpointer, debug=True, ) @@ -1543,7 +1920,7 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: assert state.values.get("total") == 2 assert ( state.config["configurable"]["checkpoint_id"] - == (await memory.aget(thread_1))["id"] + == (await checkpointer.aget(thread_1))["id"] ) # total is now 2, so output is 2+3=5 assert await app.ainvoke(3, thread_1) == 5 @@ -1552,7 +1929,7 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: assert state.values.get("total") == 7 assert ( state.config["configurable"]["checkpoint_id"] - == (await memory.aget(thread_1))["id"] + == (await checkpointer.aget(thread_1))["id"] ) # total is now 2+5=7, so output would be 7+4=11, but raises ValueError with pytest.raises(ValueError): @@ -1610,10 +1987,10 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: # the first "loop" checkpoint assert thread_1_history[-2].values["total"] == 2 # can get each checkpoint using aget with config - assert (await memory.aget(thread_1_history[0].config))[ + assert (await checkpointer.aget(thread_1_history[0].config))[ "id" ] == thread_1_history[0].config["configurable"]["checkpoint_id"] - assert (await memory.aget(thread_1_history[1].config))[ + assert (await checkpointer.aget(thread_1_history[1].config))[ "id" ] == thread_1_history[1].config["configurable"]["checkpoint_id"] @@ -1732,9 +2109,7 @@ async def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> Non add_one = mocker.Mock(side_effect=lambda x: x + 1) one = ( - Channel.subscribe_to("input") - | add_one - | Channel.write_to(output=RunnablePassthrough(), between=RunnablePassthrough()) + Channel.subscribe_to("input") | add_one | Channel.write_to("output", "between") ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") @@ -1835,7 +2210,6 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert setup_sync.call_count == 0, "Sync context manager should not be used" assert cleanup_sync.call_count == 0, "Sync context manager should not be used" assert setup_async.call_count == 1, "Expected setup to be called once" - assert cleanup_async.call_count == 0, "Expected cleanup to not be called yet" if i == 0: assert chunk == {"inbox": [3]} elif i == 1: @@ -1848,9 +2222,8 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup_async.call_count == 1, "Expected cleanup to be called once" -async def test_conditional_graph() -> None: - from copy import deepcopy - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_conditional_graph(checkpointer_name: str) -> None: from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate @@ -1888,13 +2261,16 @@ async def test_conditional_graph() -> None: # Define tool execution logic async def execute_tools(data: dict) -> dict: + data = data.copy() agent_action: AgentAction = data.pop("agent_outcome") observation = await {t.name: t for t in tools}[agent_action.tool].ainvoke( agent_action.tool_input ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] - data["intermediate_steps"].append((agent_action, observation)) + else: + data["intermediate_steps"] = data["intermediate_steps"].copy() + data["intermediate_steps"].append([agent_action, observation]) return data # Define decision-making logic @@ -1924,32 +2300,29 @@ async def test_conditional_graph() -> None: assert await app.ainvoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" ), } - # deepcopy because the nodes mutate the data - assert [ - deepcopy(c) async for c in app.astream({"input": "what is weather in sf"}) - ] == [ + assert [c async for c in app.astream({"input": "what is weather in sf"})] == [ { "agent": { "input": "what is weather in sf", @@ -1962,14 +2335,14 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -1977,14 +2350,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -1997,22 +2370,22 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2020,22 +2393,22 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2072,14 +2445,14 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2090,22 +2463,22 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2113,49 +2486,22 @@ async def test_conditional_graph() -> None: }, ] - # test state get/update methods with interrupt_after + async with awith_checkpointer(checkpointer_name) as checkpointer: + # test state get/update methods with interrupt_after - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] + config = {"configurable": {"thread_id": "1"}} - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 0, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { "agent": { "input": "what is weather in sf", "agent_outcome": AgentAction( @@ -2164,28 +2510,51 @@ async def test_conditional_graph() -> None: log="tool:search_api:query", ), } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + await app_w_interrupt.aupdate_state( + config, + { "agent_outcome": AgentAction( tool="search_api", tool_input="query", @@ -2193,17 +2562,10 @@ async def test_conditional_graph() -> None: ), "input": "what is weather in sf", }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 1, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "agent_outcome": AgentAction( tool="search_api", @@ -2211,171 +2573,174 @@ async def test_conditional_graph() -> None: log="tool:search_api:a different query", ), "input": "what is weather in sf", - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ) - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + "input": "what is weather in sf", + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ) - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), + "input": "what is weather in sf", + }, }, - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 4, - "writes": { - "agent": { + { + "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + + await app_w_interrupt.aupdate_state( + config, + { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, log="finish:a really nice answer", ), - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test state get/update methods with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "2"}} - llm.i = 0 - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 4, + "writes": { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 0, - "writes": { + # test state get/update methods with interrupt_before + + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "2"}} + llm.i = 0 + + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { "agent": { "input": "what is weather in sf", "agent_outcome": AgentAction( @@ -2384,28 +2749,51 @@ async def test_conditional_graph() -> None: log="tool:search_api:query", ), } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + await app_w_interrupt.aupdate_state( + config, + { "agent_outcome": AgentAction( tool="search_api", tool_input="query", @@ -2413,17 +2801,10 @@ async def test_conditional_graph() -> None: ), "input": "what is weather in sf", }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 1, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "agent_outcome": AgentAction( tool="search_api", @@ -2431,171 +2812,174 @@ async def test_conditional_graph() -> None: log="tool:search_api:a different query", ), "input": "what is weather in sf", - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ) - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + "input": "what is weather in sf", + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ) - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), + "input": "what is weather in sf", + }, }, - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 4, - "writes": { - "agent": { + { + "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + + await app_w_interrupt.aupdate_state( + config, + { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, log="finish:a really nice answer", ), - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test re-invoke to continue with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "2"}} - llm.i = 0 # reset the llm - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 4, + "writes": { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 0, - "writes": { + # test re-invoke to continue with interrupt_before + + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "3"}} + llm.i = 0 # reset the llm + + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { "agent": { "input": "what is weather in sf", "agent_outcome": AgentAction( @@ -2604,105 +2988,173 @@ async def test_conditional_graph() -> None: log="tool:search_api:query", ), } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ) - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ) - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", ), - ( - AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - "result for another", + } + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ], + [ + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ], + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ], + [ + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ], + ], + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" ), - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ), - ( - AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - "result for another", - ), - ], - "agent_outcome": AgentFinish( - return_values={"answer": "answer"}, log="finish:answer" - ), - } - }, - ] + } + }, + ] -async def test_conditional_graph_state(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_conditional_graph_state( + mocker: MockerFixture, checkpointer_name: str +) -> None: from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate @@ -2795,7 +3247,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) - return {"intermediate_steps": [(agent_action, observation)]} + return {"intermediate_steps": [[agent_action, observation]]} # Define decision-making logic def should_continue(data: AgentState) -> str: @@ -2827,22 +3279,22 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: assert await app.ainvoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2863,14 +3315,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2886,14 +3338,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2939,15 +3391,201 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - # test state get/update methods with interrupt_after + async with awith_checkpointer(checkpointer_name) as checkpointer: + # test state get/update methods with interrupt_after - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], + ) + config = {"configurable": {"thread_id": "1"}} + + async with assert_ctx_once(): + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "intermediate_steps": [], + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + async with assert_ctx_once(): + await app_w_interrupt.aupdate_state( + config, + { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "intermediate_steps": [], + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + async with assert_ctx_once(): + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": { + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + } + }, + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + + async with assert_ctx_once(): + await app_w_interrupt.aupdate_state( + config, + { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + }, + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + # test state get/update methods with interrupt_before + + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "2"}} + llm.i = 0 # reset the llm - async with assert_ctx_once(): assert [ c async for c in app_w_interrupt.astream( @@ -2965,40 +3603,38 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - } + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [], }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - async with assert_ctx_once(): await app_w_interrupt.aupdate_state( config, { @@ -3010,52 +3646,52 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 2, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - } + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "intermediate_steps": [], }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - async with assert_ctx_once(): assert [c async for c in app_w_interrupt.astream(None, config)] == [ { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3070,7 +3706,6 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - async with assert_ctx_once(): await app_w_interrupt.aupdate_state( config, { @@ -3081,220 +3716,46 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 5, - "writes": { - "agent": { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test state get/update methods with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "2"}} - llm.i = 0 # reset the llm - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config - ) - ] == [ - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", ), - } - }, - ] - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 2, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], - } - }, - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] - - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - "intermediate_steps": [ - ( - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ) - ], - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 5, - "writes": { - "agent": { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) async def test_conditional_entrypoint_graph() -> None: @@ -3383,7 +3844,7 @@ async def test_prebuilt_tool_chat() -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + from langchain_core.messages import AIMessage, HumanMessage from langchain_core.tools import tool class FakeFuntionChatModel(FakeMessagesListChatModel): @@ -3436,8 +3897,7 @@ async def test_prebuilt_tool_chat() -> None: ) == { "messages": [ _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3447,14 +3907,12 @@ async def test_prebuilt_tool_chat() -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ), - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3469,13 +3927,12 @@ async def test_prebuilt_tool_chat() -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", tool_call_id="tool_call234", - id=AnyStr(), ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", tool_call_id="tool_call567", @@ -3494,8 +3951,7 @@ async def test_prebuilt_tool_chat() -> None: { "agent": { "messages": [ - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3511,11 +3967,10 @@ async def test_prebuilt_tool_chat() -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", tool_call_id="tool_call123", - id=AnyStr(), ) ] } @@ -3523,8 +3978,7 @@ async def test_prebuilt_tool_chat() -> None: { "agent": { "messages": [ - AIMessage( - id=AnyStr(), + _AnyIdAIMessage( content="", tool_calls=[ { @@ -3545,17 +3999,15 @@ async def test_prebuilt_tool_chat() -> None: { "tools": { "messages": [ - ToolMessage( + _AnyIdToolMessage( content="result for another", + name="search_api", tool_call_id="tool_call234", - name="search_api", - id=AnyStr(), ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", - tool_call_id="tool_call567", name="search_api", - id=AnyStr(), + tool_call_id="tool_call567", ), ] } @@ -3564,7 +4016,14 @@ async def test_prebuilt_tool_chat() -> None: ] -async def test_state_graph_packets() -> None: +# defined outside to allow deserializer to see it +class ToolInput(BaseModel, arbitrary_types_allowed=True): + call: ToolCall + my_session: httpx.AsyncClient + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_state_graph_packets(checkpointer_name: str) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) @@ -3572,13 +4031,13 @@ async def test_state_graph_packets() -> None: AIMessage, BaseMessage, HumanMessage, - ToolCall, ToolMessage, ) from langchain_core.tools import tool class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] + session: Annotated[httpx.AsyncClient, Context(httpx.AsyncClient)] @tool() def search_api(query: str) -> str: @@ -3623,13 +4082,19 @@ async def test_state_graph_packets() -> None: # Define decision-making logic def should_continue(data: AgentState) -> str: + assert isinstance(data["session"], httpx.AsyncClient) # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: - return [Send("tools", tool_call) for tool_call in tool_calls] + return [ + Send("tools", ToolInput(call=tool_call, my_session=data["session"])) + for tool_call in tool_calls + ] else: return END - async def tools_node(tool_call: ToolCall, config: RunnableConfig) -> AgentState: + async def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + assert isinstance(input.my_session, httpx.AsyncClient) + tool_call = input.call await asyncio.sleep(tool_call["args"].get("idx", 0) / 10) output = await tools_by_name[tool_call["name"]].ainvoke( tool_call["args"], config @@ -3679,10 +4144,9 @@ async def test_state_graph_packets() -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ), AIMessage( @@ -3701,16 +4165,14 @@ async def test_state_graph_packets() -> None: }, ], ), - ToolMessage( + _AnyIdToolMessage( content="result for another", name="search_api", - id=AnyStr(), tool_call_id="tool_call234", ), - ToolMessage( + _AnyIdToolMessage( content="result for a third one", name="search_api", - id=AnyStr(), tool_call_id="tool_call567", ), AIMessage(content="answer", id="ai3"), @@ -3740,10 +4202,9 @@ async def test_state_graph_packets() -> None: }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for query", name="search_api", - id=AnyStr(), tool_call_id="tool_call123", ) } @@ -3770,20 +4231,18 @@ async def test_state_graph_packets() -> None: }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for another", name="search_api", - id=AnyStr(), tool_call_id="tool_call234", ) }, }, { "tools": { - "messages": ToolMessage( + "messages": _AnyIdToolMessage( content="result for a third one", name="search_api", - id=AnyStr(), tool_call_id="tool_call567", ), }, @@ -3791,62 +4250,20 @@ async def test_state_graph_packets() -> None: {"agent": {"messages": AIMessage(content="answer", id="ai3")}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"messages": HumanMessage(content="what is weather in sf")}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": { - "messages": AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "query"}, - }, - ], - ) - } - }, - ] + config = {"configurable": {"thread_id": "1"}} - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "query"}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + {"messages": HumanMessage(content="what is weather in sf")}, config + ) + ] == [ + { "agent": { "messages": AIMessage( id="ai1", @@ -3861,47 +4278,68 @@ async def test_state_graph_packets() -> None: ) } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - # modify ai message - last_message = (await app_w_interrupt.aget_state(config)).values["messages"][-1] - last_message.tool_calls[0]["args"]["query"] = "a different query" - await app_w_interrupt.aupdate_state(config, {"messages": last_message}) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + }, + ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "messages": AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + }, + ], + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - # message was replaced instead of appended - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 2, - "writes": { - "agent": { - "messages": AIMessage( + # modify ai message + last_message = (await app_w_interrupt.aget_state(config)).values["messages"][-1] + last_message.tool_calls[0]["args"]["query"] = "a different query" + await app_w_interrupt.aupdate_state(config, {"messages": last_message}) + + # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( id="ai1", content="", tool_calls=[ @@ -3911,97 +4349,49 @@ async def test_state_graph_packets() -> None: "args": {"query": "a different query"}, }, ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "messages": AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": { + "messages": _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", ) } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "messages": ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ) - } - }, - { - "agent": { - "messages": AIMessage( - id="ai2", - content="", - tool_calls=[ - { - "id": "tool_call234", - "name": "search_api", - "args": {"query": "another", "idx": 0}, - }, - { - "id": "tool_call567", - "name": "search_api", - "args": {"query": "a third one", "idx": 1}, - }, - ], - ) - }, - }, - ] - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ), - AIMessage( - id="ai2", - content="", - tool_calls=[ - { - "id": "tool_call234", - "name": "search_api", - "args": {"query": "another", "idx": 0}, - }, - { - "id": "tool_call567", - "name": "search_api", - "args": {"query": "a third one", "idx": 1}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"), PregelTask(AnyStr(), "tools")), - next=("tools", "tools"), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 4, - "writes": { + { "agent": { "messages": AIMessage( id="ai2", @@ -4021,65 +4411,137 @@ async def test_state_graph_packets() -> None: ) }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - await app_w_interrupt.aupdate_state( - config, - {"messages": AIMessage(content="answer", id="ai2")}, - ) + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ), + _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", + ), + AIMessage( + id="ai2", + content="", + tool_calls=[ + { + "id": "tool_call234", + "name": "search_api", + "args": {"query": "another", "idx": 0}, + }, + { + "id": "tool_call567", + "name": "search_api", + "args": {"query": "a third one", "idx": 1}, + }, + ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"), PregelTask(AnyStr(), "tools")), + next=("tools", "tools"), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": { + "agent": { + "messages": AIMessage( + id="ai2", + content="", + tool_calls=[ + { + "id": "tool_call234", + "name": "search_api", + "args": {"query": "another", "idx": 0}, + }, + { + "id": "tool_call567", + "name": "search_api", + "args": {"query": "a third one", "idx": 1}, + }, + ], + ) + }, + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - # replaces message even if object identity is different, as long as id is the same - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ), - AIMessage(content="answer", id="ai2"), - ] - }, - tasks=(), - next=(), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 5, - "writes": {"agent": {"messages": AIMessage(content="answer", id="ai2")}}, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + await app_w_interrupt.aupdate_state( + config, + {"messages": AIMessage(content="answer", id="ai2")}, + ) + + # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ), + _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", + ), + AIMessage(content="answer", id="ai2"), + ] + }, + tasks=(), + next=(), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": { + "messages": AIMessage(content="answer", id="ai2"), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) -async def test_message_graph() -> None: - from langchain_core.agents import AgentAction +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_message_graph(checkpointer_name: str) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + from langchain_core.messages import AIMessage, HumanMessage from langchain_core.tools import tool class FakeFuntionChatModel(FakeMessagesListChatModel): @@ -4097,63 +4559,46 @@ async def test_message_graph() -> None: responses=[ AIMessage( content="", - additional_kwargs={ - "function_call": { + tool_calls=[ + { + "id": "tool_call123", "name": "search_api", - "arguments": json.dumps("query"), + "args": {"query": "query"}, } - }, + ], id="ai1", ), AIMessage( content="", - additional_kwargs={ - "function_call": { + tool_calls=[ + { + "id": "tool_call456", "name": "search_api", - "arguments": json.dumps("another"), + "args": {"query": "another"}, } - }, + ], id="ai2", ), AIMessage(content="answer", id="ai3"), ] ) - tool_executor = ToolExecutor(tools) - # Define the function that determines whether to continue or not def should_continue(messages): last_message = messages[-1] # If there is no function call, then we finish - if "function_call" not in last_message.additional_kwargs: + if not last_message.tool_calls: return "end" # Otherwise if there is, we continue else: return "continue" - async def call_tool(messages): - # Based on the continue condition - # we know the last message involves a function call - last_message = messages[-1] - # We construct an AgentAction from the function_call - action = AgentAction( - tool=last_message.additional_kwargs["function_call"]["name"], - tool_input=json.loads( - last_message.additional_kwargs["function_call"]["arguments"] - ), - log="", - ) - # We call the tool_executor and get back a response - response = await tool_executor.ainvoke(action) - # We use the response to create a FunctionMessage - return FunctionMessage(content=str(response), name=action.tool) - # Define a new graph workflow = MessageGraph() # Define the two nodes we will cycle between workflow.add_node("agent", model) - workflow.add_node("tools", call_tool) + workflow.add_node("tools", ToolNode(tools)) # Set the entrypoint as `agent` # This means that this node is the first one called @@ -4190,23 +4635,41 @@ async def test_message_graph() -> None: app = workflow.compile() assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - id="ai1", + _AnyIdHumanMessage( + content="what is weather in sf", ), - FunctionMessage(content="result for query", name="search_api", id=AnyStr()), AIMessage( content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + } + ], + id="ai1", # respects ids passed in + ), + _AnyIdToolMessage( + content="result for query", + name="search_api", + tool_call_id="tool_call123", + ), + AIMessage( + content="", + tool_calls=[ + { + "id": "tool_call456", + "name": "search_api", + "args": {"query": "another"}, + } + ], id="ai2", ), - FunctionMessage(content="result for another", name="search_api", id=AnyStr()), + _AnyIdToolMessage( + content="result for another", + name="search_api", + tool_call_id="tool_call456", + ), AIMessage(content="answer", id="ai3"), ] @@ -4216,252 +4679,296 @@ async def test_message_graph() -> None: { "agent": AIMessage( content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + } + ], id="ai1", ) }, { - "tools": FunctionMessage( - content="result for query", name="search_api", id=AnyStr() - ) + "tools": [ + _AnyIdToolMessage( + content="result for query", + name="search_api", + tool_call_id="tool_call123", + ) + ] }, { "agent": AIMessage( content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, + tool_calls=[ + { + "id": "tool_call456", + "name": "search_api", + "args": {"query": "another"}, + } + ], id="ai2", ) }, { - "tools": FunctionMessage( - content="result for another", name="search_api", id=AnyStr() - ) + "tools": [ + _AnyIdToolMessage( + content="result for another", + name="search_api", + tool_call_id="tool_call456", + ) + ] }, {"agent": AIMessage(content="answer", id="ai3")}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - HumanMessage(content="what is weather in sf"), config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - id="ai1", - ) - }, - ] + config = {"configurable": {"thread_id": "1"}} - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - id="ai1", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + HumanMessage(content="what is weather in sf"), config + ) + ] == [ + { "agent": AIMessage( content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + } + ], id="ai1", ) }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - # modify ai message - last_message = (await app_w_interrupt.aget_state(config)).values[-1] - last_message.additional_kwargs["function_call"]["arguments"] = '"a different query"' - await app_w_interrupt.aupdate_state(config, last_message) - - # message was replaced instead of appended - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } - }, - id="ai1", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 2, - "writes": { - "agent": AIMessage( + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( content="", - additional_kwargs={ - "function_call": { + tool_calls=[ + { + "id": "tool_call123", "name": "search_api", - "arguments": '"a different query"', + "args": {"query": "query"}, } - }, + ], id="ai1", - ) + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": AIMessage( + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + } + ], + id="ai1", + ) + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ) - }, - { - "agent": AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - id="ai2", - ) - }, - ] + # modify ai message + last_message = (await app_w_interrupt.aget_state(config)).values[-1] + last_message.tool_calls[0]["args"] = {"query": "a different query"} + await app_w_interrupt.aupdate_state(config, last_message) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } + # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + id="ai1", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + } + ], + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": AIMessage( + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + } + ], + id="ai1", + ) }, - id="ai1", - ), - FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ), - AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - id="ai2", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 4, - "writes": { + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": [ + _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", + ) + ] + }, + { "agent": AIMessage( content="", - additional_kwargs={ - "function_call": { + tool_calls=[ + { + "id": "tool_call456", "name": "search_api", - "arguments": '"another"', + "args": {"query": "another"}, } - }, + ], id="ai2", ) }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - await app_w_interrupt.aupdate_state( - config, - AIMessage(content="answer", id="ai2"), - ) - - # replaces message even if object identity is different, as long as id is the same - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + id="ai1", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + } + ], + ), + _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", + ), + AIMessage( + content="", + tool_calls=[ + { + "id": "tool_call456", + "name": "search_api", + "args": {"query": "another"}, + } + ], + id="ai2", + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": { + "agent": AIMessage( + content="", + tool_calls=[ + { + "id": "tool_call456", + "name": "search_api", + "args": {"query": "another"}, + } + ], + id="ai2", + ) }, - id="ai1", - ), - FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ), + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + await app_w_interrupt.aupdate_state( + config, AIMessage(content="answer", id="ai2"), - ], - tasks=(), - next=(), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 5, - "writes": {"agent": AIMessage(content="answer", id="ai2")}, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ) + + # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + id="ai1", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + } + ], + ), + _AnyIdToolMessage( + content="result for a different query", + name="search_api", + tool_call_id="tool_call123", + ), + AIMessage(content="answer", id="ai2"), + ], + tasks=(), + next=(), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": {"agent": AIMessage(content="answer", id="ai2")}, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) async def test_in_one_fan_out_out_one_graph_state() -> None: @@ -4549,13 +5056,9 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", - "input": { - "query": "what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], }, }, @@ -4568,9 +5071,11 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", "result": [("query", "query: what is weather in sf")], + "error": None, + "interrupts": [], }, }, ), @@ -4582,13 +5087,9 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -4600,13 +5101,9 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -4622,9 +5119,11 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", "result": [("docs", ["doc3", "doc4"])], + "error": None, + "interrupts": [], }, }, ), @@ -4639,9 +5138,11 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", "result": [("docs", ["doc1", "doc2"])], + "error": None, + "interrupts": [], }, }, ), @@ -4659,11 +5160,10 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "input": { "query": "query: what is weather in sf", - "answer": None, "docs": ["doc1", "doc2", "doc3", "doc4"], }, "triggers": ["retriever_one", "retriever_two"], @@ -4678,9 +5178,11 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "result": [("answer", "doc1,doc2,doc3,doc4")], + "error": None, + "interrupts": [], }, }, ), @@ -4695,14 +5197,38 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: ] -async def test_start_branch_then() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_start_branch_then(checkpointer_name: str) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str + shared: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] + other: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] + + def assert_shared_value(data: State, config: RunnableConfig) -> State: + assert "shared" in data + if thread_id := config["configurable"].get("thread_id"): + if thread_id == "1": + # this is the first thread, so should not see a value + assert data["shared"] == {} + return {"shared": {"1": {"hello": "world"}}, "other": {"2": {1: 2}}} + elif thread_id == "2": + # this should get value saved by thread 1 + assert data["shared"] == {"1": {"hello": "world"}} + elif thread_id == "3": + # this is a different assistant, so should not see previous value + assert data["shared"] == {} + return {} + + def tool_two_slow(data: State, config: RunnableConfig) -> State: + return {"my_key": " slow", **assert_shared_value(data, config)} + + def tool_two_fast(data: State, config: RunnableConfig) -> State: + return {"my_key": " fast", **assert_shared_value(data, config)} tool_two_graph = StateGraph(State) - tool_two_graph.add_node("tool_two_slow", lambda s, config: {"my_key": " slow"}) - tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"}) + tool_two_graph.add_node("tool_two_slow", tool_two_slow) + tool_two_graph.add_node("tool_two_fast", tool_two_fast) tool_two_graph.set_conditional_entry_point( lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", then=END ) @@ -4717,16 +5243,18 @@ async def test_start_branch_then() -> None: "market": "US", } - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: + async with awith_checkpointer(checkpointer_name) as checkpointer: tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + store=MemoryStore(), + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], ) # missing thread_id with pytest.raises(ValueError, match="thread_id"): await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { "my_key": "value", @@ -4734,14 +5262,16 @@ async def test_start_branch_then() -> None: } assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, { + "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, ] assert await tool_two.aget_state(thread1) == StateSnapshot( @@ -4752,7 +5282,7 @@ async def test_start_branch_then() -> None: created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ "ts" ], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[ c async for c in tool_two.checkpointer.alist(thread1, limit=2) ][-1].config, @@ -4771,6 +5301,7 @@ async def test_start_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -4780,7 +5311,7 @@ async def test_start_branch_then() -> None: ][-1].config, ) - thread2 = {"configurable": {"thread_id": "2"}} + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { "my_key": "value", @@ -4794,7 +5325,7 @@ async def test_start_branch_then() -> None: created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ "ts" ], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[ c async for c in tool_two.checkpointer.alist(thread2, limit=2) ][-1].config, @@ -4813,6 +5344,7 @@ async def test_start_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -4822,7 +5354,7 @@ async def test_start_branch_then() -> None: ][-1].config, ) - thread3 = {"configurable": {"thread_id": "3"}} + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { "my_key": "value", @@ -4836,7 +5368,7 @@ async def test_start_branch_then() -> None: created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ "ts" ], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[ c async for c in tool_two.checkpointer.alist(thread3, limit=2) ][-1].config, @@ -4852,6 +5384,7 @@ async def test_start_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": {START: {"my_key": "key"}}, @@ -4874,6 +5407,7 @@ async def test_start_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -4884,7 +5418,8 @@ async def test_start_branch_then() -> None: ) -async def test_branch_then() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_branch_then(checkpointer_name: str) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -4912,9 +5447,9 @@ async def test_branch_then() -> None: "market": "US", } - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: + async with awith_checkpointer(checkpointer_name) as checkpointer: # test stream_mode=debug - tool_two = tool_two_graph.compile(checkpointer=saver) + tool_two = tool_two_graph.compile(checkpointer=checkpointer) thread10 = {"configurable": {"thread_id": "10"}} assert [ c @@ -4940,17 +5475,13 @@ async def test_branch_then() -> None: }, "values": {"my_key": ""}, "metadata": { + "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, "next": ["__start__"], - "tasks": [ - { - "id": AnyStr(), - "name": "__start__", - } - ], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, }, { @@ -4974,17 +5505,13 @@ async def test_branch_then() -> None: "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, "next": ["prepare"], - "tasks": [ - { - "id": AnyStr(), - "name": "prepare", - } - ], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, }, { @@ -4992,7 +5519,7 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "input": {"my_key": "value", "market": "DE"}, "triggers": ["start:prepare"], @@ -5003,9 +5530,11 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, }, { @@ -5029,16 +5558,14 @@ async def test_branch_then() -> None: "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, }, "next": ["tool_two_slow"], "tasks": [ - { - "id": AnyStr(), - "name": "tool_two_slow", - } + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} ], }, }, @@ -5047,7 +5574,7 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "input": {"my_key": "value prepared", "market": "DE"}, "triggers": ["branch:prepare:condition:tool_two_slow"], @@ -5058,9 +5585,11 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "result": [("my_key", " slow")], + "error": None, + "interrupts": [], }, }, { @@ -5084,17 +5613,13 @@ async def test_branch_then() -> None: "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_slow": {"my_key": " slow"}}, }, "next": ["finish"], - "tasks": [ - { - "id": AnyStr(), - "name": "finish", - } - ], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, }, { @@ -5102,7 +5627,7 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "input": {"my_key": "value prepared slow", "market": "DE"}, "triggers": ["branch:prepare:condition::then"], @@ -5113,9 +5638,11 @@ async def test_branch_then() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "result": [("my_key", " finished")], + "error": None, + "interrupts": [], }, }, { @@ -5139,6 +5666,7 @@ async def test_branch_then() -> None: "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5150,19 +5678,135 @@ async def test_branch_then() -> None: ] tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], ) # missing thread_id with pytest.raises(ValueError, match="thread_id"): await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "11"}} # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [ + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} + ], + }, + }, + ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, tasks=(PregelTask(AnyStr(), "tool_two_slow"),), @@ -5172,6 +5816,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5194,6 +5839,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5203,7 +5849,7 @@ async def test_branch_then() -> None: ][-1].config, ) - thread2 = {"configurable": {"thread_id": "2"}} + thread2 = {"configurable": {"thread_id": "12"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { "my_key": "value prepared", @@ -5218,6 +5864,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5240,6 +5887,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5249,16 +5897,15 @@ async def test_branch_then() -> None: ][-1].config, ) - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_after=["prepare"] + checkpointer=checkpointer, interrupt_after=["prepare"] ) # missing thread_id with pytest.raises(ValueError, match="thread_id"): await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "21"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { "my_key": "value prepared", @@ -5273,6 +5920,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5295,6 +5943,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5304,7 +5953,7 @@ async def test_branch_then() -> None: ][-1].config, ) - thread2 = {"configurable": {"thread_id": "2"}} + thread2 = {"configurable": {"thread_id": "22"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { "my_key": "value prepared", @@ -5319,6 +5968,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5341,6 +5991,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5350,7 +6001,7 @@ async def test_branch_then() -> None: ][-1].config, ) - thread3 = {"configurable": {"thread_id": "3"}} + thread3 = {"configurable": {"thread_id": "23"}} # update an empty thread before first run uconfig = await tool_two.aupdate_state( thread3, {"my_key": "key", "market": "DE"} @@ -5363,6 +6014,7 @@ async def test_branch_then() -> None: config=uconfig, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 0, "writes": {START: {"my_key": "key", "market": "DE"}}, @@ -5384,6 +6036,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5404,6 +6057,7 @@ async def test_branch_then() -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5414,7 +6068,8 @@ async def test_branch_then() -> None: ) -async def test_in_one_fan_out_state_graph_waiting_edge() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_in_one_fan_out_state_graph_waiting_edge(checkpointer_name: str) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -5476,31 +6131,33 @@ async def test_in_one_fan_out_state_graph_waiting_edge() -> None: {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_via_branch( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, checkpointer_name: str ) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] @@ -5567,33 +6224,35 @@ async def test_in_one_fan_out_state_graph_waiting_edge_via_branch( {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, mocker: MockerFixture, checkpointer_name: str ) -> None: - from langchain_core.pydantic_v1 import BaseModel, ValidationError + from pydantic.v1 import BaseModel, ValidationError setup = mocker.Mock() teardown = mocker.Mock() @@ -5709,74 +6368,77 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], + ) + config = {"configurable": {"thread_id": "1"}} - async with assert_ctx_once(): - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config - ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + async with assert_ctx_once(): + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] - async with assert_ctx_once(): - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + async with assert_ctx_once(): + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "query": "analyzed: query: what is weather in sf", - "answer": "doc1,doc2,doc3,doc4", - "docs": ["doc1", "doc2", "doc3", "doc4"], - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "query": "analyzed: query: what is weather in sf", + "answer": "doc1,doc2,doc3,doc4", + "docs": ["doc1", "doc2", "doc3", "doc4"], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + "step": 4, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + + async with assert_ctx_once(): + assert await app_w_interrupt.aupdate_state( + config, {"docs": ["doc5"]}, as_node="rewrite_query" + ) == { + "configurable": { + "thread_id": "1", + "checkpoint_id": AnyStr(), + "checkpoint_ns": "", + } } - }, - metadata={ - "source": "loop", - "writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - "step": 4, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - - async with assert_ctx_once(): - assert await app_w_interrupt.aupdate_state( - config, {"docs": ["doc5"]}, as_node="rewrite_query" - ) == { - "configurable": { - "thread_id": "1", - "checkpoint_id": AnyStr(), - "checkpoint_ns": "", - } - } +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, checkpointer_name: str ) -> None: from pydantic import BaseModel, ValidationError @@ -5842,9 +6504,10 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydant app = workflow.compile() - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot - assert app.get_input_schema().schema() == snapshot - assert app.get_output_schema().schema() == snapshot + if SHOULD_CHECK_SNAPSHOTS: + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + assert app.get_input_schema().model_json_schema() == snapshot + assert app.get_output_schema().model_json_schema() == snapshot with pytest.raises(ValidationError): await app.ainvoke({"query": {}}) @@ -5871,40 +6534,44 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydant {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf", "inner": {"yo": 1}}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf", "inner": {"yo": 1}}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] - assert await app_w_interrupt.aupdate_state( - config, {"docs": ["doc5"]}, as_node="rewrite_query" - ) == { - "configurable": { - "thread_id": "1", - "checkpoint_id": AnyStr(), - "checkpoint_ns": "", + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] + + assert await app_w_interrupt.aupdate_state( + config, {"docs": ["doc5"]}, as_node="rewrite_query" + ) == { + "configurable": { + "thread_id": "1", + "checkpoint_id": AnyStr(), + "checkpoint_ns": "", + } } - } -async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular( + checkpointer_name: str, +) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -5972,28 +6639,29 @@ async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"qa": {"answer": ""}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"qa": {"answer": ""}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] async def test_in_one_fan_out_state_graph_waiting_edge_multiple() -> None: @@ -6290,1247 +6958,8 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 -@pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - - class InnerState(TypedDict): - my_key: str - my_other_key: str - - async def inner_1(state: InnerState): - return { - "my_key": state["my_key"] + " here", - "my_other_key": state["my_key"], - } - - async def inner_2(state: InnerState): - return { - "my_key": state["my_key"] + " and there", - "my_other_key": state["my_key"], - } - - inner = StateGraph(InnerState) - inner.add_node("inner_1", inner_1) - inner.add_node("inner_2", inner_2) - inner.add_edge("inner_1", "inner_2") - inner.set_entry_point("inner_1") - inner.set_finish_point("inner_2") - - class State(TypedDict): - my_key: str - - async def outer_1(state: State): - return {"my_key": "hi " + state["my_key"]} - - async def outer_2(state: State): - return {"my_key": state["my_key"] + " and back again"} - - graph = StateGraph(State) - graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) - graph.add_node("outer_2", outer_2) - graph.set_entry_point("outer_1") - graph.add_edge("outer_1", "inner") - graph.add_edge("inner", "outer_2") - graph.set_finish_point("outer_2") - - app = graph.compile(checkpointer=checkpointer) - - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ - {"outer_1": {"my_key": "hi my value"}}, - ] - assert [c async for c in app.astream(None, config)] == [ - {"inner": {"my_key": "hi my value here and there"}}, - {"outer_2": {"my_key": "hi my value here and there and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - - # test interrupts BEFORE the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_before=["inner"]) - config = {"configurable": {"thread_id": "4"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # while we're waiting for the node w/ interrupt inside to finish - assert [c async for c in app.astream(None, config, stream_mode="values")] == [] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test interrupts AFTER the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_after=["inner"]) - config = {"configurable": {"thread_id": "5"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there and back again", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test restarting from checkpoint_id - config = {"configurable": {"thread_id": "6"}} - app = graph.compile(checkpointer=checkpointer) - await app.ainvoke({"my_key": "my value"}, config, debug=True) - - state_history = [c async for c in app.aget_state_history(config)] - assert state_history == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - child_state_history = [ - c - async for c in app.aget_state_history( - {"configurable": {"thread_id": "6", "checkpoint_ns": "inner"}} - ) - ] - assert child_state_history == [ - StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - # there should be a single child checkpoint because we only keep - # one child checkpoint per parent checkpoint (in which child ran) - ] - - # check that child snapshot matches id of parent - child_snapshot = child_state_history[0] - assert ( - child_snapshot.config["configurable"]["checkpoint_id"] - == state_history[0].config["configurable"]["checkpoint_id"] - ) - # check resuming from interrupt w/ checkpoint_id - interrupt_state_snapshot, before_interrupt_state_snapshot = state_history[:2] - before_interrupt_config = before_interrupt_state_snapshot.config - # going to get to interrupt again here - assert await app.ainvoke(None, before_interrupt_config, debug=True) == { - "my_key": "hi my value" - } - # one more "identical" snapshot than before, at top of list - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # going to resume from interrupt - interrupt_config = interrupt_state_snapshot.config - assert (await app.ainvoke(None, interrupt_config, debug=True)) == { - "my_key": "hi my value here and there and back again", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_nested_graph_interrupts_parallel( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] my_other_key: str @@ -7562,7 +6991,10 @@ async def test_nested_graph_interrupts_parallel( return {"my_key": " and back again"} graph = StateGraph(State) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"]), + ) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -7571,93 +7003,91 @@ async def test_nested_graph_interrupts_parallel( graph.add_edge(["inner", "outer_1"], "outer_2") graph.set_finish_point("outer_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": ""}, config, debug=True) == { - "my_key": "", - } - - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "got here and there and parallel and back again", - } - - # below combo of assertions is asserting two things - # - outer_1 finishes before inner interrupts (because we see its output in stream, which only happens after node finishes) - # - the writes of outer are persisted in 1st call and used in 2nd call, ie outer isn't called again (because we dont see outer_1 output again in 2nd stream) - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [c async for c in app.astream({"my_key": ""}, config)] == [ - # we got to parallel node first - {"outer_1": {"my_key": " and parallel"}}, - ] - assert [c async for c in app.astream(None, config)] == [ - {"inner": {"my_key": "got here and there"}}, - {"outer_2": {"my_key": " and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") - ] == [ - { + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert await app.ainvoke({"my_key": ""}, config, debug=True) == { "my_key": "", - }, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { + } + + assert await app.ainvoke(None, config, debug=True) == { "my_key": "got here and there and parallel and back again", - }, - ] + } - # # test interrupts BEFORE the parallel node - app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"]) - config = {"configurable": {"thread_id": "4"}} - assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") - ] == [{"my_key": ""}] - # while we're waiting for the node w/ interrupt inside to finish - assert [c async for c in app.astream(None, config, stream_mode="values")] == [] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, - ] + # below combo of assertions is asserting two things + # - outer_1 finishes before inner interrupts (because we see its output in stream, which only happens after node finishes) + # - the writes of outer are persisted in 1st call and used in 2nd call, ie outer isn't called again (because we dont see outer_1 output again in 2nd stream) + # test stream updates w/ nested interrupt + config = {"configurable": {"thread_id": "2"}} + assert [ + c async for c in app.astream({"my_key": ""}, config, subgraphs=True) + ] == [ + # we got to parallel node first + ((), {"outer_1": {"my_key": " and parallel"}}), + ( + (AnyStr("inner:"),), + {"inner_1": {"my_key": "got here", "my_other_key": ""}}, + ), + ] + assert [c async for c in app.astream(None, config)] == [ + {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, + {"inner": {"my_key": "got here and there"}}, + {"outer_2": {"my_key": " and back again"}}, + ] - # test interrupts AFTER the parallel node - app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"]) - config = {"configurable": {"thread_id": "5"}} - assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") - ] == [{"my_key": ""}] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": "got here and there and parallel"}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel and back again", - }, - ] + # test stream values w/ nested interrupt + config = {"configurable": {"thread_id": "3"}} + assert [ + c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + ] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] + + # # test interrupts BEFORE the parallel node + app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"]) + config = {"configurable": {"thread_id": "4"}} + assert [ + c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + ] == [ + {"my_key": ""}, + ] + # while we're waiting for the node w/ interrupt inside to finish + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] + + # test interrupts AFTER the parallel node + app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"]) + config = {"configurable": {"thread_id": "5"}} + assert [ + c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + ] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] -@pytest.mark.skip -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_doubly_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: class State(TypedDict): my_key: str @@ -7683,7 +7113,10 @@ async def test_doubly_nested_graph_interrupts( grandchild.set_finish_point("grandchild_2") child = StateGraph(ChildState) - child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.add_node( + "child_1", + grandchild.compile(interrupt_before=["grandchild_2"]), + ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -7702,49 +7135,1981 @@ async def test_doubly_nested_graph_interrupts( graph.add_edge("child", "parent_2") graph.set_finish_point("parent_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value", - } - - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ - {"parent_1": {"my_key": "hi my value"}}, - ] - assert [c async for c in app.astream(None, config)] == [ - {"child": {"my_key": "hi my value here and there"}}, - {"parent_2": {"my_key": "hi my value here and there and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { "my_key": "hi my value", - }, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { + } + + assert await app.ainvoke(None, config, debug=True) == { "my_key": "hi my value here and there and back again", - }, - ] + } + + # test stream updates w/ nested interrupt + config = {"configurable": {"thread_id": "2"}} + assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ + {"parent_1": {"my_key": "hi my value"}}, + ] + assert [c async for c in app.astream(None, config)] == [ + {"child": {"my_key": "hi my value here and there"}}, + {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ] + + # test stream values w/ nested interrupt + config = {"configurable": {"thread_id": "3"}} + assert [ + c + async for c in app.astream( + {"my_key": "my value"}, config, stream_mode="values" + ) + ] == [ + {"my_key": "my value"}, + {"my_key": "hi my value"}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": "hi my value"}, + {"my_key": "hi my value here and there"}, + {"my_key": "hi my value here and there and back again"}, + ] + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_nested_graph_state(checkpointer_name: str) -> None: + class InnerState(TypedDict): + my_key: str + my_other_key: str + + def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + inner = StateGraph(InnerState) + inner.add_node("inner_1", inner_1) + inner.add_node("inner_2", inner_2) + inner.add_edge("inner_1", "inner_2") + inner.set_entry_point("inner_1") + inner.set_finish_point("inner_2") + + class State(TypedDict): + my_key: str + other_parent_key: str + + def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"]), + ) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + # first get_state without subgraph state + assert await app.aget_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()} + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # now, get_state with subgraphs state + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": { + "": AnyStr(), + }, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # get_state_history returns outer graph checkpoints + history = [c async for c in app.aget_state_history(config)] + assert history == [ + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + # get_state_history for a subgraph returns its checkpoints + child_history = [ + c async for c in app.aget_state_history(history[0].tasks[0].state) + ] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # resume + await app.ainvoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert await app.aget_state(config) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # test full history at the end + actual_history = [c async for c in app.aget_state_history(config)] + expected_history = [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "outer_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(PregelTask(AnyStr(), "outer_2"),), + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + # test looking up parent state by checkpoint ID + for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): + assert await app.aget_state(actual_snapshot.config) == expected_snapshot + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_doubly_nested_graph_state(checkpointer_name: str) -> None: + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node( + "child_1", + grandchild.compile(interrupt_before=["grandchild_2"]), + ) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile()) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert [ + c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) + ] == [ + ((), {"parent_1": {"my_key": "hi my value"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ), + ] + # get state without subgraphs + outer_state = await app.aget_state(config) + assert outer_state == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + child_state = await app.aget_state(outer_state.tasks[0].state) + assert ( + child_state.tasks[0] + == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ).tasks[0] + ) + grandchild_state = await app.aget_state(child_state.tasks[0].state) + assert grandchild_state == StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ) + # get state with subgraphs + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state=StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state=StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr( + re.compile(r"child:.+|child1:") + ): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": { + "grandchild_1": { + "my_key": "hi my value here" + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # resume + assert [c async for c in app.astream(None, config, subgraphs=True)] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_2": {"my_key": "hi my value here and there"}}, + ), + ( + (AnyStr("child:"),), + {"child_1": {"my_key": "hi my value here and there"}}, + ), + ((), {"child": {"my_key": "hi my value here and there"}}), + ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), + ] + # get state with and without subgraphs + assert ( + await app.aget_state(config) + == await app.aget_state(config, subgraphs=True) + == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + ) + # get outer graph history + outer_history = [c async for c in app.aget_state_history(config)] + assert ( + outer_history[0] + == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ][0] + ) + # get child graph history + child_history = [ + c async for c in app.aget_state_history(outer_history[2].tasks[0].state) + ] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + } + }, + ), + ), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get grandchild graph history + grandchild_history = [ + c async for c in app.aget_state_history(child_history[1].tasks[0].state) + ] + assert grandchild_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} + }, + "step": 2, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value here"}, + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("grandchild_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # replay grandchild checkpoint + assert [ + c + async for c in app.astream( + None, grandchild_history[2].config, subgraphs=True + ) + ] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ) + ] + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_send_to_nested_graphs(checkpointer_name: str) -> None: + class OverallState(TypedDict): + subjects: list[str] + jokes: Annotated[list[str], operator.add] + + async def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + + class JokeState(TypedDict): + subject: str + + async def edit(state: JokeState): + subject = state["subject"] + return {"subject": f"{subject} - hohoho"} + + # subgraph + subgraph = StateGraph(input=JokeState, output=OverallState) + subgraph.add_node("edit", edit) + subgraph.add_node( + "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} + ) + subgraph.set_entry_point("edit") + subgraph.add_edge("edit", "generate") + subgraph.set_finish_point("generate") + + # parent graph + builder = StateGraph(OverallState) + builder.add_node( + "generate_joke", + subgraph.compile(interrupt_before=["generate"]), + ) + builder.add_conditional_edges(START, continue_to_jokes) + builder.add_edge("generate_joke", END) + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + tracer = FakeTracer() + + # invoke and pause at nested interrupt + assert await graph.ainvoke( + {"subjects": ["cats", "dogs"]}, config={**config, "callbacks": [tracer]} + ) == { + "subjects": ["cats", "dogs"], + "jokes": [], + } + assert len(tracer.runs) == 1, "Should produce exactly 1 root run" + + # check state + outer_state = await graph.aget_state(config) + assert outer_state == StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + ), + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + + # update state of dogs joke graph + await graph.aupdate_state( + outer_state.tasks[1].state, {"subject": "turtles - hohoho"} + ) + + # continue past interrupt + assert await graph.ainvoke(None, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], + } + + actual_snapshot = await graph.aget_state(config) + expected_snapshot = StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + assert actual_snapshot == expected_snapshot + + # test full history + actual_history = [c async for c in graph.aget_state_history(config)] + expected_history = [ + StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": [ + "Joke about cats - hohoho", + "Joke about turtles - hohoho", + ], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + ), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"jokes": []}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + + +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Python 3.11+ is required for async contextvars support", +) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_weather_subgraph( + checkpointer_name: str, snapshot: SnapshotAssertion +) -> None: + from langchain_core.language_models.fake_chat_models import ( + FakeMessagesListChatModel, + ) + from langchain_core.messages import AIMessage, ToolCall + from langchain_core.tools import tool + + from langgraph.graph import MessagesState + + # setup subgraph + + @tool + def get_weather(city: str): + """Get the weather for a specific city""" + return f"I'ts sunny in {city}!" + + weather_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="get_weather", + args={"city": "San Francisco"}, + ) + ], + ) + ] + ) + + class SubGraphState(MessagesState): + city: str + + def model_node(state: SubGraphState): + result = weather_model.invoke(state["messages"]) + return {"city": cast(AIMessage, result).tool_calls[0]["args"]["city"]} + + def weather_node(state: SubGraphState): + result = get_weather.invoke({"city": state["city"]}) + return {"messages": [{"role": "assistant", "content": result}]} + + subgraph = StateGraph(SubGraphState) + subgraph.add_node(model_node) + subgraph.add_node(weather_node) + subgraph.add_edge(START, "model_node") + subgraph.add_edge("model_node", "weather_node") + subgraph.add_edge("weather_node", END) + subgraph = subgraph.compile(interrupt_before=["weather_node"]) + + # setup main graph + + class RouterState(MessagesState): + route: Literal["weather", "other"] + + class Router(TypedDict): + route: Literal["weather", "other"] + + router_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="router", + args={"dest": "weather"}, + ) + ], + ) + ] + ) + + def router_node(state: RouterState): + system_message = "Classify the incoming query as either about weather or not." + messages = [{"role": "system", "content": system_message}] + state["messages"] + route = router_model.invoke(messages) + return {"route": cast(AIMessage, route).tool_calls[0]["args"]["dest"]} + + def normal_llm_node(state: RouterState): + return {"messages": [AIMessage("Hello!")]} + + def route_after_prediction(state: RouterState): + if state["route"] == "weather": + return "weather_graph" + else: + return "normal_llm_node" + + def weather_graph(state: RouterState): + # this tests that all async checkpointers tested also implement sync methods + # as the subgraph called with sync invoke will use sync checkpointer methods + return subgraph.invoke(state) + + graph = StateGraph(RouterState) + graph.add_node(router_node) + graph.add_node(normal_llm_node) + graph.add_node("weather_graph", weather_graph) + graph.add_edge(START, "router_node") + graph.add_conditional_edges("router_node", route_after_prediction) + graph.add_edge("normal_llm_node", END) + graph.add_edge("weather_graph", END) + + def get_first_in_list(): + return [*graph.get_state_history(config, limit=1)][0] + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = graph.compile(checkpointer=checkpointer) + + assert graph.get_graph(xray=1).draw_mermaid() == snapshot + + config = {"configurable": {"thread_id": "1"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + + # run until interrupt + assert [ + c + async for c in graph.astream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + + # check current state + state = await graph.aget_state(config) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("weather_graph:"), + } + }, + ), + ), + ) + # confirm that list() delegates to alist() correctly + assert await asyncio.to_thread(get_first_in_list) == state + + # update + await graph.aupdate_state(state.tasks[0].state, {"city": "la"}) + + # run after update + assert [ + c + async for c in graph.astream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (AnyStr("weather_graph:"),), + { + "weather_node": { + "messages": [ + {"role": "assistant", "content": "I'ts sunny in la!"} + ] + } + }, + ), + ( + (), + { + "weather_graph": { + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="I'ts sunny in la!"), + ] + } + }, + ), + ] + + # try updating acting as weather node + config = {"configurable": {"thread_id": "14"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + assert [ + c + async for c in graph.astream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + state = await graph.aget_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf") + ], + "city": "San Francisco", + }, + next=("weather_node",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"model_node": {"city": "San Francisco"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="weather_node"),), + ), + ), + ), + ) + await graph.aupdate_state( + state.tasks[0].state.config, + {"messages": [{"role": "assistant", "content": "rainy"}]}, + as_node="weather_node", + ) + state = await graph.aget_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [_AnyIdHumanMessage(content="what's the weather in sf")], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="rainy"), + ], + "city": "San Francisco", + }, + next=(), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "update", + "step": 2, + "writes": { + "weather_node": { + "messages": [ + {"role": "assistant", "content": "rainy"} + ] + } + }, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + ), + ), + ) + assert [ + c + async for c in graph.astream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (), + { + "weather_graph": { + "messages": [ + _AnyIdHumanMessage(content="what's the weather in sf"), + _AnyIdAIMessage(content="rainy"), + ] + } + }, + ), + ] async def test_checkpoint_metadata() -> None: @@ -7903,3 +9268,26 @@ async def test_checkpoint_metadata() -> None: assert chkpnt_tuple.metadata["thread_id"] == "2" assert chkpnt_tuple.metadata["test_config_3"] == "foo" assert chkpnt_tuple.metadata["test_config_4"] == "bar" + + +async def test_checkpointer_null_pending_writes() -> None: + class Node: + def __init__(self, name: str): + self.name = name + setattr(self, "__name__", name) + + def __call__(self, state): + return [self.name] + + builder = StateGraph(Annotated[list, operator.add]) + builder.add_node(Node("1")) + builder.add_edge(START, "1") + graph = builder.compile(checkpointer=MemorySaverNoPending()) + assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] + assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2 + assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ + "1" + ] * 3 + assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ + "1" + ] * 4 diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index ecf35eb2a..0234c5e2f 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -1,11 +1,15 @@ +import inspect +import warnings +from dataclasses import dataclass, field from typing import Annotated as Annotated2 -from typing import Any +from typing import Any, Optional import pytest +from langchain_core.runnables import RunnableConfig from pydantic.v1 import BaseModel -from typing_extensions import Annotated, TypedDict +from typing_extensions import Annotated, NotRequired, Required, TypedDict -from langgraph.graph.state import _warn_invalid_state_schema +from langgraph.graph.state import StateGraph, _warn_invalid_state_schema class State(BaseModel): @@ -44,5 +48,135 @@ def test_warns_invalid_schema(schema: Any): ) def test_doesnt_warn_valid_schema(schema: Any): # Assert the function does not raise a warning - with pytest.warns(None): + with warnings.catch_warnings(): + warnings.simplefilter("error") _warn_invalid_state_schema(schema) + + +def test_state_schema_with_type_hint(): + class InputState(TypedDict): + question: str + + class OutputState(TypedDict): + input_state: InputState + + def complete_hint(state: InputState) -> OutputState: + return {"input_state": state} + + def miss_first_hint(state, config: RunnableConfig) -> OutputState: + return {"input_state": state} + + def only_return_hint(state, config) -> OutputState: + return {"input_state": state} + + def miss_all_hint(state, config): + return {"input_state": state} + + graph = StateGraph(input=InputState, output=OutputState) + actions = [complete_hint, miss_first_hint, only_return_hint, miss_all_hint] + + for action in actions: + graph.add_node(action) + + graph.set_entry_point(actions[0].__name__) + for i in range(len(actions) - 1): + graph.add_edge(actions[i].__name__, actions[i + 1].__name__) + graph.set_finish_point(actions[-1].__name__) + + graph = graph.compile() + + input_state = InputState(question="Hello World!") + output_state = OutputState(input_state=input_state) + for i, c in enumerate(graph.stream(input_state, stream_mode="updates")): + node_name = actions[i].__name__ + assert c[node_name] == output_state + + +@pytest.mark.parametrize("total_", [True, False]) +def test_state_schema_optional_values(total_: bool): + class SomeParentState(TypedDict): + val0a: str + val0b: Optional[str] + + class InputState(SomeParentState, total=total_): # type: ignore + val1: str + val2: Optional[str] + val3: Required[str] + val4: NotRequired[dict] + val5: Annotated[Required[str], "foo"] + val6: Annotated[NotRequired[str], "bar"] + + class State(InputState): # this would be ignored + val4: dict + + builder = StateGraph(State, input=InputState) + builder.add_node("n", lambda x: x) + builder.add_edge("__start__", "n") + graph = builder.compile() + model = graph.input_schema + json_schema = model.schema() + + if total_ is False: + expected_required = set() + expected_optional = {"val2", "val1"} + else: + expected_required = {"val1"} + + expected_optional = {"val2"} + + # The others should always have precedence based on the required annotation + expected_required |= {"val0a", "val3", "val5"} + expected_optional |= {"val0b", "val4", "val6"} + + assert set(json_schema.get("required", set())) == expected_required + assert ( + set(json_schema["properties"].keys()) == expected_required | expected_optional + ) + + +@pytest.mark.parametrize("kw_only_", [False, True]) +def test_state_schema_default_values(kw_only_: bool): + kwargs = {} + if "kw_only" in inspect.signature(dataclass).parameters: + kwargs = {"kw_only": kw_only_} + + @dataclass(**kwargs) + class InputState: + val1: str + val2: Optional[int] + val3: Annotated[Optional[float], "optional annotated"] + val4: Optional[str] = None + val5: list[int] = field(default_factory=lambda: [1, 2, 3]) + val6: dict[str, int] = field(default_factory=lambda: {"a": 1}) + val7: str = field(default=...) + val8: Annotated[int, "some metadata"] = 42 + val9: Annotated[str, "more metadata"] = field(default="some foo") + val10: str = "default" + val11: Annotated[list[str], "annotated list"] = field( + default_factory=lambda: ["a", "b"] + ) + + builder = StateGraph(InputState) + builder.add_node("n", lambda x: x) + builder.add_edge("__start__", "n") + graph = builder.compile() + model = graph.input_schema + json_schema = model.schema() + + expected_required = {"val1", "val7"} + expected_optional = { + "val2", + "val3", + "val4", + "val5", + "val6", + "val8", + "val9", + "val10", + "val11", + } + + assert set(json_schema.get("required", set())) == expected_required + assert ( + set(json_schema["properties"].keys()) == expected_required | expected_optional + ) diff --git a/libs/langgraph/tests/test_store.py b/libs/langgraph/tests/test_store.py new file mode 100644 index 000000000..71494adaf --- /dev/null +++ b/libs/langgraph/tests/test_store.py @@ -0,0 +1,38 @@ +import asyncio +from typing import Any, Optional + +import pytest +from pytest_mock import MockerFixture + +from langgraph.store.base import BaseStore +from langgraph.store.batch import AsyncBatchedStore + +pytestmark = pytest.mark.anyio + + +async def test_async_batch_store(mocker: MockerFixture) -> None: + aget = mocker.stub() + alist = mocker.stub() + + class MockStore(BaseStore): + async def aget( + self, pairs: list[tuple[str, str]] + ) -> dict[tuple[str, str], Optional[dict[str, Any]]]: + aget(pairs) + return {pair: 1 for pair in pairs} + + async def alist(self, prefixes: list[str]) -> dict[str, dict[str, Any]]: + alist(prefixes) + return {prefix: {prefix: 1} for prefix in prefixes} + + store = AsyncBatchedStore(MockStore()) + + # concurrent calls are batched + results = await asyncio.gather( + store.alist(["a", "b"]), + store.alist(["c", "d"]), + ) + assert results == [{"a": {"a": 1}, "b": {"b": 1}}, {"c": {"c": 1}, "d": {"d": 1}}] + assert [c.args for c in alist.call_args_list] == [ + (["a", "b", "c", "d"],), + ] diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index 1e3bf8ea1..e8ea94fff 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -1,15 +1,30 @@ import functools import sys import uuid -from typing import TypedDict +from typing import ( + Any, + Callable, + Dict, + ForwardRef, + List, + Literal, + Optional, + TypedDict, + TypeVar, + Union, +) from unittest.mock import patch import langsmith import pytest +from typing_extensions import Annotated, NotRequired, Required from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph -from langgraph.utils import is_async_callable, is_async_generator +from langgraph.utils.fields import _is_optional_type, get_field_default +from langgraph.utils.runnable import is_async_callable, is_async_generator + +pytestmark = pytest.mark.anyio def test_is_async() -> None: @@ -119,3 +134,96 @@ async def test_runnable_callable_tracing_nested_async(rt_graph: CompiledGraph) - with langsmith.tracing_context(enabled=True): res = await rt_graph.ainvoke({"foo": 1}) assert isinstance(res["node_run_id"], uuid.UUID) + + +def test_is_optional_type(): + assert _is_optional_type(None) + assert not _is_optional_type(type(None)) + assert _is_optional_type(Optional[list]) + assert not _is_optional_type(int) + assert _is_optional_type(Optional[Literal[1, 2, 3]]) + assert not _is_optional_type(Literal[1, 2, 3]) + assert _is_optional_type(Optional[List[int]]) + assert _is_optional_type(Optional[Dict[str, int]]) + assert not _is_optional_type(List[Optional[int]]) + assert _is_optional_type(Union[Optional[str], Optional[int]]) + assert _is_optional_type( + Union[ + Union[Optional[str], Optional[int]], Union[Optional[float], Optional[dict]] + ] + ) + assert not _is_optional_type(Union[Union[str, int], Union[float, dict]]) + + assert _is_optional_type(Union[int, None]) + assert _is_optional_type(Union[str, None, int]) + assert _is_optional_type(Union[None, str, int]) + assert not _is_optional_type(Union[int, str]) + + assert not _is_optional_type(Any) # Do we actually want this? + assert _is_optional_type(Optional[Any]) + + class MyClass: + pass + + assert _is_optional_type(Optional[MyClass]) + assert not _is_optional_type(MyClass) + assert _is_optional_type(Optional[ForwardRef("MyClass")]) + assert not _is_optional_type(ForwardRef("MyClass")) + + assert _is_optional_type(Optional[Union[List[int], Dict[str, Optional[int]]]]) + assert not _is_optional_type(Union[List[int], Dict[str, Optional[int]]]) + + assert _is_optional_type(Optional[Callable[[int], str]]) + assert not _is_optional_type(Callable[[int], Optional[str]]) + + T = TypeVar("T") + assert _is_optional_type(Optional[T]) + assert not _is_optional_type(T) + + U = TypeVar("U", bound=Optional[T]) # type: ignore + assert _is_optional_type(U) + + +def test_is_required(): + class MyBaseTypedDict(TypedDict): + val_1: Required[Optional[str]] + val_2: Required[str] + val_3: NotRequired[str] + val_4: NotRequired[Optional[str]] + val_5: Annotated[NotRequired[int], "foo"] + val_6: NotRequired[Annotated[int, "foo"]] + val_7: Annotated[Required[int], "foo"] + val_8: Required[Annotated[int, "foo"]] + val_9: Optional[str] + val_10: str + + annos = MyBaseTypedDict.__annotations__ + assert get_field_default("val_1", annos["val_1"], MyBaseTypedDict) == ... + assert get_field_default("val_2", annos["val_2"], MyBaseTypedDict) == ... + assert get_field_default("val_3", annos["val_3"], MyBaseTypedDict) is None + assert get_field_default("val_4", annos["val_4"], MyBaseTypedDict) is None + # See https://peps.python.org/pep-0655/#interaction-with-annotated + assert get_field_default("val_5", annos["val_5"], MyBaseTypedDict) is None + assert get_field_default("val_6", annos["val_6"], MyBaseTypedDict) is None + assert get_field_default("val_7", annos["val_7"], MyBaseTypedDict) == ... + assert get_field_default("val_8", annos["val_8"], MyBaseTypedDict) == ... + assert get_field_default("val_9", annos["val_9"], MyBaseTypedDict) is None + assert get_field_default("val_10", annos["val_10"], MyBaseTypedDict) == ... + + class MyChildDict(MyBaseTypedDict): + val_11: int + val_11b: Optional[int] + val_11c: Union[int, None, str] + + class MyGrandChildDict(MyChildDict, total=False): + val_12: int + val_13: Required[str] + + cannos = MyChildDict.__annotations__ + gcannos = MyGrandChildDict.__annotations__ + assert get_field_default("val_11", cannos["val_11"], MyChildDict) == ... + assert get_field_default("val_11b", cannos["val_11b"], MyChildDict) is None + assert get_field_default("val_11c", cannos["val_11c"], MyChildDict) is None + assert get_field_default("val_12", gcannos["val_12"], MyGrandChildDict) is None + assert get_field_default("val_9", gcannos["val_9"], MyGrandChildDict) is None + assert get_field_default("val_13", gcannos["val_13"], MyGrandChildDict) == ... diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 300578131..8c87ebdb7 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.4", + "version": "0.0.8", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index bf98dccb4..459e9e6b1 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -24,6 +24,7 @@ import { interface ClientConfig { apiUrl?: string; + apiKey?: string; callerOptions?: AsyncCallerParams; timeoutMs?: number; defaultHeaders?: Record; @@ -48,6 +49,9 @@ class BaseClient { this.timeoutMs = config?.timeoutMs || 12_000; this.apiUrl = config?.apiUrl || "http://localhost:8123"; this.defaultHeaders = config?.defaultHeaders || {}; + if (config?.apiKey != null) { + this.defaultHeaders["X-Api-Key"] = config.apiKey; + } } protected prepareFetchOptions( @@ -129,6 +133,7 @@ export class CronsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/threads/${threadId}/runs/crons`, { method: "POST", @@ -155,6 +160,7 @@ export class CronsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/runs/crons`, { method: "POST", @@ -233,6 +239,8 @@ export class AssistantsClient extends BaseClient { graphId: string; config?: Config; metadata?: Metadata; + assistantId?: string; + ifExists?: OnConflictBehavior; }): Promise { return this.fetch("/assistants", { method: "POST", @@ -240,6 +248,8 @@ export class AssistantsClient extends BaseClient { graph_id: payload.graphId, config: payload.config, metadata: payload.metadata, + assistant_id: payload.assistantId, + if_exists: payload.ifExists, }, }); } @@ -253,7 +263,7 @@ export class AssistantsClient extends BaseClient { async update( assistantId: string, payload: { - graphId: string; + graphId?: string; config?: Config; metadata?: Metadata; }, @@ -514,7 +524,7 @@ export class RunsClient extends BaseClient { stream( threadId: null, assistantId: string, - payload?: Omit, + payload?: Omit, ): AsyncGenerator<{ event: StreamEvent; data: any; @@ -542,8 +552,6 @@ export class RunsClient extends BaseClient { payload?: RunsStreamPayload, ): AsyncGenerator<{ event: StreamEvent; - // TODO: figure out a better way to - // type this without any data: any; }> { const json: Record = { @@ -555,10 +563,12 @@ export class RunsClient extends BaseClient { assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, + checkpoint_id: payload?.checkpointId, + webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, + on_completion: payload?.onCompletion, + on_disconnect: payload?.onDisconnect, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } const endpoint = threadId == null ? `/runs/stream` : `/threads/${threadId}/runs/stream`; @@ -571,6 +581,7 @@ export class RunsClient extends BaseClient { ); let parser: EventSourceParser; + let onEndEvent: () => void; const textDecoder = new TextDecoder(); const stream: ReadableStream<{ event: string; data: any }> = ( @@ -594,9 +605,17 @@ export class RunsClient extends BaseClient { }); } }); + onEndEvent = () => { + ctrl.enqueue({ event: "end", data: undefined }); + }; }, async transform(chunk) { - parser.feed(textDecoder.decode(chunk)); + const payload = textDecoder.decode(chunk); + parser.feed(payload); + + // eventsource-parser will ignore events + // that are not terminated by a newline + if (payload.trim() === "event: end") onEndEvent(); }, }), ); @@ -625,10 +644,9 @@ export class RunsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + checkpoint_id: payload?.checkpointId, + multitask_strategy: payload?.multitaskStrategy, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } return this.fetch(`/threads/${threadId}/runs`, { method: "POST", json, @@ -639,7 +657,7 @@ export class RunsClient extends BaseClient { async wait( threadId: null, assistantId: string, - payload?: Omit, + payload?: Omit, ): Promise; async wait( @@ -668,10 +686,12 @@ export class RunsClient extends BaseClient { assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, + checkpoint_id: payload?.checkpointId, + webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, + on_completion: payload?.onCompletion, + on_disconnect: payload?.onDisconnect, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } const endpoint = threadId == null ? `/runs/wait` : `/threads/${threadId}/runs/wait`; return this.fetch(endpoint, { diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts index 897ed1ed8..1d772b7eb 100644 --- a/libs/sdk-js/src/schema.ts +++ b/libs/sdk-js/src/schema.ts @@ -2,6 +2,16 @@ import type { JSONSchema7 } from "json-schema"; type Optional = T | null | undefined; +type RunStatus = + | "pending" + | "running" + | "error" + | "success" + | "timeout" + | "interrupted"; + +type ThreadStatus = "idle" | "busy" | "interrupted"; + export interface Config { /** * Tags for this call and any sub-calls (eg. a Chain calling an LLM). @@ -80,6 +90,7 @@ export interface Thread { created_at: string; updated_at: string; metadata: Metadata; + status: ThreadStatus; } export interface Cron { @@ -101,6 +112,9 @@ export interface ThreadState { metadata: Metadata; created_at: Optional; parent_checkpoint_id: Optional; + + config: Config; + parent_config?: Config; } export interface Run { @@ -109,12 +123,6 @@ export interface Run { assistant_id: string; created_at: string; updated_at: string; - status: - | "pending" - | "running" - | "error" - | "success" - | "timeout" - | "interrupted"; + status: RunStatus; metadata: Metadata; } diff --git a/libs/sdk-js/src/types.mts b/libs/sdk-js/src/types.mts index e7d1bdc09..2a5d6c4f1 100644 --- a/libs/sdk-js/src/types.mts +++ b/libs/sdk-js/src/types.mts @@ -3,6 +3,8 @@ import { Config, Metadata } from "./schema.js"; export type StreamMode = "values" | "messages" | "updates" | "events" | "debug"; export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue"; export type OnConflictBehavior = "raise" | "do_nothing"; +export type OnCompletionBehavior = "complete" | "continue"; +export type DisconnectMode = "cancel" | "continue"; export type StreamEvent = | "events" | "metadata" @@ -30,6 +32,11 @@ interface RunsInvokePayload { */ config?: Config; + /** + * Checkpoint ID for when creating a new run. + */ + checkpointId?: string; + /** * Interrupt execution before entering these nodes. */ @@ -56,6 +63,27 @@ interface RunsInvokePayload { * Abort controller signal to cancel the run. */ signal?: AbortController["signal"]; + + /** + * Behavior to handle run completion. Only relevant if + * there is a pending/inflight run on the same thread. One of: + * - "complete": Complete the run. + * - "continue": Continue the run. + */ + onCompletion?: OnCompletionBehavior; + + /** + * Webhook to call when the run is complete. + */ + webhook?: string; + + /** + * Behavior to handle disconnection. Only relevant if + * there is a pending/inflight run on the same thread. One of: + * - "cancel": Cancel the run. + * - "continue": Continue the run. + */ + onDisconnect?: DisconnectMode; } export interface RunsStreamPayload extends RunsInvokePayload { @@ -77,12 +105,7 @@ export interface RunsStreamPayload extends RunsInvokePayload { feedbackKeys?: string[]; } -export interface RunsCreatePayload extends RunsInvokePayload { - /** - * Webhook to call when the run is complete. - */ - webhook?: string; -} +export interface RunsCreatePayload extends RunsInvokePayload {} export interface CronsCreatePayload extends RunsCreatePayload { /** diff --git a/libs/sdk-js/yarn.lock b/libs/sdk-js/yarn.lock index 9c46e6ca6..b4e6f252a 100644 --- a/libs/sdk-js/yarn.lock +++ b/libs/sdk-js/yarn.lock @@ -1219,9 +1219,9 @@ micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: parse-entities "^2.0.0" micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" diff --git a/libs/sdk-py/Makefile b/libs/sdk-py/Makefile index 98dbab0f7..86c45abe9 100644 --- a/libs/sdk-py/Makefile +++ b/libs/sdk-py/Makefile @@ -14,11 +14,11 @@ lint format: PYTHON_FILES=. lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$') lint lint_diff: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/sdk-py/langgraph_sdk/client/async_client.py b/libs/sdk-py/langgraph_sdk/client/async_client.py index 57ea318d2..bd5e5ce63 100644 --- a/libs/sdk-py/langgraph_sdk/client/async_client.py +++ b/libs/sdk-py/langgraph_sdk/client/async_client.py @@ -18,14 +18,15 @@ import httpx_sse import orjson from httpx._types import QueryParamTypes -import langgraph_sdk from langgraph_sdk.schema import ( Assistant, Config, Cron, + DisconnectMode, GraphSchema, - Metadata, + Json, MultitaskStrategy, + OnCompletionBehavior, OnConflictBehavior, Run, RunCreate, @@ -35,13 +36,16 @@ from langgraph_sdk.schema import ( ThreadState, ThreadStatus, ) -from langgraph_sdk.utils import get_api_key +from langgraph_sdk.utils import get_headers, orjson_default logger = logging.getLogger(__name__) def get_client( - *, url: Optional[str] = None, api_key: Optional[str] = None + *, + url: Optional[str] = None, + api_key: Optional[str] = None, + headers: Optional[dict[str, str]] = None, ) -> AsyncLangGraphClient: """Get a LangGraphClient instance. @@ -53,6 +57,7 @@ def get_client( 2. LANGGRAPH_API_KEY 3. LANGSMITH_API_KEY 4. LANGCHAIN_API_KEY + headers: Optional custom headers """ transport: Optional[httpx.AsyncBaseTransport] = None if url is None: @@ -65,17 +70,11 @@ def get_client( url = "http://localhost:8123" if transport is None: transport = httpx.AsyncHTTPTransport(retries=5) - headers = { - "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", - } - api_key = get_api_key(api_key) - if api_key: - headers["x-api-key"] = api_key client = httpx.AsyncClient( base_url=url, transport=transport, timeout=httpx.Timeout(connect=5, read=60, write=60, pool=5), - headers=headers, + headers=get_headers(api_key, headers), ) return AsyncLangGraphClient(client) @@ -191,23 +190,12 @@ class AsyncHttpClient: ) -def _orjson_default(obj: Any) -> Any: - if hasattr(obj, "model_dump") and callable(obj.model_dump): - return obj.model_dump() - elif hasattr(obj, "dict") and callable(obj.dict): - return obj.dict() - elif isinstance(obj, (set, frozenset)): - return list(obj) - else: - raise TypeError(f"Object of type {type(obj)} is not JSON serializable") - - async def encode_json(json: Any) -> tuple[dict[str, str], bytes]: body = await asyncio.get_running_loop().run_in_executor( None, orjson.dumps, json, - _orjson_default, + orjson_default, orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, ) content_length = str(len(body)) @@ -405,7 +393,7 @@ class AsyncAssistantsClient: graph_id: Optional[str], config: Optional[Config] = None, *, - metadata: Metadata = None, + metadata: Json = None, assistant_id: Optional[str] = None, if_exists: Optional[OnConflictBehavior] = None, ) -> Assistant: @@ -453,7 +441,7 @@ class AsyncAssistantsClient: *, graph_id: Optional[str] = None, config: Optional[Config] = None, - metadata: Metadata = None, + metadata: Json = None, ) -> Assistant: """Update an assistant. @@ -515,7 +503,7 @@ class AsyncAssistantsClient: async def search( self, *, - metadata: Metadata = None, + metadata: Json = None, graph_id: Optional[str] = None, limit: int = 10, offset: int = 0, @@ -591,7 +579,7 @@ class AsyncThreadsClient: async def create( self, *, - metadata: Metadata = None, + metadata: Json = None, thread_id: Optional[str] = None, if_exists: Optional[OnConflictBehavior] = None, ) -> Thread: @@ -666,7 +654,8 @@ class AsyncThreadsClient: async def search( self, *, - metadata: Metadata = None, + metadata: Json = None, + values: Json = None, status: Optional[ThreadStatus] = None, limit: int = 10, offset: int = 0, @@ -699,6 +688,8 @@ class AsyncThreadsClient: } if metadata: payload["metadata"] = metadata + if values: + payload["values"] = values if status: payload["status"] = status return await self.http.post( @@ -844,15 +835,14 @@ class AsyncThreadsClient: thread_id: The ID of the thread to update. values: The values to update to the state. as_node: Update the state as if this node had just executed. - - checkpoint_id: The ID of the checkpoint to get the state of. + checkpoint_id: The ID of the checkpoint to update the state of. Returns: None Example Usage: - await client.threads.get_state( + await client.threads.update_state( thread_id="my_thread_id", values={"messages":[{"role": "user", "content": "hello!"}]}, as_node="my_node", @@ -956,6 +946,8 @@ class AsyncRunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, + webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> AsyncIterator[StreamPart]: ... @@ -973,6 +965,9 @@ class AsyncRunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, + webhook: Optional[str] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> AsyncIterator[StreamPart]: ... @@ -989,8 +984,10 @@ class AsyncRunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> AsyncIterator[StreamPart]: """Create a run and stream the results. @@ -1012,6 +1009,8 @@ class AsyncRunsClient: webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. Returns: AsyncIterator[StreamPart]: Asynchronous iterator of stream results. @@ -1054,6 +1053,8 @@ class AsyncRunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_disconnect": on_disconnect, + "on_completion": on_completion, } endpoint = ( f"/threads/{thread_id}/runs/stream" @@ -1076,6 +1077,7 @@ class AsyncRunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Run: ... @@ -1109,6 +1111,7 @@ class AsyncRunsClient: interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Run: """Create a background run. @@ -1122,9 +1125,7 @@ class AsyncRunsClient: config: The configuration for the assistant. checkpoint_id: The checkpoint to start streaming from. interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. @@ -1207,6 +1208,7 @@ class AsyncRunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_completion": on_completion, } payload = {k: v for k, v in payload.items() if v is not None} if thread_id: @@ -1235,6 +1237,8 @@ class AsyncRunsClient: checkpoint_id: Optional[str] = None, interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> Union[list[dict], dict[str, Any]]: ... @@ -1250,6 +1254,9 @@ class AsyncRunsClient: config: Optional[Config] = None, interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Union[list[dict], dict[str, Any]]: ... @@ -1265,7 +1272,9 @@ class AsyncRunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Union[list[dict], dict[str, Any]]: """Create a run, wait until it finishes and return the final state. @@ -1279,12 +1288,12 @@ class AsyncRunsClient: config: The configuration for the assistant. checkpoint_id: The checkpoint to start streaming from. interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. Returns: Union[list[dict], dict[str, Any]]: The output of the run. @@ -1344,6 +1353,8 @@ class AsyncRunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_disconnect": on_disconnect, + "on_completion": on_completion, } endpoint = ( f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" @@ -1424,8 +1435,30 @@ class AsyncRunsClient: json=None, ) - async def join(self, thread_id: str, run_id: str) -> None: - """Block until a run is done. + async def join(self, thread_id: str, run_id: str) -> dict: + """Block until a run is done. Returns the final state of the thread. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + + Returns: + None + + Example Usage: + + result =await client.runs.join( + thread_id="thread_id_to_join", + run_id="run_id_to_join" + ) + + """ # noqa: E501 + return await self.http.get(f"/threads/{thread_id}/runs/{run_id}/join") + + def join_stream(self, thread_id: str, run_id: str) -> AsyncIterator[StreamPart]: + """Stream output from a run in real-time, until the run is done. + Output is not buffered, so any output produced before this call will + not be received here. Args: thread_id: The thread ID to join. @@ -1442,7 +1475,7 @@ class AsyncRunsClient: ) """ # noqa: E501 - return await self.http.get(f"/threads/{thread_id}/runs/{run_id}/join") + return self.http.stream(f"/threads/{thread_id}/runs/{run_id}/stream", "GET") async def delete(self, thread_id: str, run_id: str) -> None: """Delete a run. diff --git a/libs/sdk-py/langgraph_sdk/client/sync_client.py b/libs/sdk-py/langgraph_sdk/client/sync_client.py index 9e867cb97..096f5ba56 100644 --- a/libs/sdk-py/langgraph_sdk/client/sync_client.py +++ b/libs/sdk-py/langgraph_sdk/client/sync_client.py @@ -34,7 +34,7 @@ from langgraph_sdk.schema import ( ThreadState, ThreadStatus, ) -from langgraph_sdk.utils import get_api_key +from langgraph_sdk.utils import get_headers, orjson_default logger = logging.getLogger(__name__) @@ -60,14 +60,11 @@ def get_client( headers = { "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", } - api_key = get_api_key(api_key) - if api_key: - headers["x-api-key"] = api_key client = httpx.Client( base_url=url, transport=transport, timeout=httpx.Timeout(connect=5, read=60, write=60, pool=5), - headers=headers, + headers=get_headers(api_key, headers), ) return LangGraphClient(client) @@ -183,21 +180,10 @@ class HttpClient: ) -def _orjson_default(obj: Any) -> Any: - if hasattr(obj, "model_dump") and callable(obj.model_dump): - return obj.model_dump() - elif hasattr(obj, "dict") and callable(obj.dict): - return obj.dict() - elif isinstance(obj, (set, frozenset)): - return list(obj) - else: - raise TypeError(f"Object of type {type(obj)} is not JSON serializable") - - def encode_json(json: Any) -> tuple[dict[str, str], bytes]: body = orjson.dumps( json, - _orjson_default, + orjson_default, orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, ) content_length = str(len(body)) diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index 7d97d4de6..4efdde30e 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Any, Literal, NamedTuple, Optional, Sequence, TypedDict, Union -Metadata = Optional[dict[str, Any]] +Json = Optional[dict[str, Any]] RunStatus = Literal["pending", "running", "error", "success", "timeout", "interrupted"] @@ -9,10 +9,14 @@ ThreadStatus = Literal["idle", "busy", "interrupted"] StreamMode = Literal["values", "messages", "updates", "events", "debug"] +DisconnectMode = Literal["cancel", "continue"] + MultitaskStrategy = Literal["reject", "interrupt", "rollback", "enqueue"] OnConflictBehavior = Literal["raise", "do_nothing"] +OnCompletionBehavior = Literal["delete", "keep"] + All = Literal["*"] @@ -66,7 +70,7 @@ class Assistant(TypedDict): """The time the assistant was created.""" updated_at: datetime """The last time the assistant was updated.""" - metadata: Metadata + metadata: Json """The assistant metadata.""" @@ -77,10 +81,12 @@ class Thread(TypedDict): """The time the thread was created.""" updated_at: datetime """The last time the thread was updated.""" - metadata: Metadata + metadata: Json """The thread metadata.""" status: ThreadStatus """The status of the thread, one of 'idle', 'busy', 'interrupted'.""" + values: Json + """The current state of the thread.""" class ThreadState(TypedDict): @@ -91,7 +97,7 @@ class ThreadState(TypedDict): received.""" checkpoint_id: str """The ID of the checkpoint.""" - metadata: Metadata + metadata: Json """Metadata for this state""" created_at: Optional[str] """Timestamp of state creation""" @@ -112,7 +118,7 @@ class Run(TypedDict): """The last time the run was updated.""" status: RunStatus """The status of the run. One of 'pending', 'running', "error", 'success', "timeout", "interrupted".""" - metadata: Metadata + metadata: Json """The run metadata.""" multitask_strategy: MultitaskStrategy """Strategy to handle concurrent runs on the same thread.""" diff --git a/libs/sdk-py/langgraph_sdk/utils.py b/libs/sdk-py/langgraph_sdk/utils.py index 254d636a5..d229ec9eb 100644 --- a/libs/sdk-py/langgraph_sdk/utils.py +++ b/libs/sdk-py/langgraph_sdk/utils.py @@ -1,8 +1,12 @@ import os -from typing import Optional +from typing import Any, Optional + +import langgraph_sdk + +RESERVED_HEADERS = ("x-api-key",) -def get_api_key(api_key: Optional[str] = None) -> Optional[str]: +def _get_api_key(api_key: Optional[str] = None) -> Optional[str]: """Get the API key from the environment. Precedence: 1. explicit argument @@ -16,3 +20,34 @@ def get_api_key(api_key: Optional[str] = None) -> Optional[str]: if env := os.getenv(f"{prefix}_API_KEY"): return env.strip().strip('"').strip("'") return None # type: ignore + + +def get_headers( + api_key: Optional[str], custom_headers: Optional[dict[str, str]] +) -> dict[str, str]: + """Combine api_key and custom user-provided headers.""" + custom_headers = custom_headers or {} + for header in RESERVED_HEADERS: + if header in custom_headers: + raise ValueError(f"Cannot set reserved header '{header}'") + + headers = { + "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", + **custom_headers, + } + api_key = _get_api_key(api_key) + if api_key: + headers["x-api-key"] = api_key + + return headers + + +def orjson_default(obj: Any) -> Any: + if hasattr(obj, "model_dump") and callable(obj.model_dump): + return obj.model_dump() + elif hasattr(obj, "dict") and callable(obj.dict): + return obj.dict() + elif isinstance(obj, (set, frozenset)): + return list(obj) + else: + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") diff --git a/libs/sdk-py/poetry.lock b/libs/sdk-py/poetry.lock index 5a17cca54..5bff56b98 100644 --- a/libs/sdk-py/poetry.lock +++ b/libs/sdk-py/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "anyio" @@ -388,28 +388,29 @@ watchdog = ">=0.6.0" [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -489,4 +490,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "dcd64c96c58d777998a9a0fee6e8f78153870796e294989a9b19d0e7ac10b2b7" +content-hash = "832acea0ad21ce71ae74edef225a1ad6f8fb166f6bf1531d876fe80fac7495f0" diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 3c50594db..378c58777 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-sdk" -version = "0.1.27" +version = "0.1.30" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT" @@ -15,7 +15,7 @@ httpx-sse = ">=0.4.0" orjson = ">=3.10.1" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1"