From 85f48da84ebb73adcbe0d46446ab2965e0daed87 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:44:12 -0700 Subject: [PATCH] Docs Draft (#286) --- .github/workflows/deploy_docs.yml | 3 +- Makefile | 2 +- README.md | 88 +-------- docs/_scripts/copy_notebooks.py | 66 +++++-- docs/docs/concepts.md | 4 - docs/docs/how-tos/index.md | 35 ++-- docs/docs/index.md | 150 +++++++------- docs/docs/reference/checkpoints.md | 7 - docs/docs/reference/graphs.md | 17 +- docs/docs/reference/prebuilt.md | 53 ++++- docs/docs/static/favicon.png | Bin 0 -> 5870 bytes docs/docs/static/wordmark.png | Bin 0 -> 22358 bytes docs/docs/tutorials/index.md | 96 ++++----- docs/mkdocs.yml | 152 ++++++++------- .../langgraph_code_assistant.ipynb | 2 +- examples/rag/langgraph_adaptive_rag.ipynb | 184 +----------------- langgraph/checkpoint/base.py | 4 + langgraph/graph/graph.py | 45 +++++ langgraph/graph/state.py | 47 ++++- langgraph/prebuilt/agent_executor.py | 43 +++- langgraph/prebuilt/chat_agent_executor.py | 33 +++- langgraph/pregel/__init__.py | 40 ++++ 22 files changed, 544 insertions(+), 527 deletions(-) delete mode 100644 docs/docs/concepts.md create mode 100644 docs/docs/static/favicon.png create mode 100644 docs/docs/static/wordmark.png diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 6a18593e4..179d8f845 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -1,4 +1,4 @@ -name: Deploy SDK Docs +name: Deploy Docs on: push: @@ -36,7 +36,6 @@ jobs: poetry run pip install -r docs/docs-requirements.txt - name: Build site - working-directory: ./docs run: make build-docs - name: Configure GitHub Pages diff --git a/Makefile b/Makefile index 3b36cef84..e779fc3ac 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ spell_fix: build-docs: poetry run python docs/_scripts/copy_notebooks.py - poetry run mkdocs build --clean -f docs/mkdocs.yml + poetry run mkdocs build --clean -f docs/mkdocs.yml --strict serve-docs: build-docs poetry run mkdocs serve -f docs/mkdocs.yml diff --git a/README.md b/README.md index 16f301cd3..ec0923eac 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph) [![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues) [![](https://dcbadge.vercel.app/api/server/6adMQxSpJS?compact=true&style=flat)](https://discord.com/channels/1038097195422978059/1170024642245832774) +[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/) ⚡ Building language agents as graphs ⚡ @@ -524,92 +525,7 @@ content='' additional_kwargs={'function_call': {'arguments': '{\n', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': ' "', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': '":', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': ' "', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': '"\n', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}} -content='' -content='' -content='I' -content="'m" -content=' sorry' -content=',' -content=' but' -content=' I' -content=' couldn' -content="'t" -content=' find' -content=' the' -content=' current' -content=' weather' -content=' in' -content=' San' -content=' Francisco' -content='.' -content=' However' -content=',' -content=' you' -content=' can' -content=' check' -content=' the' -content=' historical' -content=' weather' -content=' data' -content=' for' -content=' January' -content=' ' -content='202' -content='4' -content=' in' -content=' San' -content=' Francisco' -content=' [' -content='here' -content='](' -content='https' -content='://' -content='we' -content='athers' -content='park' -content='.com' -content='/h' -content='/m' -content='/' -content='557' -content='/' -content='202' -content='4' -content='/' -content='1' -content='/H' -content='istorical' -content='-' -content='Weather' -content='-in' -content='-Jan' -content='uary' -content='-' -content='202' -content='4' -content='-in' -content='-S' -content='an' -content='-F' -content='r' -content='anc' -content='isco' -content='-Cal' -content='ifornia' -content='-' -content='United' -content='-' -content='States' -content=').' -content='' +... ``` ## When to Use diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 4ebad26d9..f7c414f13 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -9,7 +9,33 @@ docs_dir = root_dir / "docs/docs" how_tos_dir = docs_dir / "how-tos" tutorials_dir = docs_dir / "tutorials" +_MANUAL = { + "how-tos": [ + "async.ipynb", + "streaming-tokens.ipynb", + "human-in-the-loop.ipynb", + "persistence.ipynb", + "time-travel.ipynb", + "visualization.ipynb", + "state-model.ipynb", + "subgraph.ipynb", + "persistence_postgres.ipynb", + "branching.ipynb", + ], + "tutorials": [ + "chat_agent_executor_with_function_calling/base.ipynb", + "chat_agent_executor_with_function_calling/high-level.ipynb", + "chat_agent_executor_with_function_calling/high-level-tools.ipynb", + "chat_agent_executor_with_function_calling/prebuilt-tool-node.ipynb", + "agent_executor/base.ipynb", + "agent_executor/high-level.ipynb", + ], +} +_MANUAL_INVERSE = {v: docs_dir / k for k, vs in _MANUAL.items() for v in vs} _HOW_TOS = {"agent_executor", "chat_agent_executor_with_function_calling", "docs"} +_MAP = { + "persistence_postgres.ipynb": "tutorial", +} _IGNORE = (".ipynb_checkpoints", ".venv", ".cache") @@ -33,30 +59,48 @@ def clean_notebooks(): def copy_notebooks(): # Nested ones are mostly tutorials rn for root, dirs, files in os.walk(examples_dir): - if root == str(examples_dir): - continue - if any(path.startswith(".") or path.startswith("__") for path in root.split(os.sep)): + if any( + path.startswith(".") or path.startswith("__") for path in root.split(os.sep) + ): continue if any(path in _HOW_TOS for path in root.split(os.sep)): dst_dir = how_tos_dir else: dst_dir = tutorials_dir for file in files: - if file.endswith(".ipynb"): + dst_dir_ = dst_dir + if file.endswith((".ipynb", ".png")): + if file in _MAP: + dst_dir = os.path.join(dst_dir, _MAP[file]) src_path = os.path.join(root, file) dst_path = os.path.join( dst_dir, os.path.relpath(src_path, examples_dir) ) + for k in _MANUAL_INVERSE: + if src_path.endswith(k): + overridden_dir = _MANUAL_INVERSE[k] + dst_path = os.path.join(overridden_dir, os.path.relpath(src_path, examples_dir)) + print(f"Overriding {src_path} to {dst_path}") + break + os.makedirs(os.path.dirname(dst_path), exist_ok=True) shutil.copy(src_path, dst_path) + # Convert all ./img/* to ../img/* + if file.endswith(".ipynb"): + with open(dst_path, "r") as f: + content = f.read() + content = content.replace("(./img/", "(../img/") + with open(dst_path, "w") as f: + f.write(content) + dst_dir = dst_dir_ # Top level notebooks are "how-to's" - for file in examples_dir.iterdir(): - if file.suffix.endswith(".ipynb") and not os.path.isdir( - os.path.join(examples_dir, file) - ): - src_path = os.path.join(examples_dir, file) - dst_path = os.path.join(docs_dir, "how-tos", file.name) - shutil.copy(src_path, dst_path) + # for file in examples_dir.iterdir(): + # if file.suffix.endswith(".ipynb") and not os.path.isdir( + # os.path.join(examples_dir, file) + # ): + # src_path = os.path.join(examples_dir, file) + # dst_path = os.path.join(docs_dir, "how-tos", file.name) + # shutil.copy(src_path, dst_path) if __name__ == "__main__": diff --git a/docs/docs/concepts.md b/docs/docs/concepts.md deleted file mode 100644 index fb5732b9c..000000000 --- a/docs/docs/concepts.md +++ /dev/null @@ -1,4 +0,0 @@ -# Concepts - - -## State \ No newline at end of file diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 1e11de709..12d8c5200 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -2,24 +2,29 @@ Welcome to the LangGraph How-To Guides! These guides provide practical, step-by-step instructions for accomplishing key tasks in LangGraph. -## Basics - -- [State Management](state-model.ipynb): How to define and manage complex state in your graphs -- [Tool Integration](sql_example.ipynb): How to integrate external tools and data sources -- [Human-in-the-Loop](human-in-the-loop.ipynb): How to incorporate human feedback and intervention - -## Performance +## Core - [Async Execution](async.ipynb): How to run nodes asynchronously for improved performance - [Streaming Responses](streaming-tokens.ipynb): How to stream agent responses in real-time - -## Graph Structure - -- [Subgraphs](subgraph.ipynb): How to modularize your graphs with subgraphs +- [Human-in-the-Loop](human-in-the-loop.ipynb): How to incorporate human feedback and intervention +- [Persistence](persistence.ipynb): How to save and load graph state for long-running applications +- [Time Travel](time-travel.ipynb): How to navigate and manipulate graph state history +- [Visualization](visualization.ipynb): How to visualize your graphs +- [Pydantic State](state-model.ipynb): Use a pydantic model as your state +- [Subgraphs](subgraph.ipynb): How to compose subgraphs within a larger graph - [Branching](branching.ipynb): How to create branching logic in your graphs -## Development +## AgentExecutor + +- [Human-in-the-Loop](agent_executor/human-in-the-loop.ipynb) +- [Force Tool First](agent_executor/force-calling-a-tool-first.ipynb) +- [Manage Agent Steps](agent_executor/managing-agent-steps.ipynb) + +## Chat Agent (Function Calling) + +- [Human-in-the-Loop](chat_agent_executor_with_function_calling/human-in-the-loop.ipynb) +- [Force Tool First](chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb) +- [Respond in Format](chat_agent_executor_with_function_calling/respond-in-format.ipynb) +- [Dynamic Direct Return](chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb) +- [Manage Agent Steps](chat_agent_executor_with_function_calling/managing-agent-steps.ipynb) -- [Persistence](persistence.ipynb): How to save and load graph state for long-running applications -- [Visualization](visualization.ipynb): How to visualize your graphs -- [Time Travel](time-travel.ipynb): How to navigate and manipulate graph execution history \ No newline at end of file diff --git a/docs/docs/index.md b/docs/docs/index.md index 77e9dcb39..3ef70d025 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -4,119 +4,111 @@ ⚡ Build language agents as graphs ⚡ - ## Overview -Suppose you're building a customer support assistant. You want your assistant to: +Suppose you're building a customer support assistant. You want your assistant to be able to: -1. Try to answer user questions using a knowledge base -2. Escalate to a human if it's not confident in its answer -3. Relay the human's resolution back to the user -4. Remember the full conversation context across multiple user messages +1. Use tools to respond to questions +2. Connect with a human if needed +3. Be able to pause the process indefinitely and resume whenever the human responds -With raw LLMs, the code to control the agentic loop, conversation state, route between the chatbot and human, and checkpoint the full application state can get complex. +LangGraph makes this all easy. First install: -LangGraph makes it simple. First install: - -```shell +```bash pip install -U langgraph ``` Then define your assistant: ```python -from langgraph.graph import StateGraph +import json + +from langchain_anthropic import ChatAnthropic +from langchain_community.tools.tavily_search import TavilySearchResults + from langgraph.checkpoint.sqlite import SqliteSaver - -from langchain_anthropic +from langgraph.graph import END, MessageGraph +from langgraph.prebuilt.tool_node import ToolNode -# Define the chatbot state -class ChatbotState(TypedDict): - conversation_history: Annotated[ConversationHistory, operator.add] - pending_human_request: Optional[HumanRequest] - -# Create nodes for the chatbot and human -def chatbot(state: ChatbotState): - # TODO - -def human(state: ChatbotState): - # TODO - -# Create the graph -graph = StateGraph(ChatbotState) -graph.add_node("chatbot", chatbot) -graph.add_node("human", human) - -# Define routing logic between chatbot and human -def should_escalate(state): - if state['pending_human_request']: - return "human" +# 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 not last_message.tool_calls: + return END else: - return "chatbot" + return "action" -graph.add_conditional_edges("chatbot", should_escalate, { - "human": "human", - "chatbot": "chatbot" -}) -graph.add_edge("human", "chatbot") +# Define a new graph +workflow = MessageGraph() -memory = SqliteSaver.from_conn_string(":memory:") -app = graph.compile(checkpointer=memory) -# Run the graph -result = app.invoke(new_user_message) +tools = [TavilySearchResults(max_results=1)] +model = ChatAnthropic(model="claude-3-haiku-20240307").bind_tools(tools) +workflow.add_node("agent", model) +workflow.add_node("action", ToolNode(tools)) + +workflow.set_entry_point("agent") + +# Conditional agent -> action OR agent -> END +workflow.add_conditional_edges( + "agent", + should_continue, +) + +# Always transition `action` -> `agent` +workflow.add_edge("action", "agent") + +memory = SqliteSaver.from_conn_string(":memory:") # Here we only save in-memory + +# Setting the interrupt means that any time an action is called, the machine will stop +app = workflow.compile(checkpointer=memory, interrupt_before=["action"]) ``` -The graph handles all the hard parts: +Now, run the graph: -- `conversation_history` in the state contains the assistant's "memory" -- Conditional edges enable dynamic routing between the chatbot and human based on the chatbot's confidence -- Persistence makes it easy to route to a human so they can respond and resume at any time +```python +# Run the graph +thread = {"configurable": {"thread_id": "4"}} +for event in app.stream("what is the weather in sf currently", thread): + for v in event.values(): + print(v) + +``` +We configured the graph to **wait** before executing the `action`. The `SqliteSaver` persists the state. Resume at any time. + +```python +for event in app.stream(None, thread): + for v in event.values(): + print(v) +``` + +The graph orchestrates everything: + +- The `MessageGraph` contains the agent's "Memory" +- Conditional edges enable dynamic routing between the chatbot, tools, and the user +- Persistence makes it easy to stop, resume, and even rewind for full control over your application With LangGraph, you can build complex, stateful agents without getting bogged down in manual state and interrupt management. Just define your nodes, edges, and state schema - and let the graph take care of the rest. -## Concepts - -- [Graphs](concepts.md#graphs) -- [State](concepts.md#state): The data structure passed between nodes, allowing you to persist context -- [Nodes](concepts.#nodes): The building blocks of your graph - LLMs, tools, or custom logic -- [Edges](concepts.md#edges): The connections that define the flow of data between your nodes -- [Conditional Edges](concepts.md#conditional_edges): Special edges that let you dynamically route between nodes based on state -- [Persistence](concepts.md#persistence): Save and resume your graph's state for long-running applications - -## How-To Guides - -Check out the [How-To Guides](how-tos/index.md) for instructions on handling common tasks with LangGraph. - -- Manage State -- Tool Integration -- Human-in-the-Loop -- Async Execution -- Streaming Responses -- Subgraphs & Branching -- Persistence, Visualization, Time Travel -- Benchmarking ## Tutorials -Consult the [Tutorials](tutorials/index.md) to learn more about implementing advanced +Consult the [Tutorials](tutorials/index.md) to learn more about building with LangGraph, including advanced use cases. -- **Agent Executors**: Chat and Langchain agents -- **Planning Agents**: Plan-and-Execute, ReWOO, LLMCompiler -- **Reflection & Critique**: Improving quality via reflection -- **Multi-Agent Systems**: Collaboration, supervision, teams -- **Research & QA**: Web research, retrieval-augmented QA -- **Applications**: Chatbots, code assist, web tasks -- **Evaluation & Analysis**: Simulation, self-discovery, swarms + +## How-To Guides + +Check out the [How-To Guides](how-tos/index.md) for instructions on handling common tasks with LangGraph ## Why LangGraph? -LangGraph extends the core strengths of LangChain Runnables (shared interface for streaming, async, and batch calls) to make it easy to: +LangGraph is framework agnostic (each node is a regular python function). It extends the core Runnable API (shared interface for streaming, async, and batch calls) to make it easy to: - Seamless state management across multiple turns of conversation or tool usage - The ability to flexibly route between nodes based on dynamic criteria - Smooth switching between LLMs and human intervention - Persistence for long-running, multi-session applications -If you're building a straightforward DAG,, LangChain expression language is a great fit. But for more complex, stateful applications with nonlinear flows, LangGraph is the perfect tool for the job. \ No newline at end of file +If you're building a straightforward DAG, Runnables are a great fit. But for more complex, stateful applications with nonlinear flows, LangGraph is the perfect tool for the job. \ No newline at end of file diff --git a/docs/docs/reference/checkpoints.md b/docs/docs/reference/checkpoints.md index e71634330..e93d4d8e5 100644 --- a/docs/docs/reference/checkpoints.md +++ b/docs/docs/reference/checkpoints.md @@ -1,11 +1,4 @@ # Checkpoints - ::: langgraph.checkpoint handler: python - options: - selection: - docstring_style: google - rendering: - heading_level: 3 - show_root_toc_entry: false \ No newline at end of file diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md index b21f65b5d..70127f505 100644 --- a/docs/docs/reference/graphs.md +++ b/docs/docs/reference/graphs.md @@ -1,11 +1,12 @@ -# StateGraph - +# Graph Definitions ::: langgraph.graph handler: python - options: - selection: - docstring_style: google - rendering: - heading_level: 3 - show_root_toc_entry: false \ No newline at end of file + +## CompiledGraph + +::: langgraph.graph.graph.CompiledGraph + handler: python + members: + - get_graph + - invoke \ No newline at end of file diff --git a/docs/docs/reference/prebuilt.md b/docs/docs/reference/prebuilt.md index 7cb2ada1b..832921b02 100644 --- a/docs/docs/reference/prebuilt.md +++ b/docs/docs/reference/prebuilt.md @@ -1,11 +1,48 @@ -# Prebuilt +# Prebuilt +## ToolNode -::: langgraph.prebuilt +```python +from langgraph.prebuilt import ToolNode +``` + +::: langgraph.prebuilt.ToolNode handler: python - options: - selection: - docstring_style: google - rendering: - heading_level: 3 - show_root_toc_entry: false \ No newline at end of file + + +## ToolExecutor + +```python +from langgraph.prebuilt import ToolExecutor +``` + +::: langgraph.prebuilt.ToolExecutor + handler: python + + +## ToolInvocation + +```python +from langgraph.prebuilt import ToolInvocation +``` + +::: langgraph.prebuilt.ToolInvocation + handler: python + heading_level: 4 + + +## `chat_agent_executor.create_tool_calling_executor` + +```python +from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor +``` + +::: langgraph.prebuilt.chat_agent_executor + +## `create_agent_executor` + +```python +from langgraph.prebuilt import create_agent_executor +``` + +::: langgraph.prebuilt.create_agent_executor diff --git a/docs/docs/static/favicon.png b/docs/docs/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dc08f512abd976c8a1cc870f19e9f3c6af24c4e GIT binary patch literal 5870 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 literal 0 HcmV?d00001 diff --git a/docs/docs/static/wordmark.png b/docs/docs/static/wordmark.png new file mode 100644 index 0000000000000000000000000000000000000000..ba0be276366029b4000b7172acf1a4ac85f3f3b7 GIT binary patch literal 22358 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.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (6.0.1)\n", - "Requirement already satisfied: SQLAlchemy<3,>=1.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (2.0.28)\n", - "Requirement already satisfied: aiohttp<4.0.0,>=3.8.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (3.9.3)\n", - "Requirement already satisfied: dataclasses-json<0.7,>=0.5.7 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (0.6.4)\n", - "Requirement already satisfied: langchain-core<0.2.0,>=0.1.37 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (0.1.38)\n", - "Requirement already satisfied: langsmith<0.2.0,>=0.1.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (0.1.23)\n", - "Requirement already satisfied: numpy<2,>=1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (1.26.4)\n", - "Requirement already satisfied: requests<3,>=2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (2.31.0)\n", - "Requirement already satisfied: tenacity<9.0.0,>=8.1.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain_community) (8.2.3)\n", - "Requirement already satisfied: regex>=2022.1.18 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from tiktoken) (2023.12.25)\n", - "Requirement already satisfied: openai<2.0.0,>=1.10.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain-openai) (1.13.3)\n", - "Requirement already satisfied: cohere<6.0.0,>=5.1.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain-cohere) (5.1.7)\n", - "Requirement already satisfied: types-requests<3.0.0.0,>=2.31.0.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchainhub) (2.31.0.20240311)\n", - "Requirement already satisfied: build>=1.0.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.2.1)\n", - "Requirement already satisfied: pydantic>=1.9 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (2.6.4)\n", - "Requirement already satisfied: chroma-hnswlib==0.7.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.7.3)\n", - "Requirement already satisfied: fastapi>=0.95.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.109.0)\n", - "Requirement already satisfied: uvicorn>=0.18.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (0.25.0)\n", - "Requirement already satisfied: posthog>=2.4.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (3.5.0)\n", - "Requirement already satisfied: typing-extensions>=4.5.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (4.10.0)\n", - "Requirement already satisfied: pulsar-client>=3.1.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (3.4.0)\n", - "Requirement already satisfied: onnxruntime>=1.14.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.17.1)\n", - "Requirement already satisfied: opentelemetry-api>=1.2.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.24.0)\n", - "Requirement already satisfied: opentelemetry-exporter-otlp-proto-grpc>=1.2.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.24.0)\n", - "Requirement already satisfied: opentelemetry-instrumentation-fastapi>=0.41b0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.45b0)\n", - "Requirement already satisfied: opentelemetry-sdk>=1.2.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.24.0)\n", - "Requirement already satisfied: tokenizers>=0.13.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.15.2)\n", - "Requirement already satisfied: pypika>=0.48.9 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.48.9)\n", - "Requirement already satisfied: tqdm>=4.65.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (4.66.2)\n", - "Requirement already satisfied: overrides>=7.3.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (7.7.0)\n", - "Requirement already satisfied: importlib-resources in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (6.4.0)\n", - "Requirement already satisfied: grpcio>=1.58.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (1.62.1)\n", - "Requirement already satisfied: bcrypt>=4.0.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (4.1.2)\n", - "Requirement already satisfied: typer>=0.9.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (0.12.0)\n", - "Requirement already satisfied: kubernetes>=28.1.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (29.0.0)\n", - "Requirement already satisfied: mmh3>=4.0.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (4.1.0)\n", - "Requirement already satisfied: orjson>=3.9.12 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from chromadb) (3.9.15)\n", - "Requirement already satisfied: jsonpatch<2.0,>=1.33 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain) (1.33)\n", - "Requirement already satisfied: langchain-text-splitters<0.1,>=0.0.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from langchain) (0.0.1)\n", - "Requirement already satisfied: aiosignal>=1.1.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain_community) (1.3.1)\n", - "Requirement already satisfied: attrs>=17.3.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain_community) (23.2.0)\n", - "Requirement already satisfied: frozenlist>=1.1.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain_community) (1.4.1)\n", - "Requirement already satisfied: multidict<7.0,>=4.5 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain_community) (6.0.5)\n", - "Requirement already satisfied: yarl<2.0,>=1.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain_community) (1.9.4)\n", - "Requirement already satisfied: packaging>=19.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from build>=1.0.3->chromadb) (23.2)\n", - "Requirement already satisfied: pyproject_hooks in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from build>=1.0.3->chromadb) (1.0.0)\n", - "Requirement already satisfied: fastavro<2.0.0,>=1.9.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from cohere<6.0.0,>=5.1.4->langchain-cohere) (1.9.4)\n", - "Requirement already satisfied: httpx>=0.21.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from cohere<6.0.0,>=5.1.4->langchain-cohere) (0.26.0)\n", - "Requirement already satisfied: marshmallow<4.0.0,>=3.18.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from dataclasses-json<0.7,>=0.5.7->langchain_community) (3.21.1)\n", - "Requirement already satisfied: typing-inspect<1,>=0.4.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from dataclasses-json<0.7,>=0.5.7->langchain_community) (0.9.0)\n", - "Requirement already satisfied: starlette<0.36.0,>=0.35.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from fastapi>=0.95.2->chromadb) (0.35.1)\n", - "Requirement already satisfied: jsonpointer>=1.9 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from jsonpatch<2.0,>=1.33->langchain) (2.4)\n", - "Requirement already satisfied: certifi>=14.05.14 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (2024.2.2)\n", - "Requirement already satisfied: six>=1.9.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (1.16.0)\n", - "Requirement already satisfied: python-dateutil>=2.5.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (2.9.0.post0)\n", - "Requirement already satisfied: google-auth>=1.0.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (2.29.0)\n", - "Requirement already satisfied: websocket-client!=0.40.0,!=0.41.*,!=0.42.*,>=0.32.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (1.7.0)\n", - "Requirement already satisfied: requests-oauthlib in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (2.0.0)\n", - "Requirement already satisfied: oauthlib>=3.2.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (3.2.2)\n", - "Requirement already satisfied: urllib3>=1.24.2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from kubernetes>=28.1.0->chromadb) (2.2.1)\n", - "Requirement already satisfied: coloredlogs in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from onnxruntime>=1.14.1->chromadb) (15.0.1)\n", - "Requirement already satisfied: flatbuffers in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from onnxruntime>=1.14.1->chromadb) (24.3.25)\n", - "Requirement already satisfied: protobuf in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from onnxruntime>=1.14.1->chromadb) (4.25.3)\n", - "Requirement already satisfied: sympy in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from onnxruntime>=1.14.1->chromadb) (1.12)\n", - "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from openai<2.0.0,>=1.10.0->langchain-openai) (4.3.0)\n", - "Requirement already satisfied: distro<2,>=1.7.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from openai<2.0.0,>=1.10.0->langchain-openai) (1.9.0)\n", - "Requirement already satisfied: sniffio in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from openai<2.0.0,>=1.10.0->langchain-openai) (1.3.1)\n", - "Requirement already satisfied: deprecated>=1.2.6 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-api>=1.2.0->chromadb) (1.2.14)\n", - "Requirement already satisfied: importlib-metadata<=7.0,>=6.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-api>=1.2.0->chromadb) (7.0.0)\n", - "Requirement already satisfied: googleapis-common-protos~=1.52 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb) (1.63.0)\n", - "Requirement already satisfied: opentelemetry-exporter-otlp-proto-common==1.24.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb) (1.24.0)\n", - "Requirement already satisfied: opentelemetry-proto==1.24.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-grpc>=1.2.0->chromadb) (1.24.0)\n", - "Requirement already satisfied: opentelemetry-instrumentation-asgi==0.45b0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (0.45b0)\n", - "Requirement already satisfied: opentelemetry-instrumentation==0.45b0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (0.45b0)\n", - "Requirement already satisfied: opentelemetry-semantic-conventions==0.45b0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (0.45b0)\n", - "Requirement already satisfied: opentelemetry-util-http==0.45b0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (0.45b0)\n", - "Requirement already satisfied: setuptools>=16.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation==0.45b0->opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (69.0.2)\n", - "Requirement already satisfied: wrapt<2.0.0,>=1.0.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation==0.45b0->opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (1.16.0)\n", - "Requirement already satisfied: asgiref~=3.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from opentelemetry-instrumentation-asgi==0.45b0->opentelemetry-instrumentation-fastapi>=0.41b0->chromadb) (3.8.1)\n", - "Requirement already satisfied: monotonic>=1.5 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from posthog>=2.4.0->chromadb) (1.6)\n", - "Requirement already satisfied: backoff>=1.10.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from posthog>=2.4.0->chromadb) (2.2.1)\n", - "Requirement already satisfied: annotated-types>=0.4.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from pydantic>=1.9->chromadb) (0.6.0)\n", - "Requirement already satisfied: pydantic-core==2.16.3 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from pydantic>=1.9->chromadb) (2.16.3)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from requests<3,>=2->langchain_community) (3.3.2)\n", - "Requirement already satisfied: idna<4,>=2.5 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from requests<3,>=2->langchain_community) (3.6)\n", - "Requirement already satisfied: huggingface_hub<1.0,>=0.16.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from tokenizers>=0.13.2->chromadb) (0.22.1)\n", - "Requirement already satisfied: typer-slim==0.12.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (0.12.0)\n", - "Requirement already satisfied: typer-cli==0.12.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typer>=0.9.0->chromadb) (0.12.0)\n", - "Requirement already satisfied: click>=8.0.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typer-slim==0.12.0->typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (8.1.7)\n", - "Requirement already satisfied: shellingham>=1.3.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (1.5.4)\n", - "Requirement already satisfied: rich>=10.11.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (13.7.1)\n", - "Requirement already satisfied: h11>=0.8 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn>=0.18.3->uvicorn[standard]>=0.18.3->chromadb) (0.14.0)\n", - "Requirement already satisfied: httptools>=0.5.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (0.6.1)\n", - "Requirement already satisfied: python-dotenv>=0.13 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (1.0.1)\n", - "Requirement already satisfied: uvloop!=0.15.0,!=0.15.1,>=0.14.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (0.19.0)\n", - "Requirement already satisfied: watchfiles>=0.13 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (0.21.0)\n", - "Requirement already satisfied: websockets>=10.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from uvicorn[standard]>=0.18.3->chromadb) (12.0)\n", - "Requirement already satisfied: cachetools<6.0,>=2.0.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb) (5.3.3)\n", - "Requirement already satisfied: pyasn1-modules>=0.2.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb) (0.4.0)\n", - "Requirement already satisfied: rsa<5,>=3.1.4 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from google-auth>=1.0.1->kubernetes>=28.1.0->chromadb) (4.9)\n", - "Requirement already satisfied: httpcore==1.* in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from httpx>=0.21.2->cohere<6.0.0,>=5.1.4->langchain-cohere) (1.0.4)\n", - "Requirement already satisfied: filelock in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from huggingface_hub<1.0,>=0.16.4->tokenizers>=0.13.2->chromadb) (3.13.3)\n", - "Requirement already satisfied: fsspec>=2023.5.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from huggingface_hub<1.0,>=0.16.4->tokenizers>=0.13.2->chromadb) (2024.3.1)\n", - "Requirement already satisfied: zipp>=0.5 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from importlib-metadata<=7.0,>=6.0->opentelemetry-api>=1.2.0->chromadb) (3.18.1)\n", - "Requirement already satisfied: mypy-extensions>=0.3.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from typing-inspect<1,>=0.4.0->dataclasses-json<0.7,>=0.5.7->langchain_community) (1.0.0)\n", - "Requirement already satisfied: humanfriendly>=9.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from coloredlogs->onnxruntime>=1.14.1->chromadb) (10.0)\n", - "Requirement already satisfied: mpmath>=0.19 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from sympy->onnxruntime>=1.14.1->chromadb) (1.3.0)\n", - "Requirement already satisfied: pyasn1<0.7.0,>=0.4.6 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from pyasn1-modules>=0.2.1->google-auth>=1.0.1->kubernetes>=28.1.0->chromadb) (0.6.0)\n", - "Requirement already satisfied: markdown-it-py>=2.2.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from rich>=10.11.0->typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (3.0.0)\n", - "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from rich>=10.11.0->typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (2.17.2)\n", - "Requirement already satisfied: mdurl~=0.1 in /Users/nuno/dev/langgraph/.venv/lib/python3.11/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer-slim[standard]==0.12.0->typer>=0.9.0->chromadb) (0.1.2)\n", - "Downloading langchain_community-0.0.31-py3-none-any.whl (1.9 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.9/1.9 MB\u001b[0m \u001b[31m9.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m[31m10.3 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading tiktoken-0.6.0-cp311-cp311-macosx_11_0_arm64.whl (949 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m949.8/949.8 kB\u001b[0m \u001b[31m15.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m31m20.3 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading langchain_openai-0.1.1-py3-none-any.whl (32 kB)\n", - "Downloading langchain-0.1.14-py3-none-any.whl (812 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m812.8/812.8 kB\u001b[0m \u001b[31m14.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading langgraph-0.0.31-py3-none-any.whl (55 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m55.5/55.5 kB\u001b[0m \u001b[31m4.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading tavily_python-0.3.3-py3-none-any.whl (5.4 kB)\n", - "Installing collected packages: tiktoken, tavily-python, langgraph, langchain-openai, langchain_community, langchain\n", - " Attempting uninstall: tiktoken\n", - " Found existing installation: tiktoken 0.5.2\n", - " Uninstalling tiktoken-0.5.2:\n", - " Successfully uninstalled tiktoken-0.5.2\n", - " Attempting uninstall: tavily-python\n", - " Found existing installation: tavily-python 0.3.1\n", - " Uninstalling tavily-python-0.3.1:\n", - " Successfully uninstalled tavily-python-0.3.1\n", - " Attempting uninstall: langgraph\n", - " Found existing installation: langgraph 0.0.30\n", - " Uninstalling langgraph-0.0.30:\n", - " Successfully uninstalled langgraph-0.0.30\n", - " Attempting uninstall: langchain-openai\n", - " Found existing installation: langchain-openai 0.0.2.post1\n", - " Uninstalling langchain-openai-0.0.2.post1:\n", - " Successfully uninstalled langchain-openai-0.0.2.post1\n", - " Attempting uninstall: langchain_community\n", - " Found existing installation: langchain-community 0.0.27\n", - " Uninstalling langchain-community-0.0.27:\n", - " Successfully uninstalled langchain-community-0.0.27\n", - " Attempting uninstall: langchain\n", - " Found existing installation: langchain 0.1.11\n", - " Uninstalling langchain-0.1.11:\n", - " Successfully uninstalled langchain-0.1.11\n", - "Successfully installed langchain-0.1.14 langchain-openai-0.1.1 langchain_community-0.0.31 langgraph-0.0.31 tavily-python-0.3.3 tiktoken-0.6.0\n", - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ + "%%capture --no-stderr\n", "! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python" ] }, diff --git a/langgraph/checkpoint/base.py b/langgraph/checkpoint/base.py index 6e3b2fc46..a48cd2a8b 100644 --- a/langgraph/checkpoint/base.py +++ b/langgraph/checkpoint/base.py @@ -69,8 +69,12 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: class CheckpointAt(StrEnum): + """When to take a checkpoint.""" + END_OF_STEP = "end_of_step" + """Take a checkpoint at the end of each step.""" END_OF_RUN = "end_of_run" + """Take a checkpoint at the end of the run.""" class CheckpointTuple(NamedTuple): diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 5830e3a9c..4ada73c1c 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -153,6 +153,24 @@ class Graph: ], conditional_edge_mapping: Optional[dict[str, str]] = None, ) -> None: + """Add a conditional edge from the starting node to any number of destination nodes. + + + Args: + start_key (str): The key of the starting node. + condition (Union[Callable, Runnable]): The condition that determines the destination of the edge. + conditional_edge_mapping (Optional[dict[str, str]]): A dictionary that maps the response of the condition to a name of + the destination node(s). If the condition returns a list, the response will be matched against the keys of the + dictionary. If the condition returns a string, the response will be matched against the values of the dictionary. + If the condition returns a string and the dictionary contains a key with the value of `END` ("__end__"`), + the graph will finish. + + Raises: + ValueError: If the starting node is not found in the graph or if the conditional edge mapping contains missing nodes. + + Returns: + None + """ # noqa: E501 if self.compiled: logger.warning( "Adding an edge to a graph that has already been compiled. This will " @@ -182,6 +200,14 @@ class Graph: self.branches[start_key][name] = Branch(condition, conditional_edge_mapping) def set_entry_point(self, key: str) -> None: + """Specifies the first node to be called in the graph. + + Parameters: + key (str): The key of the node to set as the entry point. + + Returns: + None + """ return self.add_edge(START, key) def set_conditional_entry_point( @@ -191,9 +217,28 @@ class Graph: ], conditional_edge_mapping: Optional[Dict[str, str]] = None, ) -> None: + """Sets a conditional entry point in the graph. + + Args: + condition: A callable object that takes any number of arguments and returns a string or an awaitable string. + conditional_edge_mapping: A dictionary that maps condition names to edge names. + + Returns: + None + """ return self.add_conditional_edges(START, condition, conditional_edge_mapping) def set_finish_point(self, key: str) -> None: + """Marks a node as a finish point of the graph. + + If the graph reaches this node, it will cease execution. + + Parameters: + key (str): The key of the node to set as the finish point. + + Returns: + None + """ return self.add_edge(key, END) def validate(self, interrupt: Optional[Sequence[str]] = None) -> None: diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index 33114a795..bc42d0370 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -45,11 +45,37 @@ class StateGraph(Graph): } def add_node(self, key: str, action: RunnableLike) -> None: + """Adds a new node to the state graph. + + Args: + key (str): The key of the node. + action (RunnableLike): The action associated with the node. + + Raises: + ValueError: If the key is already being used as a state key. + + Returns: + None + """ if key in self.channels: raise ValueError(f"'{key}' is already being used as a state key") return super().add_node(key, action) def add_edge(self, start_key: Union[str, list[str]], end_key: str) -> None: + """Adds a directed edge from the start node to the end node. + + If the graph transitions to the start_key node, it will always transition to the end_key node next. + + Args: + start_key (Union[str, list[str]]): The key(s) of the start node(s) of the edge. + end_key (str): The key of the end node of the edge. + + Raises: + ValueError: If the start key is 'END' or if the start key or end key is not present in the graph. + + Returns: + None + """ if isinstance(start_key, str): return super().add_edge(start_key, end_key) @@ -77,6 +103,17 @@ class StateGraph(Graph): interrupt_after: Optional[Sequence[str]] = None, debug: bool = False, ) -> CompiledGraph: + """Compiles the state graph into a `CompiledGraph` object. + + Args: + 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. + + Returns: + CompiledGraph: The compiled state graph. + """ # assign default values interrupt_before = interrupt_before or [] interrupt_after = interrupt_after or [] @@ -135,10 +172,12 @@ class CompiledStateGraph(CompiledGraph): state_keys = list(self.graph.channels) # state updaters state_write_entries = [ - ChannelWriteEntry(key, None, skip_none=True) - if key == "__root__" - else ChannelWriteEntry( - key, RunnableCallable(_get_state_key, key=key, trace=False) + ( + ChannelWriteEntry(key, None, skip_none=True) + if key == "__root__" + else ChannelWriteEntry( + key, RunnableCallable(_get_state_key, key=key, trace=False) + ) ) for key in state_keys ] diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py index 2e942002f..74a6156b8 100644 --- a/langgraph/prebuilt/agent_executor.py +++ b/langgraph/prebuilt/agent_executor.py @@ -5,6 +5,7 @@ from langchain_core.agents import AgentAction, AgentFinish from langchain_core.messages import BaseMessage from langgraph.graph import END, StateGraph +from langgraph.graph.state import CompiledStateGraph from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.utils import RunnableCallable @@ -39,7 +40,47 @@ def _get_agent_state(input_schema=None): return AgentState -def create_agent_executor(agent_runnable, tools, input_schema=None): +def create_agent_executor( + agent_runnable, tools, input_schema=None +) -> CompiledStateGraph: + """This is a helper function for creating a graph that works with LangChain Agents. + + Args: + agent_runnable (RunnableLike): The agent runnable. + tools (list): A list of tools to be used by the agent. + input_schema (dict, optional): The input schema for the agent. Defaults to None. + + Returns: + The `CompiledStateGraph` object. + + Examples: + + from langgraph.prebuilt import create_agent_executor + from langchain_openai import ChatOpenAI + from langchain import hub + from langchain.agents import create_openai_functions_agent + from langchain_community.tools.tavily_search import TavilySearchResults + + tools = [TavilySearchResults(max_results=1)] + + # Get the prompt to use - you can modify this! + prompt = hub.pull("hwchase17/openai-functions-agent") + + # Choose the LLM that will drive the agent + llm = ChatOpenAI(model="gpt-3.5-turbo-1106") + + # Construct the OpenAI Functions agent + agent_runnable = create_openai_functions_agent(llm, tools, prompt) + + app = create_agent_executor(agent_runnable, tools) + + inputs = {"input": "what is the weather in sf", "chat_history": []} + for s in app.stream(inputs): + print(list(s.values())[0]) + print("----") + + """ + if isinstance(tools, ToolExecutor): tool_executor = tools else: diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index 473d46cad..fbc254b96 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -8,6 +8,7 @@ from langchain_core.tools import BaseTool from langchain_core.utils.function_calling import convert_to_openai_function from langgraph.graph import END, StateGraph +from langgraph.graph.graph import CompiledGraph from langgraph.graph.message import add_messages from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation from langgraph.prebuilt.tool_node import ToolNode @@ -18,12 +19,14 @@ from langgraph.prebuilt.tool_node import ToolNode # We want steps to return messages to append to the list # So we annotate the messages attribute with operator.add class AgentState(TypedDict): + """The state of the agent.""" + messages: Annotated[Sequence[BaseMessage], add_messages] def create_function_calling_executor( model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]] -): +) -> CompiledGraph: if isinstance(tools, ToolExecutor): tool_executor = tools tool_classes = tools.tools @@ -132,7 +135,33 @@ def create_function_calling_executor( def create_tool_calling_executor( model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]] -): +) -> CompiledGraph: + """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. + + Returns: + Runnable: A compiled LangChain runnable that can be used for chat interactions. + + Examples: + + from langgraph.prebuilt import chat_agent_executor + from langchain_openai import ChatOpenAI + from langchain_community.tools.tavily_search import TavilySearchResults + from langchain_core.messages import HumanMessage + + tools = [TavilySearchResults(max_results=1)] + model = ChatOpenAI() + + app = chat_agent_executor.create_tool_calling_executor(model, tools) + + inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} + for s in app.stream(inputs): + print(list(s.values())[0]) + print("----") + """ if isinstance(tools, ToolExecutor): tool_classes = tools.tools else: diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 1d4031df3..810538fc1 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -303,6 +303,7 @@ class Pregel( ) def get_state(self, config: RunnableConfig) -> StateSnapshot: + """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -323,6 +324,7 @@ class Pregel( ) async def aget_state(self, config: RunnableConfig) -> StateSnapshot: + """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -343,6 +345,7 @@ class Pregel( ) def get_state_history(self, config: RunnableConfig) -> Iterator[StateSnapshot]: + """Get the history of the state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -364,6 +367,7 @@ class Pregel( async def aget_state_history( self, config: RunnableConfig ) -> AsyncIterator[StateSnapshot]: + """Get the history of the state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -559,6 +563,7 @@ class Pregel( interrupt_after_nodes: Optional[Sequence[str]] = None, debug: Optional[bool] = None, ) -> Iterator[Union[dict[str, Any], Any]]: + """Stream graph steps for a single input.""" config = ensure_config(config) callback_manager = get_callback_manager_for_config(config) run_manager = callback_manager.on_chain_start( @@ -970,6 +975,23 @@ class Pregel( debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: + """Run the graph with a single input and config. + + Args: + input: The input data for the graph. It can be a dictionary or any other type. + config: Optional. The configuration for the graph run. + stream_mode: Optional[str]. The stream mode for the graph run. Default is "values". + output_keys: Optional. The output keys to retrieve from the graph run. + input_keys: Optional. The input keys to provide for the graph run. + interrupt_before_nodes: Optional. The nodes to interrupt the graph run before. + interrupt_after_nodes: Optional. The nodes to interrupt the graph run after. + debug: Optional. Enable debug mode for the graph run. + **kwargs: Additional keyword arguments to pass to the graph run. + + Returns: + The output of the graph run. If stream_mode is "values", it returns the latest output. + If stream_mode is not "values", it returns a list of output chunks. + """ output_keys = output_keys if output_keys is not None else self.output_channels if stream_mode == "values": latest: Union[dict[str, Any], Any] = None @@ -1008,6 +1030,24 @@ class Pregel( debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: + """Asynchronously invoke the graph on a single input. + + Args: + input: The input data for the computation. It can be a dictionary or any other type. + config: Optional. The configuration for the computation. + stream_mode: Optional. The stream mode for the computation. Default is "values". + output_keys: Optional. The output keys to include in the result. Default is None. + input_keys: Optional. The input keys to include in the result. Default is None. + interrupt_before_nodes: Optional. The nodes to interrupt before. Default is None. + interrupt_after_nodes: Optional. The nodes to interrupt after. Default is None. + debug: Optional. Whether to enable debug mode. Default is None. + **kwargs: Additional keyword arguments. + + Returns: + The result of the computation. If stream_mode is "values", it returns the latest value. + If stream_mode is "chunks", it returns a list of chunks. + """ + output_keys = output_keys if output_keys is not None else self.output_channels if stream_mode == "values": latest: Union[dict[str, Any], Any] = None