Compare commits

..
15 Commits
Author SHA1 Message Date
Vadym BardaandGitHub 4a45f6c99a langgraph, sqlite, postgres: update to use langgraph-checkpoint==2.0.0 (#1946) 2024-10-01 15:28:06 -04:00
Vadym BardaandGitHub 00662ab557 ci: update the check for changed notebooks (#1945) 2024-10-01 18:14:32 +00:00
Brace SproulandGitHub 11d636ff83 Merge pull request #1910 from langchain-ai/wfh/js_store_client
Add JS SDK Store Client
2024-10-01 10:44:06 -07:00
Vadym BardaandGitHub 78c9c15b14 langgraph, checkpoint: update versions (#1942) 2024-10-01 13:43:07 -04:00
Vadym BardaandGitHub 46b52b2426 ci: run notebook checks for changed notebooks (#1941) 2024-10-01 16:50:24 +00:00
Eugene YurtsevandGitHub 674180ad0c docs: Add linting check as part of docs deploy (#1939)
We'll need to add the same thing as part of CI testing, but a second check
during deployment wont hurt in case CI is by passed for whatever reason.
2024-10-01 10:50:39 -04:00
Vadym BardaandGitHub 71efff7cee ci: add a script to check sync and async methods in SDK (#1938) 2024-10-01 09:50:26 -04:00
William FHandGitHub dd88ac6224 Bump SDK Py (#1935) 2024-10-01 08:17:51 +00:00
Nuno CamposandGitHub 53c2e4d8c2 Merge pull request #1932 from langchain-ai/eugene/format_docs
docs: format with ruff
2024-09-30 19:13:47 -07:00
Eugene Yurtsev 70b0032191 format docs 2024-09-30 22:08:27 -04:00
William FHandGitHub 445b795037 [Docs] update min bounds from API (#1929) 2024-09-30 17:06:34 -07:00
William FHandGitHub 29f58fd9e5 Custom loads support in postgres checkpointer (#1930) 2024-09-30 16:42:23 -07:00
William Fu-Hinthorn 4fca814d3a Merge branch 'main' into wfh/js_store_client 2024-09-30 10:49:09 -07:00
William Fu-Hinthorn d309c36388 Review 2024-09-30 10:48:49 -07:00
William Fu-Hinthorn f064a5969d Add JS SDK Store Client 2024-09-30 09:08:37 -07:00
78 changed files with 2348 additions and 1569 deletions
+64
View File
@@ -0,0 +1,64 @@
import ast
import os
from itertools import filterfalse
from typing import List, Tuple
ROOT_PATH = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
CLIENT_PATH = os.path.join(ROOT_PATH, "libs", "sdk-py", "langgraph_sdk", "client.py")
def get_class_methods(node: ast.ClassDef) -> List[str]:
return [n.name for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
def find_classes(tree: ast.AST) -> List[Tuple[str, List[str]]]:
classes = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
methods = get_class_methods(node)
classes.append((node.name, methods))
return classes
def compare_sync_async_methods(sync_methods: List[str], async_methods: List[str]) -> List[str]:
sync_set = set(sync_methods)
async_set = set(async_methods)
missing_in_sync = list(async_set - sync_set)
missing_in_async = list(sync_set - async_set)
return missing_in_sync + missing_in_async
def main():
with open(CLIENT_PATH, "r") as file:
tree = ast.parse(file.read())
classes = find_classes(tree)
def is_sync(class_spec: Tuple[str, List[str]]) -> bool:
return class_spec[0].startswith("Sync")
sync_class_name_to_methods = {class_name: class_methods for class_name, class_methods in filter(is_sync, classes)}
async_class_name_to_methods = {class_name: class_methods for class_name, class_methods in filterfalse(is_sync, classes)}
mismatches = []
for async_class_name, async_class_methods in async_class_name_to_methods.items():
sync_class_name = "Sync" + async_class_name
sync_class_methods = sync_class_name_to_methods.get(sync_class_name, [])
diff = compare_sync_async_methods(sync_class_methods, async_class_methods)
if diff:
mismatches.append((sync_class_name, async_class_name, diff))
if mismatches:
error_message = "Mismatches found between sync and async client methods:\n"
for sync_class_name, async_class_name, diff in mismatches:
error_message += f"{sync_class_name} vs {async_class_name}:\n"
for method in diff:
error_message += f" - {method}\n"
raise ValueError(error_message)
print("All sync and async client methods match.")
if __name__ == "__main__":
main()
+12
View File
@@ -66,6 +66,18 @@ jobs:
uses: ./.github/workflows/_test_scheduler_kafka.yml
secrets: inherit
check-sdk-methods:
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run check_sdk_methods script
run: python .github/scripts/check_sdk_methods.py
integration-test:
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
+24
View File
@@ -22,7 +22,27 @@ concurrency:
cancel-in-progress: false
jobs:
get-changed-files:
runs-on: ubuntu-latest
outputs:
changed-files: ${{ steps.changed-files.outputs.all }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "docs/docs/**"
run-changed-notebooks:
needs: get-changed-files
uses: ./.github/workflows/run_notebooks.yml
secrets: inherit
with:
changed-files: ${{ needs.get-changed-files.outputs.changed-files }}
deploy:
# needs: run-changed-notebooks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -41,6 +61,10 @@ jobs:
poetry install --with docs
poetry run pip install -U pytest pytest-check-links langsmith langchain GitPython
- name: Lint Docs
# This step lints the docs using the existing linting set up.
# It should be very fast and should not require any external services.
run: make lint-docs
- name: Build site
run: make build-docs
env:
+18 -2
View File
@@ -2,6 +2,12 @@ name: Run notebooks
on:
workflow_dispatch:
workflow_call:
inputs:
changed-files:
required: false
type: string
description: "JSON string of changed files"
schedule:
- cron: '0 13 * * *'
@@ -14,7 +20,6 @@ jobs:
- "development"
- "latest"
name: "test (langgraph: ${{ matrix.lib-version }})"
steps:
- uses: actions/checkout@v4
- name: Set up Python + Poetry
@@ -56,7 +61,18 @@ jobs:
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
run: |
./docs/_scripts/execute_notebooks.sh
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
echo "Running all notebooks"
./docs/_scripts/execute_notebooks.sh
else
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | grep '\.ipynb$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "Running changed notebooks: $CHANGED_FILES"
./docs/_scripts/execute_notebooks.sh $CHANGED_FILES
else
echo "No notebook files changed, skipping execution"
fi
fi
- name: Stop services
run: make stop-services
+7 -2
View File
@@ -22,8 +22,13 @@ execute_notebook() {
export -f execute_notebook
# Find all notebooks and filter out those in the skip list
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
# Check if custom notebook paths are provided
if [ $# -gt 0 ]; then
notebooks="$@"
else
# Find all notebooks and filter out those in the skip list
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
fi
# Execute notebooks sequentially
for file in $notebooks; do
+15 -8
View File
@@ -11,6 +11,13 @@ NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")
BLOCKLIST_COMMANDS = (
# skip if has WebBaseLoader to avoid caching web pages
"WebBaseLoader",
# skip if has draw_mermaid_png to avoid generating mermaid images via API
"draw_mermaid_png",
)
NOTEBOOKS_NO_CASSETTES = (
"docs/docs/how-tos/visualization.ipynb",
"docs/docs/how-tos/many-tools.ipynb"
@@ -62,8 +69,13 @@ def is_magic_command(code: str) -> bool:
def is_comment(code: str) -> bool:
return code.strip().startswith("#")
def is_mermaid_command(code: str) -> bool:
return "draw_mermaid_png" in code.strip()
def has_blocklisted_command(code: str) -> bool:
code = code.strip()
for blocklisted_command in BLOCKLIST_COMMANDS:
if blocklisted_command in code:
return True
return False
def add_vcr_to_notebook(
@@ -87,10 +99,6 @@ def add_vcr_to_notebook(
if all(are_magic_lines):
continue
# skip if using mermaid
if any(is_mermaid_command(line) for line in lines):
continue
if any(are_magic_lines):
raise ValueError(
"Cannot process code cells with mixed magic and non-magic code."
@@ -100,8 +108,7 @@ def add_vcr_to_notebook(
if all(is_comment(line) or not line.strip() for line in lines):
continue
# skip if has WebBaseLoader to avoid caching web pages
if "WebBaseLoader" in cell.source:
if has_blocklisted_command(cell.source):
continue
cell_id = cell.get("id", idx)
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -36,8 +36,8 @@ Dependencies can optionally be specified in one of the following files: `pyproje
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.2.18,<0.3.0
langgraph-checkpoint>=1.0.9
langgraph>=0.2.30,<0.3.0
langgraph-checkpoint>=1.0.14
langchain-core>=0.2.38,<0.4.0
langsmith>=0.1.63
orjson>=3.9.7
@@ -36,8 +36,8 @@ Dependencies can optionally be specified in one of the following files: `pyproje
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
```
langgraph>=0.2.18,<0.3.0
langgraph-checkpoint>=1.0.9
langgraph>=0.2.30,<0.3.0
langgraph-checkpoint>=1.0.14
langchain-core>=0.2.38,<0.4.0
langsmith>=0.1.63
orjson>=3.9.7
+5 -5
View File
@@ -533,12 +533,12 @@
" # because we chose to only include LLMs, these are LLM tokens\n",
" try:\n",
" content = op[\"value\"].content[0]\n",
" if 'partial_json' in content:\n",
" print(content['partial_json'], end=\"|\")\n",
" elif 'text' in content:\n",
" print(content['text'], end='|')\n",
" if \"partial_json\" in content:\n",
" print(content[\"partial_json\"], end=\"|\")\n",
" elif \"text\" in content:\n",
" print(content[\"text\"], end=\"|\")\n",
" else:\n",
" print(content,end=\"|\")\n",
" print(content, end=\"|\")\n",
" except:\n",
" pass"
]
+3
View File
@@ -158,6 +158,7 @@
" \"openai\": openai_model,\n",
"}\n",
"\n",
"\n",
"def _call_model(state: AgentState, config: RunnableConfig):\n",
" # Access the config through the configurable key\n",
" model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n",
@@ -253,12 +254,14 @@
"source": [
"from langchain_core.messages import SystemMessage\n",
"\n",
"\n",
"# We can define a config schema to specify the configuration options for the graph\n",
"# A config schema is useful for indicating which fields are available in the configurable dict inside the config\n",
"class ConfigSchema(TypedDict):\n",
" model: Optional[str]\n",
" system_message: Optional[str]\n",
"\n",
"\n",
"def _call_model(state: AgentState, config: RunnableConfig):\n",
" # Access the config through the configurable key\n",
" model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n",
@@ -176,6 +176,7 @@
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"42\"}}\n",
"inputs = {\"messages\": [(\"user\", \"what is the weather in SF, CA?\")]}\n",
"\n",
@@ -285,10 +286,10 @@
"source": [
"state = graph.get_state(config)\n",
"\n",
"last_message = state.values['messages'][-1]\n",
"last_message.tool_calls[0]['args'] = {\"location\": \"San Francisco\"}\n",
"last_message = state.values[\"messages\"][-1]\n",
"last_message.tool_calls[0][\"args\"] = {\"location\": \"San Francisco\"}\n",
"\n",
"graph.update_state(config, {\"messages\": [ last_message]})"
"graph.update_state(config, {\"messages\": [last_message]})"
]
},
{
+8 -4
View File
@@ -21,13 +21,15 @@
"from langgraph.graph import MessagesState\n",
"from langgraph.graph import StateGraph, START, END\n",
"\n",
"llm = ChatOpenAI(model=\"o1-preview\",temperature=1)\n",
"llm = ChatOpenAI(model=\"o1-preview\", temperature=1)\n",
"\n",
"graph_builder = StateGraph(MessagesState)\n",
"\n",
"\n",
"def chatbot(state: MessagesState):\n",
" return {\"messages\": [llm.invoke(state[\"messages\"])]}\n",
"\n",
"\n",
"graph_builder.add_node(\"chatbot\", chatbot)\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
"graph_builder.add_edge(\"chatbot\", END)\n",
@@ -112,7 +114,7 @@
}
],
"source": [
"input = {\"messages\": {\"role\":\"user\", \"content\":\"how many r's are in strawberry?\"}}\n",
"input = {\"messages\": {\"role\": \"user\", \"content\": \"how many r's are in strawberry?\"}}\n",
"try:\n",
" async for event in graph.astream_events(input, version=\"v2\"):\n",
" if event[\"event\"] == \"on_chat_model_end\":\n",
@@ -138,13 +140,15 @@
"metadata": {},
"outputs": [],
"source": [
"llm = ChatOpenAI(model=\"o1-preview\",temperature=1,disable_streaming=True)\n",
"llm = ChatOpenAI(model=\"o1-preview\", temperature=1, disable_streaming=True)\n",
"\n",
"graph_builder = StateGraph(MessagesState)\n",
"\n",
"\n",
"def chatbot(state: MessagesState):\n",
" return {\"messages\": [llm.invoke(state[\"messages\"])]}\n",
"\n",
"\n",
"graph_builder.add_node(\"chatbot\", chatbot)\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
"graph_builder.add_edge(\"chatbot\", END)\n",
@@ -187,7 +191,7 @@
}
],
"source": [
"input = {\"messages\": {\"role\":\"user\", \"content\":\"how many r's are in strawberry?\"}}\n",
"input = {\"messages\": {\"role\": \"user\", \"content\": \"how many r's are in strawberry?\"}}\n",
"async for event in graph.astream_events(input, version=\"v2\"):\n",
" if event[\"event\"] == \"on_chat_model_end\":\n",
" print(event[\"data\"][\"output\"].content, end=\"\", flush=True)"
@@ -94,12 +94,15 @@
"def step_2(state: State) -> State:\n",
" # Let's optionally raise a NodeInterrupt\n",
" # if the length of the input is longer than 5 characters\n",
" if len(state['input']) > 5:\n",
" raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n",
" \n",
" if len(state[\"input\"]) > 5:\n",
" raise NodeInterrupt(\n",
" f\"Received input that is longer than 5 characters: {state['input']}\"\n",
" )\n",
"\n",
" print(\"---Step 2---\")\n",
" return state\n",
"\n",
"\n",
"def step_3(state: State) -> State:\n",
" print(\"---Step 3---\")\n",
" return state\n",
File diff suppressed because it is too large Load Diff
@@ -382,6 +382,7 @@
"\n",
"from pydantic import BaseModel\n",
"\n",
"\n",
"# We are going \"bind\" all tools to the model\n",
"# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n",
"# Since `bind_tools` takes in tools but also just tool definitions,\n",
@@ -396,6 +397,7 @@
"\n",
"# Define nodes and conditional edges\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
@@ -79,9 +79,11 @@
"class OutputState(TypedDict):\n",
" answer: str\n",
"\n",
"\n",
"class OverallState(InputState, OutputState):\n",
" pass\n",
"\n",
"\n",
"def answer_node(state: InputState):\n",
" return {\"answer\": \"bye\"}\n",
"\n",
@@ -163,7 +163,7 @@
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Next, we pass in the path map - all the possible nodes this edge could go to\n",
" ['action', END]\n",
" [\"action\", END],\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
@@ -154,7 +154,7 @@
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Next, we pass in the path map - all the possible nodes this edge could go to\n",
" ['action',END]\n",
" [\"action\", END],\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
@@ -291,7 +291,7 @@
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Next, we pass in the pathmap - all the possible nodes this edge could go to\n",
" ['action', END]\n",
" [\"action\", END],\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
+1 -5
View File
@@ -204,11 +204,7 @@
"workflow.add_node(\"tools\", tool_node)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" [\"tools\",END]\n",
")\n",
"workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n",
"workflow.add_edge(\"tools\", \"agent\")\n",
"\n",
"app = workflow.compile()"
@@ -102,7 +102,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 1,
"id": "1d36e782-80f4-4334-b7d7-ee4c79864480",
"metadata": {},
"outputs": [],
@@ -111,6 +111,7 @@
"from typing_extensions import Annotated\n",
"\n",
"from langchain_core.documents import Document\n",
"from langchain_core.messages import ToolMessage\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import InjectedState\n",
"\n",
@@ -316,7 +317,6 @@
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
File diff suppressed because one or more lines are too long
+1 -3
View File
@@ -256,9 +256,7 @@
" }\n",
" }\n",
"\n",
" checkpoint = serde.loads_typed(\n",
" (data[b\"type\"].decode(), data[b\"checkpoint\"])\n",
" )\n",
" checkpoint = serde.loads_typed((data[b\"type\"].decode(), data[b\"checkpoint\"]))\n",
" metadata = serde.loads(data[b\"metadata\"].decode())\n",
" parent_checkpoint_id = data.get(b\"parent_checkpoint_id\", b\"\").decode()\n",
" parent_config = (\n",
@@ -82,6 +82,7 @@
"from langchain_core.messages import BaseMessage\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" \"\"\"The state of the agent.\"\"\"\n",
"\n",
@@ -108,16 +109,18 @@
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" if any([city in location.lower() for city in ['sf','san francisco']]):\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\"\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = model.bind_tools(tools)"
@@ -145,13 +148,13 @@
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"# Define our tool node\n",
"def tool_node(state: AgentState):\n",
" outputs = []\n",
" for tool_call in state['messages'][-1].tool_calls:\n",
" tool_result = tools_by_name[tool_call[\"name\"]].invoke(\n",
" tool_call[\"args\"]\n",
" )\n",
" for tool_call in state[\"messages\"][-1].tool_calls:\n",
" tool_result = tools_by_name[tool_call[\"name\"]].invoke(tool_call[\"args\"])\n",
" outputs.append(\n",
" ToolMessage(\n",
" content=json.dumps(tool_result),\n",
@@ -161,17 +164,21 @@
" )\n",
" return {\"messages\": outputs}\n",
"\n",
"\n",
"# Define the node that calls the model\n",
"def call_model(\n",
" state: AgentState,\n",
" config: RunnableConfig,\n",
"):\n",
" # this is similar to customizing the create_react_agent with state_modifier, but is a lot more flexible\n",
" system_prompt = SystemMessage(\"You are a helpful AI assistant, please respond to the users query to the best of your ability!\")\n",
" response = model.invoke([system_prompt] + state['messages'], config)\n",
" system_prompt = SystemMessage(\n",
" \"You are a helpful AI assistant, please respond to the users query to the best of your ability!\"\n",
" )\n",
" response = model.invoke([system_prompt] + state[\"messages\"], config)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the conditional edge that determines whether to continue or not\n",
"def should_continue(state: AgentState):\n",
" messages = state[\"messages\"]\n",
@@ -308,6 +315,7 @@
" else:\n",
" message.pretty_print()\n",
"\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"what is the weather in sf\")]}\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
@@ -112,22 +112,28 @@
"outputs": [],
"source": [
"from pydantic import BaseModel, Field\n",
"from typing import Literal\n",
"from typing import Literal\n",
"from langchain_core.tools import tool\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.graph import MessagesState\n",
"\n",
"\n",
"class WeatherResponse(BaseModel):\n",
" \"\"\"Respond to the user with this\"\"\"\n",
"\n",
" temperature: float = Field(description=\"The temperature in fahrenheit\")\n",
" wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n",
" wind_directon: str = Field(\n",
" description=\"The direction of the wind in abbreviated form\"\n",
" )\n",
" wind_speed: float = Field(description=\"The speed of the wind in km/h\")\n",
"\n",
"# Inherit 'messages' key from MessagesState, which is a list of chat messages \n",
"\n",
"# Inherit 'messages' key from MessagesState, which is a list of chat messages\n",
"class AgentState(MessagesState):\n",
" # Final structured response from the agent\n",
" final_response: WeatherResponse\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
@@ -137,11 +143,12 @@
" return \"It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
" \n",
"\n",
"\n",
"tools = [get_weather]\n",
" \n",
"\n",
"model = ChatAnthropic(model=\"claude-3-opus-20240229\")\n",
" \n",
"\n",
"model_with_tools = model.bind_tools(tools)\n",
"model_with_structured_output = model.with_structured_output(WeatherResponse)"
]
@@ -170,33 +177,40 @@
"\n",
"tools = [get_weather, WeatherResponse]\n",
"\n",
"# Force the model to use tools by passing tool_choice=\"any\" \n",
"model_with_response_tool = model.bind_tools(tools,tool_choice=\"any\")\n",
"# Force the model to use tools by passing tool_choice=\"any\"\n",
"model_with_response_tool = model.bind_tools(tools, tool_choice=\"any\")\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state: AgentState):\n",
" response = model_with_response_tool.invoke(state['messages'])\n",
" response = model_with_response_tool.invoke(state[\"messages\"])\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function that responds to the user\n",
"def respond(state: AgentState):\n",
" # Construct the final answer from the arguments of the last tool call\n",
" response = WeatherResponse(**state['messages'][-1].tool_calls[0]['args'])\n",
" response = WeatherResponse(**state[\"messages\"][-1].tool_calls[0][\"args\"])\n",
" # We return the final answer\n",
" return {\"final_response\": response}\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: AgentState):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is only one tool call and it is the response tool call we respond to the user\n",
" if len(last_message.tool_calls) == 1 and last_message.tool_calls[0]['name'] == \"WeatherResponse\":\n",
" if (\n",
" len(last_message.tool_calls) == 1\n",
" and last_message.tool_calls[0][\"name\"] == \"WeatherResponse\"\n",
" ):\n",
" return \"respond\"\n",
" # Otherwise we will use the tool node again\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
@@ -239,7 +253,9 @@
"metadata": {},
"outputs": [],
"source": [
"answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']"
"answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})[\n",
" \"final_response\"\n",
"]"
]
},
{
@@ -292,21 +308,26 @@
"from langgraph.prebuilt import ToolNode\n",
"from langchain_core.messages import HumanMessage\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state: AgentState):\n",
" response = model_with_tools.invoke(state['messages'])\n",
" response = model_with_tools.invoke(state[\"messages\"])\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function that responds to the user\n",
"def respond(state: AgentState):\n",
" # We call the model with structured output in order to return the same format to the user every time\n",
" # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n",
" # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n",
" response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n",
" response = model_with_structured_output.invoke(\n",
" [HumanMessage(content=state[\"messages\"][-2].content)]\n",
" )\n",
" # We return the final answer\n",
" return {\"final_response\": response}\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: AgentState):\n",
" messages = state[\"messages\"]\n",
@@ -318,6 +339,7 @@
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
@@ -361,7 +383,9 @@
"metadata": {},
"outputs": [],
"source": [
"answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']"
"answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})[\n",
" \"final_response\"\n",
"]"
]
},
{
+2 -2
View File
@@ -139,7 +139,7 @@
"from langgraph.errors import GraphRecursionError\n",
"\n",
"try:\n",
" graph.invoke({\"aggregate\": []},{\"recursion_limit\":3})\n",
" graph.invoke({\"aggregate\": []}, {\"recursion_limit\": 3})\n",
"except GraphRecursionError:\n",
" print(\"Recursion Error\")"
]
@@ -169,7 +169,7 @@
],
"source": [
"try:\n",
" graph.invoke({\"aggregate\": []},{\"recursion_limit\":4})\n",
" graph.invoke({\"aggregate\": []}, {\"recursion_limit\": 4})\n",
"except GraphRecursionError:\n",
" print(\"Recursion Error\")"
]
@@ -59,29 +59,34 @@
"from langgraph.graph import StateGraph\n",
"from langgraph.graph import START, END\n",
"\n",
"\n",
"class State(TypedDict):\n",
" value: str\n",
" action_result: str\n",
"\n",
"\n",
"def router(state: State):\n",
" if state['value'] == \"end\":\n",
" if state[\"value\"] == \"end\":\n",
" return END\n",
" else:\n",
" return \"action\"\n",
"\n",
"\n",
"def decision_node(state):\n",
" return {'value':'keep going!'}\n",
" return {\"value\": \"keep going!\"}\n",
"\n",
"\n",
"def action_node(state: State):\n",
" # Do your action here ...\n",
" return {'action_result':'what a great result!'}\n",
" return {\"action_result\": \"what a great result!\"}\n",
"\n",
"\n",
"workflow = StateGraph(State)\n",
"workflow.add_node('decision',decision_node)\n",
"workflow.add_node('action',action_node)\n",
"workflow.add_edge(START,'decision')\n",
"workflow.add_conditional_edges('decision',router,['action',END])\n",
"workflow.add_edge('action','decision')\n",
"workflow.add_node(\"decision\", decision_node)\n",
"workflow.add_node(\"action\", action_node)\n",
"workflow.add_edge(START, \"decision\")\n",
"workflow.add_conditional_edges(\"decision\", router, [\"action\", END])\n",
"workflow.add_edge(\"action\", \"decision\")\n",
"app = workflow.compile()"
]
},
@@ -131,7 +136,7 @@
"from langgraph.errors import GraphRecursionError\n",
"\n",
"try:\n",
" app.invoke({\"value\":\"hi!\"})\n",
" app.invoke({\"value\": \"hi!\"})\n",
"except GraphRecursionError:\n",
" print(\"Recursion Error\")"
]
@@ -168,34 +173,39 @@
" def __call__(self, step: int) -> bool:\n",
" limit = self.config.get(\"recursion_limit\", 0)\n",
" return step >= limit - 2\n",
" \n",
"\n",
"\n",
"class State(TypedDict):\n",
" value: str\n",
" action_result: str\n",
" is_last_step: Annotated[bool, IsLastOrSecondToLastStepManager]\n",
"\n",
"\n",
"def router(state: State):\n",
" # Force the agent to end if it is on the last step\n",
" if state['is_last_step']:\n",
" if state[\"is_last_step\"]:\n",
" return END\n",
" if state['value'] == \"end\":\n",
" if state[\"value\"] == \"end\":\n",
" return END\n",
" else:\n",
" return \"action\"\n",
"\n",
"\n",
"def decision_node(state):\n",
" return {'value':'keep going!'}\n",
" return {\"value\": \"keep going!\"}\n",
"\n",
"\n",
"def action_node(state: State):\n",
" # Do your action here ...\n",
" return {'action_result':'what a great result!'}\n",
" return {\"action_result\": \"what a great result!\"}\n",
"\n",
"\n",
"workflow = StateGraph(State)\n",
"workflow.add_node('decision',decision_node)\n",
"workflow.add_node('action',action_node)\n",
"workflow.add_edge(START,'decision')\n",
"workflow.add_conditional_edges('decision',router,['action',END])\n",
"workflow.add_edge('action','decision')\n",
"workflow.add_node(\"decision\", decision_node)\n",
"workflow.add_node(\"action\", action_node)\n",
"workflow.add_edge(START, \"decision\")\n",
"workflow.add_conditional_edges(\"decision\", router, [\"action\", END])\n",
"workflow.add_edge(\"action\", \"decision\")\n",
"app = workflow.compile()"
]
},
@@ -216,7 +226,7 @@
}
],
"source": [
"app.invoke({\"value\":\"hi!\"})"
"app.invoke({\"value\": \"hi!\"})"
]
},
{
+3 -1
View File
@@ -136,8 +136,10 @@
" print(message)\n",
" else:\n",
" message.pretty_print()\n",
"\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"what is the weather in sf\")]}\n",
"config = {\"configurable\": {\"run_id\":\"12345\"}}\n",
"config = {\"configurable\": {\"run_id\": \"12345\"}}\n",
"\n",
"print_stream(graph.stream(inputs, config, stream_mode=\"values\"))"
]
+1
View File
@@ -231,6 +231,7 @@
"\n",
"from pydantic import BaseModel\n",
"\n",
"\n",
"class AgentState(BaseModel):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]"
]
+8 -4
View File
@@ -71,9 +71,10 @@
"from langgraph.graph import START, StateGraph, MessagesState, END\n",
"from langgraph.types import StreamWriter\n",
"\n",
"\n",
"async def my_node(\n",
" state: MessagesState, \n",
" writer: StreamWriter # <-- provide StreamWriter to write chunks to be streamed\n",
" state: MessagesState,\n",
" writer: StreamWriter, # <-- provide StreamWriter to write chunks to be streamed\n",
"):\n",
" chunks = [\n",
" \"Four\",\n",
@@ -87,11 +88,12 @@
" \"...\",\n",
" ]\n",
" for chunk in chunks:\n",
" # write the chunk to be streamed using stream_mode=custom \n",
" # write the chunk to be streamed using stream_mode=custom\n",
" writer(chunk)\n",
"\n",
" return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
@@ -184,6 +186,7 @@
"from langchain_core.runnables import RunnableConfig, RunnableLambda\n",
"from langchain_core.callbacks.manager import adispatch_custom_event\n",
"\n",
"\n",
"async def my_node(state: MessagesState, config: RunnableConfig):\n",
" chunks = [\n",
" \"Four\",\n",
@@ -200,11 +203,12 @@
" await adispatch_custom_event(\n",
" \"my_custom_event\",\n",
" {\"chunk\": chunk},\n",
" config=config # <-- propagate config\n",
" config=config, # <-- propagate config\n",
" )\n",
"\n",
" return {\"messages\": [AIMessage(content=\" \".join(chunks))]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
@@ -196,12 +196,19 @@
"\n",
"inputs = [HumanMessage(content=\"what is the weather in sf\")]\n",
"final_message = \"\"\n",
"async for msg, metadata in agent.astream({\"messages\": [(\"human\", \"what items are on the shelf?\")]}, stream_mode=\"messages\"):\n",
"async for msg, metadata in agent.astream(\n",
" {\"messages\": [(\"human\", \"what items are on the shelf?\")]}, stream_mode=\"messages\"\n",
"):\n",
" # Stream all messages from the tool node\n",
" if msg.content and not isinstance(msg,HumanMessage) and metadata['langgraph_node'] == 'tools' and not msg.name:\n",
" if (\n",
" msg.content\n",
" and not isinstance(msg, HumanMessage)\n",
" and metadata[\"langgraph_node\"] == \"tools\"\n",
" and not msg.name\n",
" ):\n",
" print(msg.content, end=\"|\", flush=True)\n",
" # Final message should come from our agent\n",
" if msg.content and metadata['langgraph_node'] == \"agent\":\n",
" if msg.content and metadata[\"langgraph_node\"] == \"agent\":\n",
" final_message += msg.content"
]
},
@@ -163,6 +163,7 @@
" response.id = last_ai_message.id\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"workflow.add_node(\"agent\", call_model)\n",
@@ -222,7 +223,8 @@
"source": [
"import warnings\n",
"from langchain_core._api import LangChainBetaWarning\n",
"warnings.filterwarnings('ignore', category=LangChainBetaWarning)"
"\n",
"warnings.filterwarnings(\"ignore\", category=LangChainBetaWarning)"
]
},
{
@@ -260,7 +262,11 @@
"\n",
"inputs = [HumanMessage(content=\"what is the weather in sf\")]\n",
"async for msg, metadata in app.astream({\"messages\": inputs}, stream_mode=\"messages\"):\n",
" if msg.content and not isinstance(msg,HumanMessage) and metadata['langgraph_node'] == 'final':\n",
" if (\n",
" msg.content\n",
" and not isinstance(msg, HumanMessage)\n",
" and metadata[\"langgraph_node\"] == \"final\"\n",
" ):\n",
" print(msg.content, end=\"|\", flush=True)"
]
},
+12 -4
View File
@@ -87,7 +87,7 @@
"def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n",
" if not left:\n",
" left = []\n",
" \n",
"\n",
" if not right:\n",
" right = []\n",
"\n",
@@ -141,6 +141,7 @@
" # subgraph keys\n",
" summary: str\n",
"\n",
"\n",
"def generate_summary(state: QuestionSummarizationState):\n",
" docs = state[\"logs\"]\n",
" # NOTE: you can implement custom summarization logic here\n",
@@ -255,7 +256,7 @@
" id=\"3\",\n",
" question=\"How do I create react agent in langgraph?\",\n",
" answer=\"from langgraph.prebuilt import create_react_agent\",\n",
" )\n",
" ),\n",
"]\n",
"\n",
"input = {\"raw_logs\": dummy_logs}"
@@ -335,11 +336,18 @@
"source": [
"# Format the namespace slightly nicer\n",
"def format_namespace(namespace):\n",
" return namespace[-1].split(':')[0]+' subgraph' if len(namespace) > 0 else 'parent graph'\n",
" return (\n",
" namespace[-1].split(\":\")[0] + \" subgraph\"\n",
" if len(namespace) > 0\n",
" else \"parent graph\"\n",
" )\n",
"\n",
"\n",
"for namespace, chunk in graph.stream(input, stream_mode=\"updates\", subgraphs=True):\n",
" node_name = list(chunk.keys())[0]\n",
" print(f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\")\n",
" print(\n",
" f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\"\n",
" )\n",
" print(chunk[node_name])"
]
},
@@ -99,6 +99,7 @@
" ensure_config,\n",
" get_callback_manager_for_config,\n",
")\n",
"\n",
"openai_client = AsyncOpenAI()\n",
"# define tool schema for openai tool calling\n",
"\n",
@@ -311,7 +312,10 @@
"from langchain_core.messages import AIMessageChunk\n",
"\n",
"first = True\n",
"async for msg, metadata in graph.astream({\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, stream_mode=\"messages\"):\n",
"async for msg, metadata in graph.astream(\n",
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]},\n",
" stream_mode=\"messages\",\n",
"):\n",
" if msg.content:\n",
" print(msg.content, end=\"|\", flush=True)\n",
"\n",
+2 -2
View File
@@ -339,7 +339,7 @@
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Next we pass in the path map - all the nodes this edge could go to\n",
" [\"tools\",END]\n",
" [\"tools\", END],\n",
")\n",
"\n",
"workflow.add_edge(\"tools\", \"agent\")\n",
@@ -414,7 +414,7 @@
"inputs = [HumanMessage(content=\"what is the weather in sf\")]\n",
"first = True\n",
"async for msg, metadata in app.astream({\"messages\": inputs}, stream_mode=\"messages\"):\n",
" if msg.content and not isinstance(msg,HumanMessage):\n",
" if msg.content and not isinstance(msg, HumanMessage):\n",
" print(msg.content, end=\"|\", flush=True)\n",
"\n",
" if isinstance(msg, AIMessageChunk):\n",
@@ -71,10 +71,12 @@
"class GrandChildState(TypedDict):\n",
" my_grandchild_key: str\n",
"\n",
"\n",
"def grandchild_1(state: GrandChildState) -> GrandChildState:\n",
" # NOTE: child or parent keys will not be accessible here\n",
" return {\"my_grandchild_key\": state[\"my_grandchild_key\"] + \", how are you\"}\n",
"\n",
"\n",
"grandchild = StateGraph(GrandChildState)\n",
"grandchild.add_node(\"grandchild_1\", grandchild_1)\n",
"\n",
@@ -194,11 +196,13 @@
"source": [
"class ParentState(TypedDict):\n",
" my_key: str\n",
" \n",
"\n",
"\n",
"def parent_1(state: ParentState) -> ParentState:\n",
" # NOTE: child or grandchild keys won't be accessible here\n",
" return {\"my_key\": \"hi \" + state[\"my_key\"]}\n",
"\n",
"\n",
"def parent_2(state: ParentState) -> ParentState:\n",
" return {\"my_key\": state[\"my_key\"] + \" bye!\"}\n",
"\n",
+9 -6
View File
@@ -97,7 +97,7 @@
"def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n",
" if not left:\n",
" left = []\n",
" \n",
"\n",
" if not right:\n",
" right = []\n",
"\n",
@@ -225,7 +225,7 @@
" id=\"3\",\n",
" question=\"How do I create react agent in langgraph?\",\n",
" answer=\"from langgraph.prebuilt import create_react_agent\",\n",
" )\n",
" ),\n",
"]\n",
"\n",
"\n",
@@ -323,6 +323,7 @@
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"# define a simple reducer\n",
"def reduce_list(left: list, right: list) -> list:\n",
" if not left:\n",
@@ -331,6 +332,7 @@
" right = []\n",
" return left + right\n",
"\n",
"\n",
"# define parent and child state\n",
"class ChildState(TypedDict):\n",
" name: str\n",
@@ -345,7 +347,7 @@
"# define a helper to build the graph\n",
"def make_graph(parent_schema, child_schema):\n",
" child_builder = StateGraph(child_schema)\n",
" \n",
"\n",
" child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
" child_builder.add_edge(START, \"child_start\")\n",
" child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
@@ -353,16 +355,16 @@
" child_builder.add_edge(\"child_start\", \"child_middle\")\n",
" child_builder.add_edge(\"child_middle\", \"child_end\")\n",
" child_builder.add_edge(\"child_end\", END)\n",
" \n",
"\n",
" builder = StateGraph(parent_schema)\n",
" \n",
"\n",
" builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
" builder.add_edge(START, \"grandparent\")\n",
" builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
" builder.add_node(\"child\", child_builder.compile())\n",
" builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
" builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
" \n",
"\n",
" # Add connections\n",
" builder.add_edge(\"grandparent\", \"parent\")\n",
" builder.add_edge(\"parent\", \"child\")\n",
@@ -508,6 +510,7 @@
"source": [
"import uuid\n",
"\n",
"\n",
"def reduce_list(left: list | None, right: list | None) -> list:\n",
" \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n",
" if not left:\n",
+78 -31
View File
@@ -89,6 +89,7 @@
" \"\"\"Get the weather for a specific city\"\"\"\n",
" return f\"It's sunny in {city}!\"\n",
"\n",
"\n",
"raw_model = ChatOpenAI()\n",
"model = raw_model.with_structured_output(get_weather)\n",
"\n",
@@ -98,11 +99,12 @@
"\n",
"\n",
"def model_node(state: SubGraphState):\n",
" result = model.invoke(state['messages'])\n",
" result = model.invoke(state[\"messages\"])\n",
" return {\"city\": result[\"city\"]}\n",
"\n",
"\n",
"def weather_node(state: SubGraphState):\n",
" result = get_weather.invoke({\"city\": state['city']})\n",
" result = get_weather.invoke({\"city\": state[\"city\"]})\n",
" return {\"messages\": [{\"role\": \"assistant\", \"content\": result}]}\n",
"\n",
"\n",
@@ -145,22 +147,26 @@
"class Router(TypedDict):\n",
" route: Literal[\"weather\", \"other\"]\n",
"\n",
"\n",
"router_model = raw_model.with_structured_output(Router)\n",
" \n",
"\n",
"\n",
"def router_node(state: RouterState):\n",
" system_message = \"Classify the incoming query as either about weather or not.\"\n",
" messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n",
" messages = [{\"role\": \"system\", \"content\": system_message}] + state[\"messages\"]\n",
" route = router_model.invoke(messages)\n",
" return {\"route\": route['route']}\n",
" return {\"route\": route[\"route\"]}\n",
"\n",
"\n",
"def normal_llm_node(state: RouterState):\n",
" response = raw_model.invoke(state['messages'])\n",
" response = raw_model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n",
" if state['route'] == \"weather\":\n",
"def route_after_prediction(\n",
" state: RouterState,\n",
") -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n",
" if state[\"route\"] == \"weather\":\n",
" return \"weather_graph\"\n",
" else:\n",
" return \"normal_llm_node\"\n",
@@ -421,7 +427,9 @@
"metadata": {},
"outputs": [],
"source": [
"parent_graph_state_before_subgraph = next(h for h in graph.get_state_history(config) if h.next == ('weather_graph',))"
"parent_graph_state_before_subgraph = next(\n",
" h for h in graph.get_state_history(config) if h.next == (\"weather_graph\",)\n",
")"
]
},
{
@@ -430,7 +438,11 @@
"metadata": {},
"outputs": [],
"source": [
"subgraph_state_before_model_node = next(h for h in graph.get_state_history(parent_graph_state_before_subgraph.tasks[0].state) if h.next == ('model_node',))\n",
"subgraph_state_before_model_node = next(\n",
" h\n",
" for h in graph.get_state_history(parent_graph_state_before_subgraph.tasks[0].state)\n",
" if h.next == (\"model_node\",)\n",
")\n",
"\n",
"# This pattern can be extended no matter how many levels deep - image model node was another subgraph in this case\n",
"# subsubgraph_stat_history = next(h for h in graph.get_state_history(subgraph_state_before_model_node.tasks[0].state) if h.next == ('my_subsubgraph_node',))"
@@ -486,7 +498,12 @@
}
],
"source": [
"for value in graph.stream(None, config=subgraph_state_before_model_node.config, stream_mode=\"values\", subgraphs=True):\n",
"for value in graph.stream(\n",
" None,\n",
" config=subgraph_state_before_model_node.config,\n",
" stream_mode=\"values\",\n",
" subgraphs=True,\n",
"):\n",
" print(value)"
]
},
@@ -546,7 +563,7 @@
],
"source": [
"state = graph.get_state(config, subgraphs=True)\n",
"state.values['messages']"
"state.values[\"messages\"]"
]
},
{
@@ -637,16 +654,22 @@
"source": [
"config = {\"configurable\": {\"thread_id\": \"14\"}}\n",
"inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n",
"for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n",
"for update in graph.stream(\n",
" inputs, config=config, stream_mode=\"updates\", subgraphs=True\n",
"):\n",
" print(update)\n",
"# Graph execution should stop before the weather node\n",
"print(\"interrupted!\")\n",
"state = graph.get_state(config, subgraphs=True)\n",
"# We update the state by passing in the message we want returned from the weather node, and make sure to use as_node\n",
"graph.update_state(state.tasks[0].state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n",
"graph.update_state(\n",
" state.tasks[0].state.config,\n",
" {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n",
" as_node=\"weather_node\",\n",
")\n",
"for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n",
" print(update)\n",
"print(graph.get_state(config).values['messages'])"
"print(graph.get_state(config).values[\"messages\"])"
]
},
{
@@ -679,16 +702,22 @@
"source": [
"config = {\"configurable\": {\"thread_id\": \"8\"}}\n",
"inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n",
"for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n",
"for update in graph.stream(\n",
" inputs, config=config, stream_mode=\"updates\", subgraphs=True\n",
"):\n",
" print(update)\n",
"# Graph execution should stop before the weather node\n",
"print(\"interrupted!\")\n",
"# We update the state by passing in the message we want returned from the weather graph, making sure to use as_node\n",
"# Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n",
"graph.update_state(config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_graph\")\n",
"graph.update_state(\n",
" config,\n",
" {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n",
" as_node=\"weather_graph\",\n",
")\n",
"for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n",
" print(update)\n",
"print(graph.get_state(config).values['messages'])"
"print(graph.get_state(config).values[\"messages\"])"
]
},
{
@@ -723,22 +752,26 @@
"class Router(TypedDict):\n",
" route: Literal[\"weather\", \"other\"]\n",
"\n",
"\n",
"router_model = raw_model.with_structured_output(Router)\n",
" \n",
"\n",
"\n",
"def router_node(state: RouterState):\n",
" system_message = \"Classify the incoming query as either about weather or not.\"\n",
" messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n",
" messages = [{\"role\": \"system\", \"content\": system_message}] + state[\"messages\"]\n",
" route = router_model.invoke(messages)\n",
" return {\"route\": route['route']}\n",
" return {\"route\": route[\"route\"]}\n",
"\n",
"\n",
"def normal_llm_node(state: RouterState):\n",
" response = raw_model.invoke(state['messages'])\n",
" response = raw_model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n",
" if state['route'] == \"weather\":\n",
"def route_after_prediction(\n",
" state: RouterState,\n",
") -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n",
" if state[\"route\"] == \"weather\":\n",
" return \"weather_graph\"\n",
" else:\n",
" return \"normal_llm_node\"\n",
@@ -765,24 +798,30 @@
"\n",
"memory = MemorySaver()\n",
"\n",
"\n",
"class GrandfatherState(MessagesState):\n",
" to_continue: bool\n",
" \n",
"\n",
"\n",
"def router_node(state: GrandfatherState):\n",
" # Dummy logic that will always continue\n",
" return {\"to_continue\": True}\n",
"\n",
"\n",
"def route_after_prediction(state: GrandfatherState):\n",
" if state['to_continue']:\n",
" if state[\"to_continue\"]:\n",
" return \"graph\"\n",
" else:\n",
" return END\n",
"\n",
"\n",
"grandparent_graph = StateGraph(GrandfatherState)\n",
"grandparent_graph.add_node(router_node)\n",
"grandparent_graph.add_node(\"graph\", graph)\n",
"grandparent_graph.add_edge(START, \"router_node\")\n",
"grandparent_graph.add_conditional_edges(\"router_node\", route_after_prediction, ['graph',END])\n",
"grandparent_graph.add_conditional_edges(\n",
" \"router_node\", route_after_prediction, [\"graph\", END]\n",
")\n",
"grandparent_graph.add_edge(\"graph\", END)\n",
"grandparent_graph = grandparent_graph.compile(checkpointer=MemorySaver())"
]
@@ -835,7 +874,9 @@
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n",
"for update in grandparent_graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n",
"for update in grandparent_graph.stream(\n",
" inputs, config=config, stream_mode=\"updates\", subgraphs=True\n",
"):\n",
" print(update)"
]
},
@@ -897,10 +938,16 @@
"grandparent_graph_state = state\n",
"parent_graph_state = grandparent_graph_state.tasks[0].state\n",
"subgraph_state = parent_graph_state.tasks[0].state\n",
"grandparent_graph.update_state(subgraph_state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n",
"for update in grandparent_graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n",
"grandparent_graph.update_state(\n",
" subgraph_state.config,\n",
" {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]},\n",
" as_node=\"weather_node\",\n",
")\n",
"for update in grandparent_graph.stream(\n",
" None, config=config, stream_mode=\"updates\", subgraphs=True\n",
"):\n",
" print(update)\n",
"print(grandparent_graph.get_state(config).values['messages'])"
"print(grandparent_graph.get_state(config).values[\"messages\"])"
]
},
{
+4 -16
View File
@@ -155,11 +155,7 @@
"workflow.add_node(\"tools\", tool_node)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" ['tools',END]\n",
")\n",
"workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n",
"workflow.add_edge(\"tools\", \"agent\")\n",
"\n",
"app = workflow.compile()"
@@ -296,6 +292,7 @@
"from langchain_core.output_parsers import StrOutputParser\n",
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"class HaikuRequest(BaseModel):\n",
" topic: list[str] = Field(\n",
" max_length=3,\n",
@@ -303,7 +300,6 @@
" )\n",
"\n",
"\n",
"\n",
"@tool\n",
"def master_haiku_generator(request: HaikuRequest):\n",
" \"\"\"Generates a haiku based on the provided topics.\"\"\"\n",
@@ -341,11 +337,7 @@
"workflow.add_node(\"tools\", tool_node)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" ['tools',END]\n",
")\n",
"workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n",
"workflow.add_edge(\"tools\", \"agent\")\n",
"\n",
"app = workflow.compile()\n",
@@ -483,11 +475,7 @@
"workflow.add_node(\"fallback_agent\", call_fallback_model)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" ['tools',END]\n",
")\n",
"workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n",
"workflow.add_conditional_edges(\"tools\", should_fallback)\n",
"workflow.add_edge(\"remove_failed_tool_call_attempt\", \"fallback_agent\")\n",
"workflow.add_edge(\"fallback_agent\", \"tools\")\n",
+1 -5
View File
@@ -335,11 +335,7 @@
"workflow.add_node(\"tools\", tool_node)\n",
"\n",
"workflow.add_edge(START, \"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" ['tools',END]\n",
")\n",
"workflow.add_conditional_edges(\"agent\", should_continue, [\"tools\", END])\n",
"workflow.add_edge(\"tools\", \"agent\")\n",
"\n",
"app = workflow.compile()"
+3 -1
View File
@@ -425,7 +425,9 @@
"try:\n",
" display(Image(app.get_graph().draw_png()))\n",
"except ImportError:\n",
" print(\"You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt\")"
" print(\n",
" \"You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt\"\n",
" )"
]
}
],
+1 -1
View File
@@ -6,4 +6,4 @@ title: Home
---
{!README.md!}
{!README.md!}
@@ -407,7 +407,7 @@
}
],
"source": [
"for chunk in simulation.stream({\"messages\":[]}):\n",
"for chunk in simulation.stream({\"messages\": []}):\n",
" # Print out all events aside from the final end chunk\n",
" if END not in chunk:\n",
" print(chunk)\n",
@@ -378,8 +378,8 @@
],
"source": [
"import uuid\n",
" \n",
"cached_human_responses = ['hi!','rag prompt','1 rag, 2 none, 3 no, 4 no','red','q']\n",
"\n",
"cached_human_responses = [\"hi!\", \"rag prompt\", \"1 rag, 2 none, 3 no, 4 no\", \"red\", \"q\"]\n",
"cached_response_index = 0\n",
"config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
"while True:\n",
@@ -196,7 +196,9 @@
"llm = ChatOpenAI(temperature=0, model=expt_llm)\n",
"code_gen_chain_oai = code_gen_prompt | llm.with_structured_output(code)\n",
"question = \"How do I build a RAG chain in LCEL?\"\n",
"solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})\n",
"solution = code_gen_chain_oai.invoke(\n",
" {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n",
")\n",
"solution"
]
},
@@ -618,7 +620,7 @@
],
"source": [
"question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n",
"solution = app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0, \"error\":\"\"})"
"solution = app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0, \"error\": \"\"})"
]
},
{
@@ -639,7 +641,7 @@
}
],
"source": [
"solution['generation']"
"solution[\"generation\"]"
]
},
{
@@ -764,7 +766,9 @@
"\n",
"def predict_langgraph(example: dict):\n",
" \"\"\"LangGraph\"\"\"\n",
" graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0, \"error\": \"\"})\n",
" graph = app.invoke(\n",
" {\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0, \"error\": \"\"}\n",
" )\n",
" solution = graph[\"generation\"]\n",
" return {\"imports\": solution.imports, \"code\": solution.code}"
]
@@ -107,6 +107,8 @@
" f.write(response.content)\n",
" # Backup - we will use this to \"reset\" our DB in each section\n",
" shutil.copy(local_file, backup_file)\n",
"\n",
"\n",
"# Convert the flights to present time for our tutorial\n",
"def update_dates(file):\n",
" shutil.copy(backup_file, file)\n",
@@ -151,6 +153,7 @@
"\n",
" return file\n",
"\n",
"\n",
"db = update_dates(local_file)"
]
},
@@ -2560,9 +2563,7 @@
"\n",
"\n",
"builder.add_conditional_edges(\n",
" \"assistant\",\n",
" route_tools,\n",
" [\"safe_tools\", \"sensitive_tools\", END]\n",
" \"assistant\", route_tools, [\"safe_tools\", \"sensitive_tools\", END]\n",
")\n",
"builder.add_edge(\"safe_tools\", \"assistant\")\n",
"builder.add_edge(\"sensitive_tools\", \"assistant\")\n",
@@ -3540,7 +3541,11 @@
"\n",
"builder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\n",
"builder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\n",
"builder.add_conditional_edges(\"update_flight\", route_update_flight, [\"update_flight_sensitive_tools\",\"update_flight_safe_tools\",\"leave_skill\",END])\n",
"builder.add_conditional_edges(\n",
" \"update_flight\",\n",
" route_update_flight,\n",
" [\"update_flight_sensitive_tools\", \"update_flight_safe_tools\", \"leave_skill\", END],\n",
")\n",
"\n",
"\n",
"# This node will be shared for exiting all specialized assistants\n",
@@ -3620,7 +3625,16 @@
"\n",
"builder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\n",
"builder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\n",
"builder.add_conditional_edges(\"book_car_rental\", route_book_car_rental, [\"book_car_rental_safe_tools\",\"book_car_rental_sensitive_tools\",\"leave_skill\",END])"
"builder.add_conditional_edges(\n",
" \"book_car_rental\",\n",
" route_book_car_rental,\n",
" [\n",
" \"book_car_rental_safe_tools\",\n",
" \"book_car_rental_sensitive_tools\",\n",
" \"leave_skill\",\n",
" END,\n",
" ],\n",
")"
]
},
{
@@ -3672,7 +3686,11 @@
"\n",
"builder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\n",
"builder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\n",
"builder.add_conditional_edges(\"book_hotel\", route_book_hotel, [\"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", END])"
"builder.add_conditional_edges(\n",
" \"book_hotel\",\n",
" route_book_hotel,\n",
" [\"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", END],\n",
")"
]
},
{
@@ -3725,7 +3743,11 @@
"\n",
"builder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\n",
"builder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\n",
"builder.add_conditional_edges(\"book_excursion\", route_book_excursion, [\"book_excursion_safe_tools\",\"book_excursion_sensitive_tools\",\"leave_skill\",END])"
"builder.add_conditional_edges(\n",
" \"book_excursion\",\n",
" route_book_excursion,\n",
" [\"book_excursion_safe_tools\", \"book_excursion_sensitive_tools\", \"leave_skill\", END],\n",
")"
]
},
{
+4 -2
View File
@@ -291,7 +291,7 @@
" return \"validator\"\n",
" return END\n",
"\n",
" builder.add_conditional_edges(\"llm\", route_validator, ['validator',END])\n",
" builder.add_conditional_edges(\"llm\", route_validator, [\"validator\", END])\n",
" builder.add_edge(\"fallback\", \"validator\")\n",
" max_attempts = retry_strategy.get(\"max_attempts\", 3)\n",
"\n",
@@ -307,7 +307,9 @@
" return \"fallback\"\n",
" return \"finalizer\"\n",
"\n",
" builder.add_conditional_edges(\"validator\", route_validation, [\"finalizer\", \"fallback\"])\n",
" builder.add_conditional_edges(\n",
" \"validator\", route_validation, [\"finalizer\", \"fallback\"]\n",
" )\n",
"\n",
" builder.add_edge(\"finalizer\", END)\n",
"\n",
+2
View File
@@ -2108,6 +2108,7 @@
"source": [
"from pydantic import BaseModel\n",
"\n",
"\n",
"class RequestAssistance(BaseModel):\n",
" \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n",
"\n",
@@ -2668,6 +2669,7 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.messages import AIMessage, ToolMessage\n",
"\n",
"# NOTE: you must use langchain-core >= 0.3 with Pydantic v2\n",
"from pydantic import BaseModel\n",
"from typing_extensions import TypedDict\n",
+45 -17
View File
@@ -142,6 +142,7 @@
" def normalized_score(self) -> float:\n",
" return self.score / 10.0\n",
"\n",
"\n",
"class Node:\n",
" def __init__(\n",
" self,\n",
@@ -476,12 +477,22 @@
" \"\"\"Generate the initial candidate response.\"\"\"\n",
" res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n",
" parsed = parser.invoke(res)\n",
" tool_responses = [tool_node.invoke(\n",
" {\"messages\": [\n",
" AIMessage(content=\"\",tool_calls=[{\"name\":r[\"type\"], \"args\":r[\"args\"], 'id':r['id']}]) \n",
" ]}\n",
" ) for r in parsed]\n",
" output_messages = [res] + [tr['messages'][0] for tr in tool_responses]\n",
" tool_responses = [\n",
" tool_node.invoke(\n",
" {\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=\"\",\n",
" tool_calls=[\n",
" {\"name\": r[\"type\"], \"args\": r[\"args\"], \"id\": r[\"id\"]}\n",
" ],\n",
" )\n",
" ]\n",
" }\n",
" )\n",
" for r in parsed\n",
" ]\n",
" output_messages = [res] + [tr[\"messages\"][0] for tr in tool_responses]\n",
" reflection = reflection_chain.invoke(\n",
" {\"input\": state[\"input\"], \"candidate\": output_messages}\n",
" )\n",
@@ -575,12 +586,13 @@
"source": [
"from collections import defaultdict\n",
"\n",
"\n",
"def select(root: Node) -> dict:\n",
" \"\"\"Starting from the root node a child node is selected at each tree level until a leaf node is reached.\"\"\"\n",
"\n",
" if not root.children:\n",
" return root\n",
" \n",
"\n",
" node = root\n",
" while node.children:\n",
" max_child = max(node.children, key=lambda child: child.upper_confidence_bound())\n",
@@ -588,6 +600,7 @@
"\n",
" return node\n",
"\n",
"\n",
"def expand(state: TreeState, config: RunnableConfig) -> dict:\n",
" \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n",
" root = state[\"root\"]\n",
@@ -603,16 +616,31 @@
" for i, tool_calls in enumerate(parsed)\n",
" for tool_call in tool_calls\n",
" ]\n",
" tool_responses = [(i,tool_node.invoke(\n",
" {\"messages\":\n",
" [AIMessage(content=\"\",tool_calls=[{\"name\":tool_call[\"type\"], \"args\":tool_call[\"args\"], 'id':tool_call['id']}])]\n",
" }\n",
" )) for i, tool_call in flattened]\n",
" tool_responses = [\n",
" (\n",
" i,\n",
" tool_node.invoke(\n",
" {\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=\"\",\n",
" tool_calls=[\n",
" {\n",
" \"name\": tool_call[\"type\"],\n",
" \"args\": tool_call[\"args\"],\n",
" \"id\": tool_call[\"id\"],\n",
" }\n",
" ],\n",
" )\n",
" ]\n",
" }\n",
" ),\n",
" )\n",
" for i, tool_call in flattened\n",
" ]\n",
" collected_responses = defaultdict(list)\n",
" for i, resp in tool_responses:\n",
" collected_responses[i].append(\n",
" resp['messages'][0]\n",
" )\n",
" collected_responses[i].append(resp[\"messages\"][0])\n",
" output_messages = []\n",
" for i, candidate in enumerate(new_candidates):\n",
" output_messages.append([candidate] + collected_responses[i])\n",
@@ -675,13 +703,13 @@
" \"start\",\n",
" # Either expand/rollout or finish\n",
" should_loop,\n",
" ['expand',END]\n",
" [\"expand\", END],\n",
")\n",
"builder.add_conditional_edges(\n",
" \"expand\",\n",
" # Either continue to rollout or finish\n",
" should_loop,\n",
" ['expand',END]\n",
" [\"expand\", END],\n",
")\n",
"\n",
"graph = builder.compile()"
@@ -859,8 +859,7 @@
" args_for_tasks[task[\"idx\"]] = task[\"args\"]\n",
" if (\n",
" # Depends on other tasks\n",
" deps\n",
" and (any([dep not in observations for dep in deps]))\n",
" deps and (any([dep not in observations for dep in deps]))\n",
" ):\n",
" futures.append(\n",
" executor.submit(\n",
@@ -883,7 +882,10 @@
" }\n",
" tool_messages = [\n",
" FunctionMessage(\n",
" name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}, tool_call_id = k\n",
" name=name,\n",
" content=str(obs),\n",
" additional_kwargs={\"idx\": k, \"args\": task_args},\n",
" tool_call_id=k,\n",
" )\n",
" for k, (name, task_args, obs) in new_observations.items()\n",
" ]\n",
@@ -936,7 +938,9 @@
"metadata": {},
"outputs": [],
"source": [
"tool_messages = plan_and_schedule.invoke({\"messages\":[HumanMessage(content=example_question)]})['messages']"
"tool_messages = plan_and_schedule.invoke(\n",
" {\"messages\": [HumanMessage(content=example_question)]}\n",
")[\"messages\"]"
]
},
{
@@ -1050,11 +1054,13 @@
"def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n",
" response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n",
" if isinstance(decision.action, Replan):\n",
" return {\"messages\": response + [\n",
" SystemMessage(\n",
" content=f\"Context from last attempt: {decision.action.feedback}\"\n",
" )\n",
" ]\n",
" return {\n",
" \"messages\": response\n",
" + [\n",
" SystemMessage(\n",
" content=f\"Context from last attempt: {decision.action.feedback}\"\n",
" )\n",
" ]\n",
" }\n",
" else:\n",
" return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n",
@@ -1102,7 +1108,7 @@
}
],
"source": [
"joiner.invoke({\"messages\":input_messages})"
"joiner.invoke({\"messages\": input_messages})"
]
},
{
@@ -1217,7 +1223,7 @@
],
"source": [
"# Final answer\n",
"print(step['join']['messages'][-1].content)"
"print(step[\"join\"][\"messages\"][-1].content)"
]
},
{
@@ -1248,12 +1254,13 @@
}
],
"source": [
"steps = chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n",
" )\n",
" ]\n",
"steps = chain.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n",
" )\n",
" ]\n",
" },\n",
" {\n",
" \"recursion_limit\": 100,\n",
@@ -1280,7 +1287,7 @@
],
"source": [
"# Final answer\n",
"print(step['join']['messages'][-1].content)"
"print(step[\"join\"][\"messages\"][-1].content)"
]
},
{
@@ -1307,12 +1314,14 @@
}
],
"source": [
"for step in chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n",
" )\n",
" ]}\n",
"for step in chain.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n",
" )\n",
" ]\n",
" }\n",
"):\n",
" print(step)"
]
@@ -1335,7 +1344,7 @@
],
"source": [
"# Final answer\n",
"print(step['join']['messages'][-1].content)"
"print(step[\"join\"][\"messages\"][-1].content)"
]
},
{
@@ -1364,12 +1373,14 @@
}
],
"source": [
"for step in chain.stream({\"messages\":\n",
" [\n",
" HumanMessage(\n",
" content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n",
" )\n",
" ]}\n",
"for step in chain.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n",
" )\n",
" ]\n",
" }\n",
"):\n",
" print(step)"
]
@@ -158,7 +158,7 @@
" user_id = config[\"configurable\"].get(\"user_id\")\n",
" if user_id is None:\n",
" raise ValueError(\"User ID needs to be provided to save a memory.\")\n",
" \n",
"\n",
" return user_id\n",
"\n",
"\n",
@@ -166,7 +166,9 @@
"def save_recall_memory(memory: str, config: RunnableConfig) -> str:\n",
" \"\"\"Save memory to vectorstore for later semantic retrieval.\"\"\"\n",
" user_id = get_user_id(config)\n",
" document = Document(page_content=memory, id=str(uuid.uuid4()), metadata={\"user_id\": user_id})\n",
" document = Document(\n",
" page_content=memory, id=str(uuid.uuid4()), metadata={\"user_id\": user_id}\n",
" )\n",
" recall_vector_store.add_documents([document])\n",
" return memory\n",
"\n",
@@ -175,10 +177,13 @@
"def search_recall_memories(query: str, config: RunnableConfig) -> List[str]:\n",
" \"\"\"Search for relevant memories.\"\"\"\n",
" user_id = get_user_id(config)\n",
"\n",
" def _filter_function(doc: Document) -> bool:\n",
" return doc.metadata.get(\"user_id\") == user_id\n",
"\n",
" documents = recall_vector_store.similarity_search(query, k=3, filter=_filter_function)\n",
" documents = recall_vector_store.similarity_search(\n",
" query, k=3, filter=_filter_function\n",
" )\n",
" return [document.page_content for document in documents]"
]
},
@@ -281,7 +286,7 @@
" \" information you want to retain in the next conversation. If you\"\n",
" \" do call tools, all text preceding the tool call is an internal\"\n",
" \" message. Respond AFTER calling the tool, once you have\"\n",
" \" confirmation that the tool completed successfully.\\n\\n\"\n",
" \" confirmation that the tool completed successfully.\\n\\n\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
@@ -300,6 +305,7 @@
"\n",
"tokenizer = tiktoken.encoding_for_model(\"gpt-4o\")\n",
"\n",
"\n",
"def agent(state: State) -> State:\n",
" \"\"\"Process the current state and generate a response using the LLM.\n",
"\n",
@@ -354,7 +360,7 @@
" msg = state[\"messages\"][-1]\n",
" if msg.tool_calls:\n",
" return \"tools\"\n",
" \n",
"\n",
" return END"
]
},
@@ -594,7 +600,10 @@
}
],
"source": [
"for chunk in graph.stream({\"messages\": [(\"user\", \"yes -- pepperoni!\")]}, config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}}):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"yes -- pepperoni!\")]},\n",
" config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -638,7 +647,10 @@
}
],
"source": [
"for chunk in graph.stream({\"messages\": [(\"user\", \"i also just moved to new york\")]}, config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}}):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"i also just moved to new york\")]},\n",
" config={\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"1\"}},\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -682,7 +694,9 @@
"source": [
"config = {\"configurable\": {\"user_id\": \"1\", \"thread_id\": \"2\"}}\n",
"\n",
"for chunk in graph.stream({\"messages\": [(\"user\", \"where should i go for dinner?\")]}, config=config):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"where should i go for dinner?\")]}, config=config\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -740,7 +754,10 @@
}
],
"source": [
"for chunk in graph.stream({\"messages\": [(\"user\", \"what's the address for joe's in greenwich village?\")]}, config=config):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"what's the address for joe's in greenwich village?\")]},\n",
" config=config,\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -924,7 +941,9 @@
}
],
"source": [
"for chunk in graph.stream({\"messages\": [(\"user\", \"My friend John likes Pizza.\")]}, config=config):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"My friend John likes Pizza.\")]}, config=config\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -962,7 +981,9 @@
"source": [
"config = {\"configurable\": {\"user_id\": \"3\", \"thread_id\": \"2\"}}\n",
"\n",
"for chunk in graph.stream({\"messages\": [(\"user\", \"What food should I bring to John's party?\")]}, config=config):\n",
"for chunk in graph.stream(\n",
" {\"messages\": [(\"user\", \"What food should I bring to John's party?\")]}, config=config\n",
"):\n",
" pretty_print_stream_chunk(chunk)"
]
},
@@ -1007,7 +1028,9 @@
"\n",
"\n",
"# Fetch records\n",
"records = recall_vector_store.similarity_search(\"Alice\", k=2, filter=lambda doc: doc.metadata[\"user_id\"] == \"3\")\n",
"records = recall_vector_store.similarity_search(\n",
" \"Alice\", k=2, filter=lambda doc: doc.metadata[\"user_id\"] == \"3\"\n",
")\n",
"\n",
"\n",
"# Plot graph\n",
@@ -124,9 +124,12 @@
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"\n",
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}"
" return {\n",
" \"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]\n",
" }"
]
},
{
@@ -163,9 +166,11 @@
"# and decides when the work is completed\n",
"options = [\"FINISH\"] + members\n",
"\n",
"\n",
"class routeResponse(BaseModel):\n",
" next: Literal[*options]\n",
"\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt),\n",
@@ -181,11 +186,9 @@
"\n",
"llm = ChatOpenAI(model=\"gpt-4o\")\n",
"\n",
"\n",
"def supervisor_agent(state):\n",
" supervisor_chain = (\n",
" prompt\n",
" | llm.with_structured_output(routeResponse)\n",
" )\n",
" supervisor_chain = prompt | llm.with_structured_output(routeResponse)\n",
" return supervisor_chain.invoke(state)"
]
},
@@ -216,6 +219,7 @@
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"# The agent state is the input to each node in the graph\n",
"class AgentState(TypedDict):\n",
" # The annotation tells the graph that new messages will always\n",
@@ -305,7 +305,9 @@
"\n",
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}\n",
" return {\n",
" \"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]\n",
" }\n",
"\n",
"\n",
"def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n",
@@ -340,7 +342,8 @@
" ]\n",
" ).partial(options=str(options), team_members=\", \".join(members))\n",
" return (\n",
" prompt | trimmer\n",
" prompt\n",
" | trimmer\n",
" | llm.bind_functions(functions=[function_def], function_call=\"route\")\n",
" | JsonOutputFunctionsParser()\n",
" )"
@@ -379,6 +382,7 @@
"from langchain_openai.chat_models import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"# ResearchTeam graph state\n",
"class ResearchTeamState(TypedDict):\n",
" # A message is added after each team member finishes\n",
@@ -595,14 +599,16 @@
"\n",
"llm = ChatOpenAI(model=\"gpt-4o\")\n",
"\n",
"doc_writer_agent = create_react_agent(llm, tools=[write_document, edit_document, read_document])\n",
"doc_writer_agent = create_react_agent(\n",
" llm, tools=[write_document, edit_document, read_document]\n",
")\n",
"# Injects current directory working state before each call\n",
"context_aware_doc_writer_agent = prelude | doc_writer_agent\n",
"doc_writing_node = functools.partial(\n",
" agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n",
")\n",
"\n",
"note_taking_agent = create_react_agent(llm,tools=[create_outline, read_document])\n",
"note_taking_agent = create_react_agent(llm, tools=[create_outline, read_document])\n",
"context_aware_note_taking_agent = prelude | note_taking_agent\n",
"note_taking_node = functools.partial(\n",
" agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n",
@@ -393,6 +393,7 @@
"from typing import Literal\n",
"from langgraph.graph import END\n",
"\n",
"\n",
"async def execute_step(state: PlanExecute):\n",
" plan = state[\"plan\"]\n",
" plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n",
@@ -459,7 +460,7 @@
" \"replan\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_end,\n",
" [\"agent\",END]\n",
" [\"agent\", END],\n",
")\n",
"\n",
"# Finally, we compile it!\n",
@@ -185,7 +185,6 @@
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"\n",
"# Data model\n",
"class RouteQuery(BaseModel):\n",
" \"\"\"Route a user query to the most relevant datasource.\"\"\"\n",
@@ -59,9 +59,10 @@
"source": [
"### LLM\n",
"from langchain_ollama import ChatOllama\n",
"local_llm = 'llama3.2:3b-instruct-fp16'\n",
"\n",
"local_llm = \"llama3.2:3b-instruct-fp16\"\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"llm_json_mode = ChatOllama(model=local_llm, temperature=0, format='json')"
"llm_json_mode = ChatOllama(model=local_llm, temperature=0, format=\"json\")"
]
},
{
@@ -76,19 +77,22 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "8a8792f5",
"metadata": {},
"outputs": [],
"source": [
"import os, getpass\n",
"import os\n",
"import getpass\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"TAVILY_API_KEY\")\n",
"os.environ['TOKENIZERS_PARALLELISM'] = 'true'"
"os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\""
]
},
{
@@ -194,7 +198,7 @@
"import json\n",
"from langchain_core.messages import HumanMessage, SystemMessage\n",
"\n",
"# Prompt \n",
"# Prompt\n",
"router_instructions = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n",
"\n",
"The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n",
@@ -204,10 +208,27 @@
"Return JSON with single key, datasource, that is 'websearch' or 'vectorstore' depending on the question.\"\"\"\n",
"\n",
"# Test router\n",
"test_web_search = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"Who is favored to win the NFC Championship game in the 2024 season?\")])\n",
"test_web_search_2 = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"What are the models released today for llama3.2?\")])\n",
"test_vector_store = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=\"What are the types of agent memory?\")])\n",
"print(json.loads(test_web_search.content), json.loads(test_web_search_2.content), json.loads(test_vector_store.content))"
"test_web_search = llm_json_mode.invoke(\n",
" [SystemMessage(content=router_instructions)]\n",
" + [\n",
" HumanMessage(\n",
" content=\"Who is favored to win the NFC Championship game in the 2024 season?\"\n",
" )\n",
" ]\n",
")\n",
"test_web_search_2 = llm_json_mode.invoke(\n",
" [SystemMessage(content=router_instructions)]\n",
" + [HumanMessage(content=\"What are the models released today for llama3.2?\")]\n",
")\n",
"test_vector_store = llm_json_mode.invoke(\n",
" [SystemMessage(content=router_instructions)]\n",
" + [HumanMessage(content=\"What are the types of agent memory?\")]\n",
")\n",
"print(\n",
" json.loads(test_web_search.content),\n",
" json.loads(test_web_search_2.content),\n",
" json.loads(test_vector_store.content),\n",
")"
]
},
{
@@ -228,9 +249,9 @@
}
],
"source": [
"### Retrieval Grader \n",
"### Retrieval Grader\n",
"\n",
"# Doc grader instructions \n",
"# Doc grader instructions\n",
"doc_grader_instructions = \"\"\"You are a grader assessing relevance of a retrieved document to a user question.\n",
"\n",
"If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant.\"\"\"\n",
@@ -246,8 +267,13 @@
"question = \"What is Chain of thought prompting?\"\n",
"docs = retriever.invoke(question)\n",
"doc_txt = docs[1].page_content\n",
"doc_grader_prompt_formatted = doc_grader_prompt.format(document=doc_txt, question=question)\n",
"result = llm_json_mode.invoke([SystemMessage(content=doc_grader_instructions)] + [HumanMessage(content=doc_grader_prompt_formatted)])\n",
"doc_grader_prompt_formatted = doc_grader_prompt.format(\n",
" document=doc_txt, question=question\n",
")\n",
"result = llm_json_mode.invoke(\n",
" [SystemMessage(content=doc_grader_instructions)]\n",
" + [HumanMessage(content=doc_grader_prompt_formatted)]\n",
")\n",
"json.loads(result.content)"
]
},
@@ -287,10 +313,12 @@
"\n",
"Answer:\"\"\"\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Test\n",
"docs = retriever.invoke(question)\n",
"docs_txt = format_docs(docs)\n",
@@ -318,9 +346,9 @@
}
],
"source": [
"### Hallucination Grader \n",
"### Hallucination Grader\n",
"\n",
"# Hallucination grader instructions \n",
"# Hallucination grader instructions\n",
"hallucination_grader_instructions = \"\"\"\n",
"\n",
"You are a teacher grading a quiz. \n",
@@ -348,9 +376,14 @@
"\n",
"Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER is grounded in the FACTS. And a key, explanation, that contains an explanation of the score.\"\"\"\n",
"\n",
"# Test using documents and generation from above \n",
"hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(documents=docs_txt, generation=generation.content)\n",
"result = llm_json_mode.invoke([SystemMessage(content=hallucination_grader_instructions)] + [HumanMessage(content=hallucination_grader_prompt_formatted)])\n",
"# Test using documents and generation from above\n",
"hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(\n",
" documents=docs_txt, generation=generation.content\n",
")\n",
"result = llm_json_mode.invoke(\n",
" [SystemMessage(content=hallucination_grader_instructions)]\n",
" + [HumanMessage(content=hallucination_grader_prompt_formatted)]\n",
")\n",
"json.loads(result.content)"
]
},
@@ -373,9 +406,9 @@
}
],
"source": [
"### Answer Grader \n",
"### Answer Grader\n",
"\n",
"# Answer grader instructions \n",
"# Answer grader instructions\n",
"answer_grader_instructions = \"\"\"You are a teacher grading a quiz. \n",
"\n",
"You will be given a QUESTION and a STUDENT ANSWER. \n",
@@ -401,13 +434,18 @@
"\n",
"Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER meets the criteria. And a key, explanation, that contains an explanation of the score.\"\"\"\n",
"\n",
"# Test \n",
"# Test\n",
"question = \"What are the vision models released today as part of Llama 3.2?\"\n",
"answer = \"The Llama 3.2 models released today include two vision models: Llama 3.2 11B Vision Instruct and Llama 3.2 90B Vision Instruct, which are available on Azure AI Model Catalog via managed compute. These models are part of Meta's first foray into multimodal AI and rival closed models like Anthropic's Claude 3 Haiku and OpenAI's GPT-4o mini in visual reasoning. They replace the older text-only Llama 3.1 models.\"\n",
"\n",
"# Test using question and generation from above \n",
"answer_grader_prompt_formatted = answer_grader_prompt.format(question=question, generation=answer)\n",
"result = llm_json_mode.invoke([SystemMessage(content=answer_grader_instructions)] + [HumanMessage(content=answer_grader_prompt_formatted)])\n",
"# Test using question and generation from above\n",
"answer_grader_prompt_formatted = answer_grader_prompt.format(\n",
" question=question, generation=answer\n",
")\n",
"result = llm_json_mode.invoke(\n",
" [SystemMessage(content=answer_grader_instructions)]\n",
" + [HumanMessage(content=answer_grader_prompt_formatted)]\n",
")\n",
"json.loads(result.content)"
]
},
@@ -428,6 +466,7 @@
"source": [
"### Search\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
},
@@ -461,17 +500,19 @@
"from typing_extensions import TypedDict\n",
"from typing import List, Annotated\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Graph state is a dictionary that contains information we want to propagate to, and modify in, each graph node.\n",
" \"\"\"\n",
" question : str # User question\n",
" generation : str # LLM generation\n",
" web_search : str # Binary decision to run web search\n",
" max_retries : int # Max number of retries for answer generation \n",
" answers : int # Number of answers generated\n",
" loop_step: Annotated[int, operator.add] \n",
" documents : List[str] # List of retrieved documents"
"\n",
" question: str # User question\n",
" generation: str # LLM generation\n",
" web_search: str # Binary decision to run web search\n",
" max_retries: int # Max number of retries for answer generation\n",
" answers: int # Number of answers generated\n",
" loop_step: Annotated[int, operator.add]\n",
" documents: List[str] # List of retrieved documents"
]
},
{
@@ -504,6 +545,7 @@
"from langchain.schema import Document\n",
"from langgraph.graph import END\n",
"\n",
"\n",
"### Nodes\n",
"def retrieve(state):\n",
" \"\"\"\n",
@@ -522,6 +564,7 @@
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer using RAG on retrieved documents\n",
@@ -536,12 +579,13 @@
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" loop_step = state.get(\"loop_step\", 0)\n",
" \n",
"\n",
" # RAG generation\n",
" docs_txt = format_docs(documents)\n",
" rag_prompt_formatted = rag_prompt.format(context=docs_txt, question=question)\n",
" generation = llm.invoke([HumanMessage(content=rag_prompt_formatted)])\n",
" return {\"generation\": generation, \"loop_step\": loop_step+1}\n",
" return {\"generation\": generation, \"loop_step\": loop_step + 1}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
@@ -558,14 +602,19 @@
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" \n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" web_search = \"No\" \n",
" web_search = \"No\"\n",
" for d in documents:\n",
" doc_grader_prompt_formatted = doc_grader_prompt.format(document=d.page_content, question=question)\n",
" result = llm_json_mode.invoke([SystemMessage(content=doc_grader_instructions)] + [HumanMessage(content=doc_grader_prompt_formatted)])\n",
" grade = json.loads(result.content)['binary_score']\n",
" doc_grader_prompt_formatted = doc_grader_prompt.format(\n",
" document=d.page_content, question=question\n",
" )\n",
" result = llm_json_mode.invoke(\n",
" [SystemMessage(content=doc_grader_instructions)]\n",
" + [HumanMessage(content=doc_grader_prompt_formatted)]\n",
" )\n",
" grade = json.loads(result.content)[\"binary_score\"]\n",
" # Document relevant\n",
" if grade.lower() == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
@@ -578,7 +627,8 @@
" web_search = \"Yes\"\n",
" continue\n",
" return {\"documents\": filtered_docs, \"web_search\": web_search}\n",
" \n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based based on the question\n",
@@ -601,11 +651,13 @@
" documents.append(web_results)\n",
" return {\"documents\": documents}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def route_question(state):\n",
" \"\"\"\n",
" Route question to web search or RAG \n",
" Route question to web search or RAG\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
@@ -615,15 +667,19 @@
" \"\"\"\n",
"\n",
" print(\"---ROUTE QUESTION---\")\n",
" route_question = llm_json_mode.invoke([SystemMessage(content=router_instructions)] + [HumanMessage(content=state[\"question\"])])\n",
" source = json.loads(route_question.content)['datasource']\n",
" if source == 'websearch':\n",
" route_question = llm_json_mode.invoke(\n",
" [SystemMessage(content=router_instructions)]\n",
" + [HumanMessage(content=state[\"question\"])]\n",
" )\n",
" source = json.loads(route_question.content)[\"datasource\"]\n",
" if source == \"websearch\":\n",
" print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
" return \"websearch\"\n",
" elif source == 'vectorstore':\n",
" elif source == \"vectorstore\":\n",
" print(\"---ROUTE QUESTION TO RAG---\")\n",
" return \"vectorstore\"\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or add web search\n",
@@ -643,13 +699,16 @@
" if web_search == \"Yes\":\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\"---DECISION: NOT ALL DOCUMENTS ARE RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\")\n",
" print(\n",
" \"---DECISION: NOT ALL DOCUMENTS ARE RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n",
" )\n",
" return \"websearch\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"def grade_generation_v_documents_and_question(state):\n",
" \"\"\"\n",
" Determines whether the generation is grounded in the document and answers question\n",
@@ -665,21 +724,31 @@
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
" max_retries = state.get(\"max_retries\", 3) # Default to 3 if not provided\n",
" max_retries = state.get(\"max_retries\", 3) # Default to 3 if not provided\n",
"\n",
" hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(documents=format_docs(documents), generation=generation.content)\n",
" result = llm_json_mode.invoke([SystemMessage(content=hallucination_grader_instructions)] + [HumanMessage(content=hallucination_grader_prompt_formatted)])\n",
" grade = json.loads(result.content)['binary_score']\n",
" hallucination_grader_prompt_formatted = hallucination_grader_prompt.format(\n",
" documents=format_docs(documents), generation=generation.content\n",
" )\n",
" result = llm_json_mode.invoke(\n",
" [SystemMessage(content=hallucination_grader_instructions)]\n",
" + [HumanMessage(content=hallucination_grader_prompt_formatted)]\n",
" )\n",
" grade = json.loads(result.content)[\"binary_score\"]\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
" # Check question-answering\n",
" print(\"---GRADE GENERATION vs QUESTION---\")\n",
" # Test using question and generation from above \n",
" answer_grader_prompt_formatted = answer_grader_prompt.format(question=question, generation=generation.content)\n",
" result = llm_json_mode.invoke([SystemMessage(content=answer_grader_instructions)] + [HumanMessage(content=answer_grader_prompt_formatted)])\n",
" grade = json.loads(result.content)['binary_score']\n",
" # Test using question and generation from above\n",
" answer_grader_prompt_formatted = answer_grader_prompt.format(\n",
" question=question, generation=generation.content\n",
" )\n",
" result = llm_json_mode.invoke(\n",
" [SystemMessage(content=answer_grader_instructions)]\n",
" + [HumanMessage(content=answer_grader_prompt_formatted)]\n",
" )\n",
" grade = json.loads(result.content)[\"binary_score\"]\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
@@ -688,7 +757,7 @@
" return \"not useful\"\n",
" else:\n",
" print(\"---DECISION: MAX RETRIES REACHED---\")\n",
" return \"max retries\" \n",
" return \"max retries\"\n",
" elif state[\"loop_step\"] <= max_retries:\n",
" print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\"\n",
@@ -729,10 +798,10 @@
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"websearch\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generate\n",
"workflow.add_node(\"websearch\", web_search) # web search\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generate\n",
"\n",
"# Build graph\n",
"workflow.set_conditional_entry_point(\n",
@@ -798,7 +867,10 @@
"outputs": [],
"source": [
"# Test on current events\n",
"inputs = {\"question\": \"What are the models released today for llama3.2?\", \"max_retries\": 3}\n",
"inputs = {\n",
" \"question\": \"What are the models released today for llama3.2?\",\n",
" \"max_retries\": 3,\n",
"}\n",
"for event in graph.stream(inputs, stream_mode=\"values\"):\n",
" print(event)"
]
@@ -836,7 +908,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
"version": "3.11.4"
}
},
"nbformat": 4,
@@ -699,7 +699,9 @@
" \"\"\"\n",
" Find all tool calls in the messages returned\n",
" \"\"\"\n",
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
" tool_calls = [\n",
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
" ]\n",
" return tool_calls\n",
"\n",
"\n",
+15 -13
View File
@@ -58,6 +58,7 @@
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"_set_if_undefined(\"FIREWORKS_API_KEY\")"
]
@@ -108,8 +109,7 @@
" ]\n",
")\n",
"llm = ChatFireworks(\n",
" model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n",
" max_tokens=32768\n",
" model=\"accounts/fireworks/models/mixtral-8x7b-instruct\", max_tokens=32768\n",
")\n",
"generate = prompt | llm"
]
@@ -331,15 +331,15 @@
"\n",
"\n",
"async def generation_node(state: State) -> State:\n",
" return {\"messages\": [await generate.ainvoke(state['messages'])]}\n",
" return {\"messages\": [await generate.ainvoke(state[\"messages\"])]}\n",
"\n",
"\n",
"async def reflection_node(state: State) -> State:\n",
" # Other messages we need to adjust\n",
" cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n",
" # First message is the original user request. We hold it the same for all nodes\n",
" translated = [state['messages'][0]] + [\n",
" cls_map[msg.type](content=msg.content) for msg in state['messages'][1:]\n",
" translated = [state[\"messages\"][0]] + [\n",
" cls_map[msg.type](content=msg.content) for msg in state[\"messages\"][1:]\n",
" ]\n",
" res = await reflect.ainvoke(translated)\n",
" # We treat the output of this as human feedback for the generator\n",
@@ -359,7 +359,6 @@
" return \"reflect\"\n",
"\n",
"\n",
"\n",
"builder.add_conditional_edges(\"generate\", should_continue)\n",
"builder.add_edge(\"reflect\", \"generate\")\n",
"memory = MemorySaver()\n",
@@ -406,13 +405,16 @@
}
],
"source": [
"async for event in graph.astream({\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n",
" )\n",
" ],\n",
"}, config):\n",
"async for event in graph.astream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n",
" )\n",
" ],\n",
" },\n",
" config,\n",
"):\n",
" print(event)\n",
" print(\"---\")"
]
+25 -16
View File
@@ -66,6 +66,7 @@
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"_set_if_undefined(\"ANTHROPIC_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")"
]
@@ -192,7 +193,7 @@
" response = []\n",
" for attempt in range(3):\n",
" response = self.runnable.invoke(\n",
" {\"messages\": state['messages']}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
" {\"messages\": state[\"messages\"]}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
" )\n",
" try:\n",
" self.validator.invoke(response)\n",
@@ -259,7 +260,9 @@
"outputs": [],
"source": [
"example_question = \"Why is reflection useful in AI?\"\n",
"initial = first_responder.respond({\"messages\":[HumanMessage(content=example_question)]})"
"initial = first_responder.respond(\n",
" {\"messages\": [HumanMessage(content=example_question)]}\n",
")"
]
},
{
@@ -332,20 +335,26 @@
"import json\n",
"\n",
"revised = revisor.respond(\n",
" {\"messages\": [\n",
" HumanMessage(content=example_question),\n",
" initial['messages'],\n",
" ToolMessage(\n",
" tool_call_id=initial['messages'].tool_calls[0][\"id\"],\n",
" content=json.dumps(\n",
" tavily_tool.invoke(\n",
" {\"query\": initial['messages'].tool_calls[0][\"args\"][\"search_queries\"][0]}\n",
" )\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(content=example_question),\n",
" initial[\"messages\"],\n",
" ToolMessage(\n",
" tool_call_id=initial[\"messages\"].tool_calls[0][\"id\"],\n",
" content=json.dumps(\n",
" tavily_tool.invoke(\n",
" {\n",
" \"query\": initial[\"messages\"].tool_calls[0][\"args\"][\n",
" \"search_queries\"\n",
" ][0]\n",
" }\n",
" )\n",
" ),\n",
" ),\n",
" ),\n",
" ]}\n",
" ]\n",
" }\n",
")\n",
"revised['messages']"
"revised[\"messages\"]"
]
},
{
@@ -439,7 +448,7 @@
"\n",
"def event_loop(state: list):\n",
" # in our case, we'll just stop after N plans\n",
" num_iterations = _get_num_iterations(state['messages'])\n",
" num_iterations = _get_num_iterations(state[\"messages\"])\n",
" if num_iterations > MAX_ITERATIONS:\n",
" return END\n",
" return \"execute_tools\"\n",
@@ -598,7 +607,7 @@
")\n",
"for i, step in enumerate(events):\n",
" print(f\"Step {i}\")\n",
" step['messages'][-1].pretty_print()"
" step[\"messages\"][-1].pretty_print()"
]
},
{
+3 -3
View File
@@ -317,7 +317,7 @@
" \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n",
" _step = _get_current_task(state)\n",
" _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n",
" _results = (state[\"results\"] or {}) if \"results\" in state else {}\n",
" _results = (state[\"results\"] or {}) if \"results\" in state else {}\n",
" for k, v in _results.items():\n",
" tool_input = tool_input.replace(k, v)\n",
" if tool == \"Google\":\n",
@@ -363,7 +363,7 @@
"def solve(state: ReWOO):\n",
" plan = \"\"\n",
" for _plan, step_name, tool, tool_input in state[\"steps\"]:\n",
" _results = (state[\"results\"] or {}) if \"results\" in state else {}\n",
" _results = (state[\"results\"] or {}) if \"results\" in state else {}\n",
" for k, v in _results.items():\n",
" tool_input = tool_input.replace(k, v)\n",
" step_name = step_name.replace(k, v)\n",
@@ -464,7 +464,7 @@
],
"source": [
"# Print out the final result\n",
"print(s['solve']['result'])"
"print(s[\"solve\"][\"result\"])"
]
},
{
+3 -1
View File
@@ -847,7 +847,9 @@
"builder.add_edge(\"ask_question\", \"answer_question\")\n",
"\n",
"builder.add_edge(START, \"ask_question\")\n",
"interview_graph = builder.compile(checkpointer=False).with_config(run_name=\"Conduct Interviews\")"
"interview_graph = builder.compile(checkpointer=False).with_config(\n",
" run_name=\"Conduct Interviews\"\n",
")"
]
},
{
+1 -3
View File
@@ -172,9 +172,7 @@
"\n",
"\n",
"summary_llm_chain = (\n",
" summary_prompt\n",
" | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
" | StrOutputParser()\n",
" summary_prompt | ChatAnthropic(model=\"claude-3-haiku-20240307\") | StrOutputParser()\n",
" # Customize the tracing name for easier organization\n",
").with_config(run_name=\"GenerateSummary\")\n",
"summary_chain = summary_llm_chain | parse_summary\n",
@@ -1,8 +1,18 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Iterable, Sequence, cast
from typing import (
Any,
AsyncIterator,
Callable,
Iterable,
Optional,
Sequence,
Union,
cast,
)
import orjson
from psycopg import AsyncConnection, AsyncCursor
from psycopg.errors import UndefinedTable
from psycopg.rows import dict_row
@@ -11,6 +21,7 @@ from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, Sea
from langgraph.store.postgres.base import (
BasePostgresStore,
Row,
_decode_ns_bytes,
_group_ops,
_row_to_item,
)
@@ -19,7 +30,16 @@ logger = logging.getLogger(__name__)
class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
def __init__(self, conn: AsyncConnection[Any]) -> None:
def __init__(
self,
conn: AsyncConnection[Any],
*,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
) -> None:
super().__init__(deserializer=deserializer)
self.conn = conn
self.conn = conn
self.loop = asyncio.get_running_loop()
@@ -87,7 +107,9 @@ class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
for idx, key in items:
row = key_to_row.get(key)
if row:
results[idx] = _row_to_item(namespace, row)
results[idx] = _row_to_item(
namespace, row, loader=self._deserializer
)
else:
results[idx] = None
@@ -106,16 +128,21 @@ class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
results: list[Result],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[AsyncCursor[Any], int, SearchOp]] = []
cursors: list[tuple[AsyncCursor[Any], int]] = []
for (query, params), (idx, op) in zip(queries, search_ops):
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
cursors.append((cur, idx, op))
cursors.append((cur, idx))
for cur, idx, op in cursors:
for cur, idx in cursors:
rows = cast(list[Row], await cur.fetchall())
items = [_row_to_item(op.namespace_prefix, row) for row in rows]
items = [
_row_to_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
]
results[idx] = items
async def _batch_list_namespaces_ops(
@@ -132,9 +159,7 @@ class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
for cur, idx in cursors:
rows = cast(list[dict], await cur.fetchall())
namespaces = [
tuple(row["truncated_prefix"].decode()[1:].split(".")) for row in rows
]
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
results[idx] = namespaces
@classmethod
@@ -4,7 +4,18 @@ import logging
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime
from typing import Any, Generic, Iterable, Iterator, Sequence, TypeVar, Union, cast
from typing import (
Any,
Callable,
Generic,
Iterable,
Iterator,
Optional,
Sequence,
TypeVar,
Union,
cast,
)
import orjson
from psycopg import BaseConnection, Connection, Cursor
@@ -54,6 +65,17 @@ C = TypeVar("C", bound=BaseConnection)
class BasePostgresStore(BaseStore, Generic[C]):
MIGRATIONS = MIGRATIONS
conn: C
__slots__ = ("_deserializer",)
def __init__(
self,
*,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
) -> None:
super().__init__()
self._deserializer = deserializer
def _get_batch_GET_ops_queries(
self,
@@ -130,7 +152,7 @@ class BasePostgresStore(BaseStore, Generic[C]):
queries: list[tuple[str, Sequence]] = []
for _, op in search_ops:
query = """
SELECT key, value, created_at, updated_at
SELECT prefix, key, value, created_at, updated_at, prefix
FROM store
WHERE prefix <@ %s
"""
@@ -191,7 +213,15 @@ class BasePostgresStore(BaseStore, Generic[C]):
class PostgresStore(BasePostgresStore[Connection]):
def __init__(self, conn: Connection[Any]) -> None:
def __init__(
self,
conn: Connection[Any],
*,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
) -> None:
super().__init__(deserializer=deserializer)
self.conn = conn
def batch(self, ops: Iterable[Op]) -> list[Result]:
@@ -246,7 +276,9 @@ class PostgresStore(BasePostgresStore[Connection]):
for idx, key in items:
row = key_to_row.get(key)
if row:
results[idx] = _row_to_item(namespace, row)
results[idx] = _row_to_item(
namespace, row, loader=self._deserializer
)
else:
results[idx] = None
@@ -265,16 +297,21 @@ class PostgresStore(BasePostgresStore[Connection]):
results: list[Result],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[Cursor[Any], int, SearchOp]] = []
cursors: list[tuple[Cursor[Any], int]] = []
for (query, params), (idx, op) in zip(queries, search_ops):
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor(binary=True)
cur.execute(query, params)
cursors.append((cur, idx, op))
cursors.append((cur, idx))
for cur, idx, op in cursors:
for cur, idx in cursors:
rows = cast(list[Row], cur.fetchall())
items = [_row_to_item(op.namespace_prefix, row) for row in rows]
items = [
_row_to_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
]
results[idx] = items
def _batch_list_namespaces_ops(
@@ -291,9 +328,7 @@ class PostgresStore(BasePostgresStore[Connection]):
for cur, idx in cursors:
rows = cast(list[dict], cur.fetchall())
namespaces = [
tuple(row["truncated_prefix"].decode()[1:].split(".")) for row in rows
]
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
results[idx] = namespaces
@classmethod
@@ -361,11 +396,17 @@ def _namespace_to_ltree(namespace: tuple[str, ...]) -> str:
return ".".join(namespace)
def _row_to_item(namespace: tuple[str, ...], row: Row) -> Item:
def _row_to_item(
namespace: tuple[str, ...],
row: Row,
*,
loader: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]] = None,
) -> Item:
"""Convert a row from the database into an Item."""
loader = loader or _json_loads
val = row["value"]
return Item(
value=val if isinstance(val, dict) else _json_loads(val),
value=val if isinstance(val, dict) else loader(val),
key=row["key"],
namespace=namespace,
created_at=row["created_at"],
@@ -392,3 +433,9 @@ def _json_loads(content: Union[bytes, orjson.Fragment]) -> Any:
else:
content = content.contents.encode()
return orjson.loads(cast(bytes, content))
def _decode_ns_bytes(namespace: Union[str, bytes]) -> tuple[str, ...]:
if isinstance(namespace, bytes):
namespace = namespace.decode()[1:]
return tuple(namespace.split("."))
+2 -2
View File
@@ -324,7 +324,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "1.0.11"
version = "2.0.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1105,4 +1105,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "4b174f142964238b985de1a2209680bc83e9a310e6797be14dc26f61fe1352e2"
content-hash = "8f763cd1727287f8c8b5ad2b4d8df00fb446e68d0cd4e88c278e4007969b83fd"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "1.0.10"
version = "2.0.0"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^1.0.11"
langgraph-checkpoint = "^2.0.0"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
@@ -45,12 +45,14 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -61,6 +63,7 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
]
)
@@ -151,12 +154,14 @@ async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -205,12 +210,14 @@ async def test_batch_search_ops(store: AsyncPostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -422,13 +429,26 @@ class TestAsyncPostgresStore:
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
]
empty = await store.asearch(
(
"scoped",
"assistant_id",
"shared",
"6c5356f6-63ab-4158-868d-cd9fd14c736e",
),
limit=10,
offset=0,
)
assert len(empty) == 0
for namespace, item in zip(test_namespaces, test_items):
await store.aput(namespace, f"item_{namespace[-1]}", item)
docs_result = await store.asearch(["test_search", "documents"])
assert len(docs_result) == 2
assert all(item.namespace[1] == "documents" for item in docs_result)
assert all([item.namespace[1] == "documents" for item in docs_result]), [
item.namespace for item in docs_result
]
reports_result = await store.asearch(["test_search", "reports"])
assert len(reports_result) == 2
@@ -460,6 +480,51 @@ class TestAsyncPostgresStore:
all_items = page1 + page2
assert len(all_items) == 4
assert len(set(item.key for item in all_items)) == 4
empty = await store.asearch(
(
"scoped",
"assistant_id",
"shared",
"again",
"maybe",
"some-long",
"6be5cb0e-2eb4-42e6-bb6b-fba3c269db25",
),
limit=10,
offset=0,
)
assert len(empty) == 0
# Test with a namespace beginning with a number (like a UUID)
uuid_namespace = (str(uuid.uuid4()), "documents")
uuid_item_id = "uuid_doc"
uuid_item_value = {
"title": "UUID Document",
"content": "This document has a UUID namespace.",
}
# Insert the item with the UUID namespace
await store.aput(uuid_namespace, uuid_item_id, uuid_item_value)
# Retrieve the item to verify it was stored correctly
retrieved_item = await store.aget(uuid_namespace, uuid_item_id)
assert retrieved_item is not None
assert retrieved_item.namespace == uuid_namespace
assert retrieved_item.key == uuid_item_id
assert retrieved_item.value == uuid_item_value
# Search for the item using the UUID namespace
search_result = await store.asearch([uuid_namespace[0]])
assert len(search_result) == 1
assert search_result[0].key == uuid_item_id
assert search_result[0].value == uuid_item_value
# Clean up: delete the item with the UUID namespace
await store.adelete(uuid_namespace, uuid_item_id)
# Verify the item was deleted
deleted_item = await store.aget(uuid_namespace, uuid_item_id)
assert deleted_item is None
for namespace in test_namespaces:
await store.adelete(namespace, f"item_{namespace[-1]}")
+10 -1
View File
@@ -43,12 +43,14 @@ def test_batch_order(store: PostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -59,6 +61,7 @@ def test_batch_order(store: PostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
]
)
@@ -149,12 +152,14 @@ def test_batch_get_ops(store: PostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -203,12 +208,14 @@ def test_batch_search_ops(store: PostgresStore) -> None:
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
@@ -423,7 +430,9 @@ class TestPostgresStore:
docs_result = store.search(["test_search", "documents"])
assert len(docs_result) == 2
assert all(item.namespace[1] == "documents" for item in docs_result)
assert all(
[item.namespace[1] == "documents" for item in docs_result]
), docs_result
reports_result = store.search(["test_search", "reports"])
assert len(reports_result) == 2
+2 -2
View File
@@ -332,7 +332,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "1.0.11"
version = "2.0.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1001,4 +1001,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0"
content-hash = "b38677f65f382dfb391bc663c876a6ced7772c9a894ab4502e36c97fd663575a"
content-hash = "e0091cc2deab4de99a6bc4eb262b0040b771a9659dd3638ac1c4a225a1f11dc2"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "1.0.4"
version = "2.0.0"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^1.0.11"
langgraph-checkpoint = "^2.0.0"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "1.0.14"
version = "2.0.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+6 -6
View File
@@ -1238,7 +1238,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "1.0.14"
version = "2.0.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1255,7 +1255,7 @@ url = "../checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
version = "1.0.9"
version = "2.0.0"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1263,7 +1263,7 @@ files = []
develop = true
[package.dependencies]
langgraph-checkpoint = "^1.0.11"
langgraph-checkpoint = "^2.0.0"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
@@ -1274,7 +1274,7 @@ url = "../checkpoint-postgres"
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "1.0.4"
version = "2.0.0"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0"
@@ -1283,7 +1283,7 @@ develop = true
[package.dependencies]
aiosqlite = "^0.20.0"
langgraph-checkpoint = "^1.0.11"
langgraph-checkpoint = "^2.0.0"
[package.source]
type = "directory"
@@ -3216,4 +3216,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.0,<4.0"
content-hash = "f31194d9fa8e2eb80b125c437f20baa4ef6d6c5a61e1ad2675a57e73801cdf25"
content-hash = "734cf2b68846e513403f580fb7eeb00f9c26cd46919d9b9529e43d97e7c43216"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.31"
version = "0.2.32"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
[tool.poetry.dependencies]
python = ">=3.9.0,<4.0"
langchain-core = ">=0.2.39,<0.4"
langgraph-checkpoint = "^1.0.14"
langgraph-checkpoint = "^2.0.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.2"
+174
View File
@@ -12,6 +12,9 @@ import {
AssistantVersion,
Subgraphs,
Checkpoint,
SearchItemsResponse,
ListNamespaceResponse,
Item,
} from "./schema.js";
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
import {
@@ -983,6 +986,171 @@ export class RunsClient extends BaseClient {
}
}
interface APIItem {
namespace: string[];
key: string;
value: Record<string, any>;
created_at: string;
updated_at: string;
}
interface APISearchItemsResponse {
items: APIItem[];
}
export class StoreClient extends BaseClient {
/**
* Store or update an item.
*
* @param namespace A list of strings representing the namespace path.
* @param key The unique identifier for the item within the namespace.
* @param value A dictionary containing the item's data.
* @returns Promise<void>
*/
async putItem(
namespace: string[],
key: string,
value: Record<string, any>,
): Promise<void> {
namespace.forEach((label) => {
if (label.includes(".")) {
throw new Error(
`Invalid namespace label '${label}'. Namespace labels cannot contain periods ('.')`,
);
}
});
const payload = {
namespace,
key,
value,
};
return this.fetch<void>("/store/items", {
method: "PUT",
json: payload,
});
}
/**
* Retrieve a single item.
*
* @param namespace A list of strings representing the namespace path.
* @param key The unique identifier for the item.
* @returns Promise<Item>
*/
async getItem(namespace: string[], key: string): Promise<Item | null> {
namespace.forEach((label) => {
if (label.includes(".")) {
throw new Error(
`Invalid namespace label '${label}'. Namespace labels cannot contain periods ('.')`,
);
}
});
const response = await this.fetch<APIItem>("/store/items", {
params: { namespace: namespace.join("."), key },
});
return {
...response,
createdAt: response.created_at,
updatedAt: response.updated_at,
};
}
/**
* Delete an item.
*
* @param namespace A list of strings representing the namespace path.
* @param key The unique identifier for the item.
* @returns Promise<void>
*/
async deleteItem(namespace: string[], key: string): Promise<void> {
namespace.forEach((label) => {
if (label.includes(".")) {
throw new Error(
`Invalid namespace label '${label}'. Namespace labels cannot contain periods ('.')`,
);
}
});
return this.fetch<void>("/store/items", {
method: "DELETE",
json: { namespace, key },
});
}
/**
* Search for items within a namespace prefix.
*
* @param namespacePrefix List of strings representing the namespace prefix.
* @param options.filter Optional dictionary of key-value pairs to filter results.
* @param options.limit Maximum number of items to return (default is 10).
* @param options.offset Number of items to skip before returning results (default is 0).
* @returns Promise<SearchItemsResponse>
*/
async searchItems(
namespacePrefix: string[],
options?: {
filter?: Record<string, any>;
limit?: number;
offset?: number;
},
): Promise<SearchItemsResponse> {
const payload = {
namespace_prefix: namespacePrefix,
filter: options?.filter,
limit: options?.limit ?? 10,
offset: options?.offset ?? 0,
};
const response = await this.fetch<APISearchItemsResponse>(
"/store/items/search",
{
method: "POST",
json: payload,
},
);
return {
items: response.items.map((item) => ({
...item,
createdAt: item.created_at,
updatedAt: item.updated_at,
})),
};
}
/**
* List namespaces with optional match conditions.
*
* @param options.prefix Optional list of strings representing the prefix to filter namespaces.
* @param options.suffix Optional list of strings representing the suffix to filter namespaces.
* @param options.maxDepth Optional integer specifying the maximum depth of namespaces to return.
* @param options.limit Maximum number of namespaces to return (default is 100).
* @param options.offset Number of namespaces to skip before returning results (default is 0).
* @returns Promise<ListNamespaceResponse>
*/
async listNamespaces(options?: {
prefix?: string[];
suffix?: string[];
maxDepth?: number;
limit?: number;
offset?: number;
}): Promise<ListNamespaceResponse> {
const payload = {
prefix: options?.prefix,
suffix: options?.suffix,
max_depth: options?.maxDepth,
limit: options?.limit ?? 100,
offset: options?.offset ?? 0,
};
return this.fetch<ListNamespaceResponse>("/store/namespaces", {
method: "POST",
json: payload,
});
}
}
export class Client {
/**
* The client for interacting with assistants.
@@ -1004,10 +1172,16 @@ export class Client {
*/
public crons: CronsClient;
/**
* The client for interacting with the KV store.
*/
public store: StoreClient;
constructor(config?: ClientConfig) {
this.assistants = new AssistantsClient(config);
this.threads = new ThreadsClient(config);
this.runs = new RunsClient(config);
this.crons = new CronsClient(config);
this.store = new StoreClient(config);
}
}
+16
View File
@@ -219,3 +219,19 @@ export interface Checkpoint {
checkpoint_id: Optional<string>;
checkpoint_map: Optional<Record<string, unknown>>;
}
export interface ListNamespaceResponse {
namespaces: string[][];
}
export interface SearchItemsResponse {
items: Item[];
}
export interface Item {
namespace: string[];
key: string;
value: Record<string, any>;
createdAt: string;
updatedAt: string;
}
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.31"
version = "0.1.32"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"