mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a45f6c99a | ||
|
|
00662ab557 | ||
|
|
11d636ff83 | ||
|
|
78c9c15b14 | ||
|
|
46b52b2426 | ||
|
|
674180ad0c | ||
|
|
71efff7cee | ||
|
|
dd88ac6224 | ||
|
|
53c2e4d8c2 | ||
|
|
70b0032191 | ||
|
|
445b795037 | ||
|
|
29f58fd9e5 | ||
|
|
4fca814d3a | ||
|
|
d309c36388 | ||
|
|
f064a5969d |
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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]})"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+582
-581
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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!\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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\"))"
|
||||
]
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
"\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class AgentState(BaseModel):\n",
|
||||
" messages: Annotated[Sequence[BaseMessage], operator.add]"
|
||||
]
|
||||
|
||||
@@ -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)"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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
@@ -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",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(\"---\")"
|
||||
]
|
||||
|
||||
@@ -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()"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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("."))
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -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]}")
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
Generated
+6
-6
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
Reference in New Issue
Block a user