From f335105853266dbda96b94d146ad262086a34402 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Mon, 13 May 2024 18:32:42 -0700 Subject: [PATCH] Update Refs to clean up syntax highlighting (#443) --- .github/workflows/link_check.yml | 19 +- docs/css/mkdocstrings.css | 7 - docs/docs/reference/graphs.md | 24 +- docs/docs/reference/prebuilt.md | 6 +- docs/mkdocs.yml | 1 + .../plan-and-execute/plan-and-execute.ipynb | 18 +- langgraph/checkpoint/aiosqlite.py | 67 +++--- langgraph/checkpoint/sqlite.py | 116 ++++----- langgraph/graph/message.py | 119 +++++----- langgraph/graph/state.py | 53 ++++- langgraph/prebuilt/__init__.py | 1 + langgraph/prebuilt/chat_agent_executor.py | 220 +++++++++++++++--- langgraph/prebuilt/tool_executor.py | 44 ++-- langgraph/prebuilt/tool_node.py | 72 +++--- langgraph/pregel/__init__.py | 5 +- tests/test_jsonplus.py | 1 + 16 files changed, 493 insertions(+), 280 deletions(-) delete mode 100644 docs/css/mkdocstrings.css diff --git a/.github/workflows/link_check.yml b/.github/workflows/link_check.yml index c560293a0..f923fce4c 100644 --- a/.github/workflows/link_check.yml +++ b/.github/workflows/link_check.yml @@ -20,6 +20,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check links in Markdown files uses: gaurav-nelson/github-action-markdown-link-check@v1 with: @@ -41,19 +44,23 @@ jobs: cache-key: core - name: Install dependencies - shell: bash run: | python -m pip install --upgrade pip poetry install --with test poetry run pip install -U pytest pytest-check-links langsmith langchain GitPython + - name: Check links in notebooks env: LANGCHAIN_API_KEY: test - shell: bash run: | if [ "${{ github.event_name }}" != "schedule" ]; then - CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | grep '\.ipynb$') - poetry run pytest -o python_files=non_python_only --check-links $CHANGED_FILES --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" + git fetch origin main + CHANGED_FILES=$(git diff --name-only origin/main | grep '\.ipynb$') + if [ -n "$CHANGED_FILES" ]; then + poetry run pytest -o python_files=non_python_only --check-links --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" $CHANGED_FILES + else + echo "No notebook files changed." + fi else - poetry run pytest -o python_files=non_python_only --check-links --ignore="*.py" -k .ipynb --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" ./examples - + poetry run pytest -o python_files=non_python_only --check-links --ignore="*.py" -k .ipynb --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" ./examples + fi diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css deleted file mode 100644 index 5a30c0cc2..000000000 --- a/docs/css/mkdocstrings.css +++ /dev/null @@ -1,7 +0,0 @@ -/* https://mkdocstrings.github.io/crystal/styling.html#recommended-styles */ - -/* Indent and distinguish sub-items */ -div.doc-contents:not(.first) { - padding-left: 15px; - border-left: 4px solid rgba(230, 230, 230); -} \ No newline at end of file diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md index 8704f023b..01cfdd874 100644 --- a/docs/docs/reference/graphs.md +++ b/docs/docs/reference/graphs.md @@ -2,20 +2,26 @@ Graphs are the core abstraction of LangGraph. Each [StateGraph](#langgraph.graph.StateGraph) implementation is used to create graph workflows. Once compiled, you can run the [CompiledGraph](#compiledgraph) to run the application. -::: langgraph.graph +## StateGraph + +```python +from langgraph.graph import StateGraph +from typing_extensions import TypedDict +class MyState(TypedDict) + ... +graph = StateGraph(MyState) +``` + +::: langgraph.graph.StateGraph handler: python -## CompiledGraph - -::: langgraph.graph.graph.CompiledGraph - handler: python - - ## MessageGraph ::: langgraph.graph.message.MessageGraph -## add_messages +## CompiledGraph + +::: langgraph.graph.graph.CompiledGraph + handler: python -::: langgraph.graph.message.add_messages \ No newline at end of file diff --git a/docs/docs/reference/prebuilt.md b/docs/docs/reference/prebuilt.md index dd11d90a6..20fef193b 100644 --- a/docs/docs/reference/prebuilt.md +++ b/docs/docs/reference/prebuilt.md @@ -31,13 +31,13 @@ from langgraph.prebuilt import ToolInvocation heading_level: 4 -## `chat_agent_executor.create_tool_calling_executor` +## create_react_agent ```python -from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor +from langgraph.prebuilt import create_react_agent ``` -::: langgraph.prebuilt.chat_agent_executor.create_tool_calling_executor +::: langgraph.prebuilt.create_react_agent ## `tools_condition` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index d56e6f53d..61a383a14 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -174,6 +174,7 @@ markdown_extensions: - pymdownx.highlight: anchor_linenums: true line_spans: __span + use_pygments: true pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.keys diff --git a/examples/plan-and-execute/plan-and-execute.ipynb b/examples/plan-and-execute/plan-and-execute.ipynb index cbd691966..9ead0eac4 100644 --- a/examples/plan-and-execute/plan-and-execute.ipynb +++ b/examples/plan-and-execute/plan-and-execute.ipynb @@ -186,9 +186,7 @@ } ], "source": [ - "agent_executor.invoke(\n", - " {\"messages\": [(\"user\", \"who is the winnner of the us open\")]}\n", - ")" + "agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})" ] }, { @@ -297,7 +295,11 @@ ], "source": [ "planner.invoke(\n", - " {\"messages\": [(\"user\", \"what is the hometown of the current Australia open winner?\")]}\n", + " {\n", + " \"messages\": [\n", + " (\"user\", \"what is the hometown of the current Australia open winner?\")\n", + " ]\n", + " }\n", ")" ] }, @@ -354,7 +356,9 @@ ")\n", "\n", "\n", - "replanner = replanner_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).with_structured_output(Act)" + "replanner = replanner_prompt | ChatOpenAI(\n", + " model=\"gpt-4o\", temperature=0\n", + ").with_structured_output(Act)" ] }, { @@ -383,7 +387,9 @@ " 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({\"messages\": [(\"user\", task_formatted)]})\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", diff --git a/langgraph/checkpoint/aiosqlite.py b/langgraph/checkpoint/aiosqlite.py index 38fb1eb67..76676e64e 100644 --- a/langgraph/checkpoint/aiosqlite.py +++ b/langgraph/checkpoint/aiosqlite.py @@ -36,43 +36,40 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): Examples: Usage within a StateGraph: - - import asyncio - import aiosqlite - - from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver - from langgraph.graph import StateGraph - - builder = StateGraph(int) - builder.add_node("add_one", lambda x: x + 1) - builder.set_entry_point("add_one") - builder.set_finish_point("add_one") - - memory = AsyncSqliteSaver.from_conn_string("checkpoints.sqlite") - graph = builder.compile(checkpointer=memory) - coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}}) - asyncio.run(coro) # Output: 2 - + ```pycon + >>> import asyncio + >>> import aiosqlite + >>> + >>> from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver + >>> from langgraph.graph import StateGraph + >>> + >>> builder = StateGraph(int) + >>> builder.add_node("add_one", lambda x: x + 1) + >>> builder.set_entry_point("add_one") + >>> builder.set_finish_point("add_one") + >>> memory = AsyncSqliteSaver.from_conn_string("checkpoints.sqlite") + >>> graph = builder.compile(checkpointer=memory) + >>> coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}}) + >>> asyncio.run(coro) + Output: 2 + ``` Raw usage: - - import asyncio - import aiosqlite - from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver - - - async def main(): - async with aiosqlite.connect("checkpoints.db") as conn: - saver = AsyncSqliteSaver(conn) - config = {"configurable": {"thread_id": "1"}} - checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}} - saved_config = await saver.aput(config, checkpoint) - print( - saved_config - ) # Output: {"configurable": {"thread_id": "1", "thread_ts": "2023-05-03T10:00:00Z"}} - - - asyncio.run(main()) + ```pycon + >>> import asyncio + >>> import aiosqlite + >>> from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver + >>> + >>> async def main(): + >>> async with aiosqlite.connect("checkpoints.db") as conn: + ... saver = AsyncSqliteSaver(conn) + ... config = {"configurable": {"thread_id": "1"}} + ... checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}} + ... saved_config = await saver.aput(config, checkpoint) + ... print(saved_config) + >>> asyncio.run(main()) + {"configurable": {"thread_id": "1", "thread_ts": "2023-05-03T10:00:00Z"}} + ``` """ serde = JsonPlusSerializerCompat() diff --git a/langgraph/checkpoint/sqlite.py b/langgraph/checkpoint/sqlite.py index 84dd96a82..a34e295c0 100644 --- a/langgraph/checkpoint/sqlite.py +++ b/langgraph/checkpoint/sqlite.py @@ -27,19 +27,17 @@ class JsonPlusSerializerCompat(JsonPlusSerializer): JsonPlusSerializer behavior is used. Examples: - - import pickle - - from langgraph.checkpoint.sqlite import JsonPlusSerializerCompat - - serializer = JsonPlusSerializerCompat() - pickled_data = pickle.dumps({"key": "value"}) - loaded_data = serializer.loads(pickled_data) - print(loaded_data) # Output: {"key": "value"} - - json_data = '{"key": "value"}'.encode("utf-8") - loaded_data = serializer.loads(json_data) - print(loaded_data) # Output: {"key": "value"} + >>> import pickle + >>> from langgraph.checkpoint.sqlite import JsonPlusSerializerCompat + >>> + >>> serializer = JsonPlusSerializerCompat() + >>> pickled_data = pickle.dumps({"key": "value"}) + >>> loaded_data = serializer.loads(pickled_data) + >>> print(loaded_data) # Output: {"key": "value"} + >>> + >>> json_data = '{"key": "value"}'.encode("utf-8") + >>> loaded_data = serializer.loads(json_data) + >>> print(loaded_data) # Output: {"key": "value"} """ def loads(self, data: bytes) -> Any: @@ -64,24 +62,22 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): Examples: - import sqlite3 - - from langgraph.checkpoint.sqlite import SqliteSaver - from langgraph.graph import StateGraph - - builder = StateGraph(int) - builder.add_node("add_one", lambda x: x + 1) - builder.set_entry_point("add_one") - builder.set_finish_point("add_one") - conn = sqlite3.connect("checkpoints.sqlite") - memory = SqliteSaver(conn) - graph = builder.compile(checkpointer=memory) - - config = {"configurable": {"thread_id": "1"}} - # checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}} - result = graph.invoke(3, config) - graph.get_state(config) - # Output: StateSnapshot(values=4, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '2024-05-04T06:32:42.235444+00:00'}}, parent_config=None) + >>> import sqlite3 + >>> from langgraph.checkpoint.sqlite import SqliteSaver + >>> from langgraph.graph import StateGraph + >>> + >>> builder = StateGraph(int) + >>> builder.add_node("add_one", lambda x: x + 1) + >>> builder.set_entry_point("add_one") + >>> builder.set_finish_point("add_one") + >>> conn = sqlite3.connect("checkpoints.sqlite") + >>> memory = SqliteSaver(conn) + >>> graph = builder.compile(checkpointer=memory) + >>> config = {"configurable": {"thread_id": "1"}} + >>> graph.get_state(config) + >>> result = graph.invoke(3, config) + >>> graph.get_state(config) + StateSnapshot(values=4, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '2024-05-04T06:32:42.235444+00:00'}}, parent_config=None) """ # noqa serde = JsonPlusSerializerCompat() @@ -203,21 +199,22 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): Examples: Basic: - - config = {"configurable": {"thread_id": "1"}} - checkpoint_tuple = memory.get_tuple(config) - print(checkpoint_tuple) # Output: CheckpointTuple(...) + >>> config = {"configurable": {"thread_id": "1"}} + >>> checkpoint_tuple = memory.get_tuple(config) + >>> print(checkpoint_tuple) + CheckpointTuple(...) With timestamp: - config = { - "configurable": { - "thread_id": "1", - "thread_ts": "2024-05-04T06:32:42.235444+00:00", - } - } - checkpoint_tuple = memory.get_tuple(config) - print(checkpoint_tuple) # Output: CheckpointTuple(...) + >>> config = { + ... "configurable": { + ... "thread_id": "1", + ... "thread_ts": "2024-05-04T06:32:42.235444+00:00", + ... } + ... } + >>> checkpoint_tuple = memory.get_tuple(config) + >>> print(checkpoint_tuple) + CheckpointTuple(...) """ # noqa with self.cursor(transaction=False) as cur: if config["configurable"].get("thread_ts"): @@ -292,14 +289,19 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): Iterator[CheckpointTuple]: An iterator of checkpoint tuples. Examples: - config = {"configurable": {"thread_id": "1"}} - checkpoints = list(memory.list(config, limit=2)) - print(checkpoints) # Output: [CheckpointTuple(...), CheckpointTuple(...)] + >>> from langgraph.checkpoint.sqlite import SqliteSaver + >>> memory = SqliteSaver.from_conn_string(":memory:") + ... # Run a graph, then list the checkpoints + >>> config = {"configurable": {"thread_id": "1"}} + >>> checkpoints = list(memory.list(config, limit=2)) + >>> print(checkpoints) + [CheckpointTuple(...), CheckpointTuple(...)] - config = {"configurable": {"thread_id": "1"}} - before = {"configurable": {"thread_ts": "2024-05-04T06:32:42.235444+00:00"}} - checkpoints = list(memory.list(config, before=before)) - print(checkpoints) # Output: [CheckpointTuple(...), ...] + >>> config = {"configurable": {"thread_id": "1"}} + >>> before = {"configurable": {"thread_ts": "2024-05-04T06:32:42.235444+00:00"}} + >>> checkpoints = list(memory.list(config, before=before)) + >>> print(checkpoints) + [CheckpointTuple(...), ...] """ query = ( "SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC" @@ -358,12 +360,14 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): Examples: - config = {"configurable": {"thread_id": "1"}} - checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "data": {"key": "value"}} - saved_config = memory.put(config, checkpoint) - print( - saved_config - ) # Output: {"configurable": {"thread_id": "1", "thread_ts": 2024-05-04T06:32:42.235444+00:00"}} + >>> from langgraph.checkpoint.sqlite import SqliteSaver + >>> memory = SqliteSaver.from_conn_string(":memory:") + ... # Run a graph, then list the checkpoints + >>> config = {"configurable": {"thread_id": "1"}} + >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "data": {"key": "value"}} + >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}) + >>> print(saved_config) + {"configurable": {"thread_id": "1", "thread_ts": 2024-05-04T06:32:42.235444+00:00"}} """ with self.lock, self.cursor() as cur: cur.execute( diff --git a/langgraph/graph/message.py b/langgraph/graph/message.py index 0b4db9152..6e2a1729e 100644 --- a/langgraph/graph/message.py +++ b/langgraph/graph/message.py @@ -30,35 +30,33 @@ def add_messages(left: Messages, right: Messages) -> Messages: message from `right` will replace the message from `left`. Examples: + ```pycon + >>> from langchain_core.messages import AIMessage, HumanMessage + >>> msgs1 = [HumanMessage(content="Hello", id="1")] + >>> msgs2 = [AIMessage(content="Hi there!", id="2")] + >>> add_messages(msgs1, msgs2) + [HumanMessage(content='Hello', id='1'), AIMessage(content='Hi there!', id='2')] - msgs1 = [HumanMessage(content="Hello", id="1")] - msgs2 = [AIMessage(content="Hi there!", id="2")] - add_messages(msgs1, msgs2) - # [HumanMessage(content="Hello", id="1"), AIMessage(content="Hi there!", id="2")] + >>> msgs1 = [HumanMessage(content="Hello", id="1")] + >>> msgs2 = [HumanMessage(content="Hello again", id="1")] + >>> add_messages(msgs1, msgs2) + [HumanMessage(content='Hello again', id='1')] - - msgs1 = [HumanMessage(content="Hello", id="1")] - msgs2 = [HumanMessage(content="Hello again", id="1")] - add_messages(msgs1, msgs2) - # [HumanMessage(content="Hello again", id="1")] - - - from typing import Annotated - from typing_extensions import TypedDict - from langgraph.graph import StateGraph - - - class State(TypedDict): - messages: Annotated[list, add_messages] - - - builder = StateGraph(State) - builder.add_node("chatbot", lambda state: {"messages": [("assistant", "Hello")]}) - builder.set_entry_point("chatbot") - builder.set_finish_point("chatbot") - graph = builder.compile() - graph.invoke({}) - # {'messages': [AIMessage(content='Hello', id='f657fb65-b6af-4790-a5b5-1d266a2ed26e')]} + >>> from typing import Annotated + >>> from typing_extensions import TypedDict + >>> from langgraph.graph import StateGraph + >>> + >>> class State(TypedDict): + ... messages: Annotated[list, add_messages] + ... + >>> builder = StateGraph(State) + >>> builder.add_node("chatbot", lambda state: {"messages": [("assistant", "Hello")]}) + >>> builder.set_entry_point("chatbot") + >>> builder.set_finish_point("chatbot") + >>> graph = builder.compile() + >>> graph.invoke({}) + {'messages': [AIMessage(content='Hello', id=...)]} + ``` """ # coerce to list @@ -96,42 +94,41 @@ class MessageGraph(StateGraph): into the existing list of messages in the graph's state. Examples: - - from langgraph.graph.message import MessageGraph - - builder = MessageGraph() - builder.add_node("chatbot", lambda state: [("assistant", "Hello!")]) - builder.set_entry_point("chatbot") - builder.set_finish_point("chatbot") - builder.compile().invoke([("user", "Hi there.")]) - # {'messages': [HumanMessage(content="Hi there.", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'), - # AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8')]} + ```pycon + >>> from langgraph.graph.message import MessageGraph + ... + >>> builder = MessageGraph() + >>> builder.add_node("chatbot", lambda state: [("assistant", "Hello!")]) + >>> builder.set_entry_point("chatbot") + >>> builder.set_finish_point("chatbot") + >>> builder.compile().invoke([("user", "Hi there.")]) + [HumanMessage(content="Hi there.", id='...'), AIMessage(content="Hello!", id='...')] - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage - - from langgraph.graph.message import MessageGraph - - builder = MessageGraph() - builder.add_node( - "chatbot", - lambda state: [ - AIMessage( - content="Hello!", - tool_calls=[{"name": "search", "id": "123", "args": {"query": "X"}}], - ) - ], - ) - builder.add_node( - "search", lambda state: [ToolMessage(content="Searching...", tool_call_id="123")] - ) - builder.set_entry_point("chatbot") - builder.add_edge("chatbot", "search") - builder.set_finish_point("search") - builder.compile().invoke([HumanMessage(content="Hi there. Can you search for X?")]) - # {'messages': [HumanMessage(content="Hi there. Can you search for X?", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'), - # AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8'), - # ToolMessage(content="Searching...", id='d8f4f4d9-c1d8-4f4d-b8b7-d8f4f4d9c1d8', tool_call_id="123")]} + >>> from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + >>> from langgraph.graph.message import MessageGraph + ... + >>> builder = MessageGraph() + >>> builder.add_node( + ... "chatbot", + ... lambda state: [ + ... AIMessage( + ... content="Hello!", + ... tool_calls=[{"name": "search", "id": "123", "args": {"query": "X"}}], + ... ) + ... ], + ... ) + >>> builder.add_node( + ... "search", lambda state: [ToolMessage(content="Searching...", tool_call_id="123")] + ... ) + >>> builder.set_entry_point("chatbot") + >>> builder.add_edge("chatbot", "search") + >>> builder.set_finish_point("search") + >>> builder.compile().invoke([HumanMessage(content="Hi there. Can you search for X?")]) + {'messages': [HumanMessage(content="Hi there. Can you search for X?", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'), + AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8'), + ToolMessage(content="Searching...", id='d8f4f4d9-c1d8-4f4d-b8b7-d8f4f4d9c1d8', tool_call_id="123")]} + ``` """ def __init__(self) -> None: diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index 9d121159d..19b60b25b 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -33,7 +33,49 @@ class StateGraph(Graph): Each state key can optionally be annotated with a reducer function that will be used to aggregate the values of that key received from multiple nodes. The signature of a reducer function is (Value, Value) -> Value. - """ + + Args: + state_schema (Type[Any]): The schema class that defines the state. + config_schema (Optional[Type[Any]]): The schema class that defines the configuration. + Use this to expose configurable parameters in your API. + + + Examples: + >>> from langchain_core.runnables import RunnableConfig + >>> from typing_extensions import Annotated, TypedDict + >>> from langgraph.checkpoint import MemorySaver + >>> from langgraph.graph import StateGraph + >>> + >>> def reducer(a: list, b: int | None) -> int: + ... if b is not None: + ... return a + [b] + ... return a + >>> + >>> class State(TypedDict): + ... x: Annotated[list, reducer] + >>> + >>> class ConfigSchema(TypedDict): + ... r: float + >>> + >>> graph = StateGraph(State, config_schema=ConfigSchema) + >>> + >>> def node(state: State, config: RunnableConfig) -> dict: + ... r = config["configurable"].get("r", 1.0) + ... x = state["x"][-1] + ... next_value = x * r * (1 - x) + ... return {"x": next_value} + >>> + >>> graph.add_node("A", node) + >>> graph.set_entry_point("A") + >>> graph.set_finish_point("A") + >>> compiled = graph.compile() + >>> + >>> print(compiled.config_specs) + [ConfigurableFieldSpec(id='r', annotation=, name=None, description=None, default=None, is_shared=False, dependencies=None)] + >>> + >>> step1 = compiled.invoke({"x": 0.5}, {"configurable": {"r": 3.0}}) + >>> print(step1) + {'x': [0.5, 0.75]}""" def __init__( self, state_schema: Type[Any], config_schema: Optional[Type[Any]] = None @@ -133,10 +175,11 @@ class StateGraph(Graph): # validate the graph self.validate( - interrupt=(interrupt_before if interrupt_before != "*" else []) - + interrupt_after - if interrupt_after != "*" - else [] + interrupt=( + (interrupt_before if interrupt_before != "*" else []) + interrupt_after + if interrupt_after != "*" + else [] + ) ) # prepare output channels diff --git a/langgraph/prebuilt/__init__.py b/langgraph/prebuilt/__init__.py index 1a4629b60..48f20d2af 100644 --- a/langgraph/prebuilt/__init__.py +++ b/langgraph/prebuilt/__init__.py @@ -1,3 +1,4 @@ +"""langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools.""" from langgraph.prebuilt import chat_agent_executor from langgraph.prebuilt.agent_executor import create_agent_executor from langgraph.prebuilt.chat_agent_executor import create_react_agent diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index 13ccb1f72..f87d7ad24 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -41,22 +41,23 @@ def create_function_calling_executor( """Creates a graph that works with a chat model that utilizes function calling. Examples: - - # Since this is deprecated, you should use `create_react_agent` instead. - # Example usage: - from langgraph.prebuilt import create_react_agent - from langchain_openai import ChatOpenAI - from langchain_community.tools.tavily_search import TavilySearchResults - - tools = [TavilySearchResults(max_results=1)] - model = ChatOpenAI() - - app = create_react_agent(model, tools) - - inputs = {"messages": [("user", "what is the weather in sf")]} - for s in app.stream(inputs): - print(list(s.values())[0]) - print("----") + ```pycon + >>> # Since this is deprecated, you should use `create_react_agent` instead. + >>> # Example usage: + >>> from langgraph.prebuilt import create_react_agent + >>> from langchain_openai import ChatOpenAI + >>> from langchain_community.tools.tavily_search import TavilySearchResults + >>> + >>> tools = [TavilySearchResults(max_results=1)] + >>> model = ChatOpenAI() + >>> + >>> app = create_react_agent(model, tools) + >>> + >>> inputs = {"messages": [("user", "what is the weather in sf")]} + >>> for s in app.stream(inputs): + ... print(list(s.values())[0]) + ... print("----") + ``` """ if isinstance(tools, ToolExecutor): tool_executor = tools @@ -176,40 +177,191 @@ def create_react_agent( """Creates a graph that works with a chat model that utilizes tool calling. Args: - model (LanguageModelLike): The chat model that supports OpenAI tool calling. - tools (Union[ToolExecutor, Sequence[BaseTool]]): A list of tools or a ToolExecutor instance. - messages_modifier: (Optional[Union[SystemMessage, str, Callable, Runnable]]): An optional + model: The chat model that supports OpenAI tool calling. + tools: A list of tools or a ToolExecutor instance. + messages_modifier: An optional messages modifier. This applies to messages BEFORE they are passed into the LLM. Can take a few different forms: - SystemMessage: this is added to the beginning of the list of messages. - str: This is converted to a SystemMessage and added to the beginning of the list of messages. - Callable: This function should take in a list of messages and the output is then passed to the language model. - Runnable: This runnable should take in a list of messages and the output is then passed to the language model. - checkpointer (Optional[BaseCheckpointSaver]): An optional checkpoint saver object. - interrupt_before (Optional[Sequence[str]]): An optional list of node names to interrupt before. - interrupt_after (Optional[Sequence[str]]): An optional list of node names to interrupt after. - debug (bool): A flag indicating whether to enable debug mode. + checkpointer: An optional checkpoint saver object. This is useful for persisting + the state of the graph (e.g., as chat memory). + interrupt_before: An optional list of node names to interrupt before. + Should be one of the following: "agent", "action". + This is useful if you want to add a user confirmation or other interrupt before taking an action. + interrupt_after: An optional list of node names to interrupt after. + Should be one of the following: "agent", "action". + This is useful if you want to return directly or run additional processing on an output. + debug: A flag indicating whether to enable debug mode. Returns: - Runnable: A compiled LangChain runnable that can be used for chat interactions. + A compiled LangChain runnable that can be used for chat interactions. Examples: + Use with a simple tool: - from langchain_community.tools.tavily_search import TavilySearchResults - from langchain_openai import ChatOpenAI + ```pycon + >>> from datetime import datetime + >>> from langchain_core.tools import tool + >>> from langchain_openai import ChatOpenAI + >>> from langgraph.prebuilt import create_react_agent + >>> + >>> @tool + ... def check_weather(location: str, at_time: datetime | None = None) -> float: + ... '''Return the weather forecast for the specified location.''' + ... return f"It's always sunny in {location}" + >>> + >>> tools = [check_weather] + >>> model = ChatOpenAI(model="gpt-4o") + >>> graph = create_react_agent(model, tools=tools) + >>> inputs = {"messages": [("user", "what is the weather in sf")]} + >>> for s in graph.stream(inputs, stream_mode="values"): + ... message = s["messages"][-1] + ... if isinstance(message, tuple): + ... print(message) + ... else: + ... message.pretty_print() + ('user', 'what is the weather in sf') + ================================== Ai Message ================================== + Tool Calls: + check_weather (call_LUzFvKJRuaWQPeXvBOzwhQOu) + Call ID: call_LUzFvKJRuaWQPeXvBOzwhQOu + Args: + location: San Francisco + ================================= Tool Message ================================= + Name: check_weather + It's always sunny in San Francisco + ================================== Ai Message ================================== + The weather in San Francisco is sunny. + ``` + Add a system prompt for the LLM: - from langgraph.prebuilt import chat_agent_executor + ```pycon + >>> system_prompt = "You are a helpful bot named Fred." + >>> graph = create_react_agent(model, tools, messages_modifier=system_prompt) + >>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]} + >>> for s in graph.stream(inputs, stream_mode="values"): + ... message = s["messages"][-1] + ... if isinstance(message, tuple): + ... print(message) + ... else: + ... message.pretty_print() + ('user', "What's your name? And what's the weather in SF?") + ================================== Ai Message ================================== + Hi, my name is Fred. Let me check the weather in San Francisco for you. + Tool Calls: + check_weather (call_lqhj4O0hXYkW9eknB4S41EXk) + Call ID: call_lqhj4O0hXYkW9eknB4S41EXk + Args: + location: San Francisco + ================================= Tool Message ================================= + Name: check_weather + It's always sunny in San Francisco + ================================== Ai Message ================================== + The weather in San Francisco is currently sunny. If you need any more details or have other questions, feel free to ask! + ``` - tools = [TavilySearchResults(max_results=1)] - model = ChatOpenAI() + Add a more complex prompt for the LLM: - app = chat_agent_executor.create_tool_calling_executor(model, tools) + ```pycon + >>> from langchain_core.prompts import ChatPromptTemplate + >>> prompt = ChatPromptTemplate.from_messages([ + ... ("system", "You are a helpful bot named Fred."), + ... ("placeholder", "{messages}"), + ... ("user", "Remember, always be polite!"), + ... ]) + >>> def modify_messages(messages: list): + ... # You can do more complex modifications here + ... return prompt.invoke(messages=messages) + >>> + >>> app = create_react_agent(model, tools, messages_modifier=modify_messages) + >>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]} + >>> for s in graph.stream(inputs, stream_mode="values"): + ... message = s["messages"][-1] + ... if isinstance(message, tuple): + ... print(message) + ... else: + ... message.pretty_print() + ``` - inputs = {"messages": [("user", "what is the weather in sf")]} - for s in app.stream(inputs): - print(list(s.values())[0]) - print("----") + Add "chat memory" to the graph: + + ```pycon + >>> from langgraph.checkpoint import MemorySaver + >>> graph = create_react_agent(model, tools, checkpointer=MemorySaver()) + >>> config = {"configurable": {"thread_id": "thread-1"}} + >>> def print_stream(graph, inputs, config): + ... for s in graph.stream(inputs, config, stream_mode="values"): + ... message = s["messages"][-1] + ... if isinstance(message, tuple): + ... print(message) + ... else: + ... message.pretty_print() + >>> inputs = {"messages": [("user", "What's the weather in SF?")]} + >>> print_stream(graph, inputs, config) + >>> inputs2 = {"messages": [("user", "Cool, so then should i go biking today?")]} + >>> print_stream(graph, inputs2, config) + ('user', "What's the weather in SF?") + ================================== Ai Message ================================== + Tool Calls: + check_weather (call_ChndaktJxpr6EMPEB5JfOFYc) + Call ID: call_ChndaktJxpr6EMPEB5JfOFYc + Args: + location: San Francisco + ================================= Tool Message ================================= + Name: check_weather + It's always sunny in San Francisco + ================================== Ai Message ================================== + The weather in San Francisco is sunny. Enjoy your day! + ================================ Human Message ================================= + Cool, so then should i go biking today? + ================================== Ai Message ================================== + Since the weather in San Francisco is sunny, it sounds like a great day for biking! Enjoy your ride! + ``` + + Add an interrupt to let the user confirm before taking an action: + + ```pycon + >>> graph = create_react_agent( + ... model, tools, interrupt_before=["action"], checkpointer=MemorySaver() + >>> ) + >>> config = {"configurable": {"thread_id": "thread-1"}} + >>> def print_stream(graph, inputs, config): + ... for s in graph.stream(inputs, config, stream_mode="values"): + ... message = s["messages"][-1] + ... if isinstance(message, tuple): + ... print(message) + ... else: + ... message.pretty_print() + + >>> inputs = {"messages": [("user", "What's the weather in SF?")]} + >>> print_stream(graph, inputs, config) + >>> snapshot = graph.get_state(config) + >>> print("Next step: ", snapshot.next) + >>> print_stream(graph, None, config) + ``` + + Add a timeout for a given step: + + ```pycon + >>> import time + >>> @tool + ... def check_weather(location: str, at_time: datetime | None = None) -> float: + ... '''Return the weather forecast for the specified location.''' + ... time.sleep(2) + ... return f"It's always sunny in {location}" + >>> + >>> tools = [check_weather] + >>> graph = create_react_agent(model, tools) + >>> graph.step_timeout = 1 # Seconds + >>> for s in graph.stream({"messages": [("user", "what is the weather in sf")]}): + ... print(s) + TimeoutError: Timed out at step 2 + ``` """ + if isinstance(tools, ToolExecutor): tool_classes = tools.tools else: diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py index 38b93bef0..8ce1c65e4 100644 --- a/langgraph/prebuilt/tool_executor.py +++ b/langgraph/prebuilt/tool_executor.py @@ -54,28 +54,30 @@ class ToolExecutor(RunnableCallable): Examples: - from langchain_core.tools import tool - from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation + >>> from langchain_core.tools import tool + >>> from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation + ... + ... + >>> @tool + ... def search(query: str) -> str: + ... \"\"\"Search engine.\"\"\" + ... return f"Searching for: {query}" + ... + ... + >>> tools = [search] + >>> executor = ToolExecutor(tools) + ... + >>> invocation = ToolInvocation(tool="search", tool_input="What is the capital of France?") + >>> result = executor.invoke(invocation) + >>> print(result) + Output: "Searching for: What is the capital of France?" - - @tool - def search(query: str) -> str: - \"\"\"Search engine.\"\"\" - return f"Searching for: {query}" - - - tools = [search] - executor = ToolExecutor(tools) - - invocation = ToolInvocation(tool="search", tool_input="What is the capital of France?") - result = executor.invoke(invocation) - print(result) # Output: "Searching for: What is the capital of France?" - - invocation = ToolInvocation( - tool="nonexistent", tool_input="What is the capital of France?" - ) - result = executor.invoke(invocation) - print(result) # Output: "nonexistent is not a valid tool, try one of [search]." + >>> invocation = ToolInvocation( + ... tool="nonexistent", tool_input="What is the capital of France?" + ... ) + >>> result = executor.invoke(invocation) + >>> print(result) + Output: "nonexistent is not a valid tool, try one of [search]." """ def __init__( diff --git a/langgraph/prebuilt/tool_node.py b/langgraph/prebuilt/tool_node.py index 89fdb2104..5f759d0b4 100644 --- a/langgraph/prebuilt/tool_node.py +++ b/langgraph/prebuilt/tool_node.py @@ -109,45 +109,45 @@ def tools_condition( "messages" key (StateGraph). Returns: - Literal["tools", "__end__"]: The next node to route to. + The next node to route to. Examples: - - from langchain_anthropic import ChatAnthropic - from langchain_core.tools import tool - - from langgraph.graph import MessageGraph - from langgraph.prebuilt import ToolNode, tools_condition - - - @tool - def divide(a: float, b: float) -> int: - \"\"\"Return a / b.\"\"\" - return a / b - - - llm = ChatAnthropic(model="claude-3-haiku-20240307") - tools = [divide] - - graph_builder = MessageGraph() - graph_builder.add_node("tools", ToolNode(tools)) - graph_builder.add_node("chatbot", llm.bind_tools(tools)) - graph_builder.add_edge("tools", "chatbot") - graph_builder.add_conditional_edges( - "chatbot", - # highlight-next-line - tools_condition, - { - # If it returns 'action', route to the 'tools' node - "action": "tools", - # If it returns '__end__', route to the end - "__end__": "__end__", - }, - ) - graph_builder.set_entry_point("chatbot") - graph = graph_builder.compile() - graph.invoke([("user", "What's 329993 divided by 13662?")]) + Create a custom ReAct-style agent with tools. + ```pycon + >>> from langchain_anthropic import ChatAnthropic + >>> from langchain_core.tools import tool + >>> + >>> from langgraph.graph import MessageGraph + >>> from langgraph.prebuilt import ToolNode, tools_condition + >>> + >>> @tool + >>> def divide(a: float, b: float) -> int: + >>> \"\"\"Return a / b.\"\"\" + >>> return a / b + >>> + >>> llm = ChatAnthropic(model="claude-3-haiku-20240307") + >>> tools = [divide] + >>> + >>> graph_builder = MessageGraph() + >>> graph_builder.add_node("tools", ToolNode(tools)) + >>> graph_builder.add_node("chatbot", llm.bind_tools(tools)) + >>> graph_builder.add_edge("tools", "chatbot") + >>> graph_builder.add_conditional_edges( + >>> "chatbot", + >>> # highlight-next-line + >>> tools_condition, + >>> { + >>> # If it returns 'action', route to the 'tools' node + >>> "action": "tools", + >>> # If it returns '__end__', route to the end + >>> "__end__": "__end__", + >>> }, + >>> ) + >>> graph_builder.set_entry_point("chatbot") + >>> graph = graph_builder.compile() + >>> graph.invoke([("user", "What's 329993 divided by 13662?")]) + ``` """ if isinstance(state, list): ai_message = state[-1] diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 597f617d5..c720d4446 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -195,9 +195,9 @@ class Pregel( auto_validate: bool = True stream_mode: StreamMode = "values" + """Mode to stream output, defaults to 'values'.""" output_channels: Union[str, Sequence[str]] - """Channels to output, defaults to channel named 'output'.""" stream_channels: Optional[Union[str, Sequence[str]]] = None """Channels to stream, defaults to all channels not in reserved channels""" @@ -209,10 +209,13 @@ class Pregel( 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) + """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.""" config_type: Optional[Type[Any]] = None diff --git a/tests/test_jsonplus.py b/tests/test_jsonplus.py index 9b32127e5..c33cfdf92 100644 --- a/tests/test_jsonplus.py +++ b/tests/test_jsonplus.py @@ -35,6 +35,7 @@ if sys.version_info < (3, 10): class MyDataclassWSlots(MyDataclass): pass + else: @dataclasses.dataclass(slots=True)