mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-30 02:41:30 +01:00
Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701907adf4 | ||
|
|
067c4dd246 | ||
|
|
ccc21974e0 | ||
|
|
a6e66746f7 | ||
|
|
3a17df6106 | ||
|
|
cee6a450dc | ||
|
|
0b3bf37a55 | ||
|
|
305a676675 | ||
|
|
5b73e38c38 | ||
|
|
41fb5ec77c | ||
|
|
cff349e22e | ||
|
|
8f32fc4819 | ||
|
|
5690555394 | ||
|
|
c5b118a672 | ||
|
|
72bec9161a | ||
|
|
ae17e77522 | ||
|
|
a96fc75c55 | ||
|
|
4c89bb39d4 | ||
|
|
05a4fcc8bb | ||
|
|
adac016e33 | ||
|
|
2d13904abf | ||
|
|
dfeb9d3b46 | ||
|
|
81935a73d8 | ||
|
|
615fc8b4ae | ||
|
|
13e6f6cbde | ||
|
|
e89633f30b | ||
|
|
0bbf5829e8 | ||
|
|
7ed5288f8f | ||
|
|
cba240e70e | ||
|
|
e9b5046076 | ||
|
|
af6552a17e | ||
|
|
e38c30a434 | ||
|
|
e41dea4cf9 | ||
|
|
f9f8c19ec4 | ||
|
|
64ab3217f6 | ||
|
|
9af243d138 | ||
|
|
3f1d440aee | ||
|
|
78901599e6 | ||
|
|
958c0df2d7 | ||
|
|
6919de8b3e | ||
|
|
e9a66cef46 | ||
|
|
728679e48e | ||
|
|
f90c81f280 | ||
|
|
f118a61101 | ||
|
|
8963bb2b68 | ||
|
|
499a1812e8 | ||
|
|
aef463c633 |
No files matched your search
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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 truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
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 truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNrtWQlUE+faBkHFKyoXQRAVxwhYlITsCSAqZZEtbGERROlkMiEjSSbOTNgEBPeiRSNal7qhLIoKFFGpCC7U/bpQFYSioKDibquCVi33C6Bi7X/+/v/1nnO9xzkHkpnv/d73edd5PphXEI8SJIar9HdhKgolYIQCN2T2vAICna1BSWpBvhKl5Lg0NyhQHLpNQ2D14+UUpSadHR1hNcbA1agKxhgIrnSMZzkicphyBN/VCrRLTa4ElyY16AfOoSlRkoRjUZLmPH0ODcGBKRVFc6ZFgA3jSIiSo1ACCoMPAsJUUECk+2SaA43AFSiQ0ZAoQUud4UBT4lJUAR7Eqik6FwcCJEWgsJLmLIMVJOpAo1ClGnhAaQiwjclggic4rug2SSWpdbpkGlWXg2Dz26/Oc2gqWKlbjUWpmB4YQECKkgiBqbtlaGEkCmBiACsOAblecGU4oYR1UgywSQ0TQBcIJKnTqyZAfAgKQ7vuFDgCv7HYgwd4gKliaampwFsQb4xApQDuO0ngdY8kLpmFIhSQTJ2RWiBHYSkwsTxXjpOUtuj9RBTDCIKCEKEqBJcC7do9scmY2gGSojIFTKEOUDJJSQtBDlRoVwC0hXEoqqbDCiweze/eqy2B1WoF1g3CcRaJq3b15Iyug/PhcqEuSXSQYBWlLQsEUNx8HIOSQN2oIBaDz2KwShLpJAVjKgWoA7oCBqjy1V3rFb0X1DASB5TQe2pSm9+9uai3DE5q80QwEih+TyVMIHJtHkwo+dw9vZ8TGhWFKVFtgXvQh+Z6Ft+Z4zBYLIbT9+8pJpNUiDavq8T2v7cZpYgkOoIDHdocZtGb+ChQVSwl127jcHjbCZRUgy5A5+eDbZSGnJcLMoL+42RBTzdsDfR7k8preha5HiA72spQucYBYnIgNzUBsZlsHsTiOvP4zjwuNFUUusu9x0zon6bh+1ACVpEykArPN8kvQOQaVRwqLXT/04RX6hIOvNHBB/1GRxPVOInSe1Bpd02jh3TPAbqPx57uGqPjRCyswpK7zGp36JIJ+h5TlfUsg6LXqQTG6UpSu43HYhX1rLyJcyHwi0lnMelM1oFEOuhYVIEpMRC7rt89gwekmccEV/mHEhQeh6pI7Q5WlwC4qnrLEKgSoNGZf6eJ6wSug38u9VZbl5CTE/PA+3Ik2gvTNpaSLP9wvUfHVia5K/GNMB2TauttwE0Ml81zkvD5AoTJhVkyCcpDhDKBkC0TSgUIj8Xm/aAbAwjQosueGicoOokiYNRSSdp6ByWcqGssVw6Lx+EDX13AyEEUGikq1kg8cJ0TpAukJlAFDkuLERkdgRE5Su8uOG2BR2SAm8jHvVAMQLrjeByGrmzQt4yJQWQxEqUrGpcU6e//pTxYFqEi5WLK29MjQS2WsFk8RmwimxcuEgREipLjkmczcDpLwOXwndhsFpfOYjAZoC3pngnuXqH+Uo8IWOhFfKlhMCmePNCbNRsJVPuHqQlsNj/IN8CbrwtBKBooFnnMnspWhsZpYkVciZgREiRUJom9SVUcK9kDjpF7JmOokCPwC0rmOwXKZ0eFJ/ghGlZIEsr3VHKBi2Deujq6QKBCwZgkXXv6hA76hK7rEp4z+02XuEDSrsC4Mt6fjC6QN3h1BaoUSS6QWBdhFHyCcS3GKNQ1AFeh9atAYDTxmNQ1ytctSBwkkmLhbIE4cprMM1hFhXmQykTUO8JH4y+KERBcf4odJhJwe0WGA/Awe4LDZ3KFXcX5Dvr/E9W+afTebU8P7HongeSqcFKFyWT5YpQAraUtRBS4RgqGPIHmu3vRQ9witWVObNiJD3NRvlQq5KECKd0zIqTkjba3QyJX94YogBWg8OIR7R45x5XmzOVyaC6QEnYV8rlMZtebPCO/+311rM/A0UuN9LouA/DT2blMHIw3ME3SzheXPBxuZ2Tse20fttU/Kk9z7xIWqr0XbVqcpaxfM2zB80diY5uM+J9+NYnseFHx8mHFE4bEWNpvgEEwYbo6rHr5zpu81U8aUxg7by6/59v4i4u1oyB86WXLfoOqJ/7u5jau1gUev3NFbk3/acLWBaLaX1Kc19FvJE4cMAVlhw7aXcLdsB6emFlQXXNZzXUx9RvbN9n7yqMlrOPx15u8Lw1XQfC2I/YhOR0Hp3g3+C3L4BR+rdcxM/nG+q8uZByVf3l1e+3eENf0ZcE7mn01C4c0FSU8TLnhm9ck2znkzNmUA69/S3mUdvrRzGuNTdfS1j29OyoEV5qsqosq+/3pY/6maDPEmFvcZ/8U35gV4q8fzjtnqY64ZD5rVm61atBpo2s20e6li0akwIeWzZFstlHWlSzmtE9KvvfqAuc4PHNO0UyX43v5ZncNz1hnnazamMVeqLlVExM0Zk3Ub2zNyVT5gUeplKmpzZRU/sySc6HfhrADLTOrJhf1oc7UZbS8+LuNe7Tml3r7tG/pZ1eUG1/ELQLUwZJaVsWxFo2daMiS5I2Hhj0Jf+jeEWLRmV1lsGCK39q8WtjuWFL1Vd7isxdf1e2tbYEsJx6ItWfafl20zmuRU/Hl65fTm5KPjj+bx7gQOa76YkZjfsTSCYwbx1IOpstHv+6ry7iB3t1iz6/cDfX0PiZV7LPrI1BFh17bVBqF4u0yDN5KYDCC593UMAaBFf8jP8QAE6PpBGKC+T5uUxMF3CRR/DQiHBOTWMC0kBBfzl8hkTARq1ECJMAKbU70W2IXTXOGomkBaAIUiRNxkDsY9dG0VJqO3PVGT/OhICUWK6cgCQp1dXeSzmlVEuIAJWCUHIIhEEcVgkK4DCLAbIFglRTqxYNJSKPWMVYhE9C/WAJFSca7aOlC0DsSMX/B5c88/E94+DU9k89M/D+FiecjXTRH26Df/1OkOf8GAvLB2YQv5P7fzibD/pezCe8TPZvwucJP6mzCF370s4kTKkSFsFDI4yJMDlPIlHAkCIcr4Uo5Qp5EwpT9+88m/zq95XGcpE68j0dv9e/9kd6GiPCfmcaVLyNMnMctbEFFG5D+intoTFvO1SDLZR7To1CfFW12btrOSVd8Wqh76TOp5pZt1+a0ivtBy0wnmISMPcO/cufys4LzVwOTspv2Ld/fpHihuRqNz1U8SGh1sbdzrS6em90aW7vwqbLkx+MVBg5jx4gna/atPVThsFTCH51eO6HUdfeAiRtfYlqLJ1jVhltrpm5t9woxnfB8tb7exEG4Y419Q6Z5a/ZtXz972F2imdXHpObnKhrTNtty1NJTRr6P1jZu+c1rqMIoUmzY0jxQ2XzuRP/nXyX2341xq9dPiWracdWQv4gHeX8TYPrcimbVv82MuTeDRljYu9msaijvd0DvPrxu/lY7U9vnS5xXXpQ+vxEdlHvA78rt685x5/Woc5z2pMHBjaO1r08dfCZakrbx962ZDcJsynxAdkJlzY+TluhfjdXrcD95dWmjx5KqQdHfz91yveLX1/AGy/3O847dMCqNLUNSw82yI46HZVp+I4qdbmqaZ8cLObFaUrBsskEp52JnYZKy/Uj57LPTjy9PX2W2NtScZj82xtn6Z9qzqZ2H9olPzv9K+8UkKx/zEZmvaWaXr9RXu2aW+fkrw0bUBRnXjMwqHXy5NHqrw+lju8zqJ7006Kav+rs55hsNPi59NUj6TF//s+nre/BC/5CM3v78EfhfRgyoMCIHU+QD6JAXLCdQlRzFqF5evEvq+9h66gWjoDgVnqCCALv9/Dfxz1z8Mxf/L+PiQqHTR+TiAmcm+9Pk4rkCNkvwSZFxDuvj/6OAz2cJZHyelAszeUIU4TJZMiGLxeNJmXy+lCX4JMi4UIIIPyIZNxz8RzIeJsIbvbrJ+N7jyXDxqBPtZ32+G1IWYCVUWl06Ms8q2HTPtpx/XL8fP6L1kMXYB4s33rpwpo/Lhg7vqjGbq0bqmUWbRwSVDzurqZyc3P7bzZ03I5/drihLqUu5P6zD8fzNB9/NfZj67MTdrWulC1ZtPlU/jKf56dmmZbbL4IIFXuuetd6pv/PipiL+V6kRFaI9X5K/fxvHaf2o8+eyshyzss4LDodfCXYoai420Et3rMYZrNCnCbP7nsh+4DOVJXE/dvmh6ZScG098MlgE1zjcv91t3JEvjMMO5ZinmM04TO7jqRd4rghLjzIz+Xa7/75+OzI2GsY3s0JH3aUFHLbFfWoMFssV9oJR49Jz5tmuyTwCmQiUgPmHzv7b/p+Cb5V+uyVbbvJjyVNDXo5tntHIDYHmM64urFCzF1ZXT3f3M2iLYPgoUxMT+9aZ1M2wnzS6zjfUR5C7e/vQgrbaoY9vlD6E4IEL6lgWkysH7vF/tcklUz1nc5A9+Xtp3Hf0L3c0lvWb1Hl8wETxkVkL70U7Wd3JP8v2fDLSaso9m3a6vejRUSx/bsCRUv6hHMbxla6z3C8U/LRnvrVGP6Qu12Bkn6yQ8ZHWls3MzB/DynZLspSulQNj1/1+uyjW9uRdk4l+9ycMPrtinCJmPKO2xr4996WHa7/g+1eOrMTMD8FpK9ft3y0rtbjik+kSHL61ypmwRhr/pl/pu7rx8dOLFepNwydt6VDH+UrXtab6BZGHmy1dy3Y0yl67qtNychYbp60/IrAPuSvpV2UtG7NmI960YsfTG/XMw4kThs7YKbJ+VanN5LaKn1/3UhXNHTl6/NDohNz71hlNitC5x2o89hha/LBoc6KP1YBN47JGylOqsg4OMj39GGp5LDbMa8g+843aMrSQ3xaVO6/ti9Z15SNlkgom0ufa4D2lcUdKCuQH0jw2xY2LM3Dh5x8kTf1/8He3t3M/+qA0p2HJFfot/stTswM6+yBnTNurs/5uYuVom7z9BCS8X38hcMtqoU2Obd+lzUluJU8yB5mnQFH+T4qqcHvTLaYpATV6OfnmZY1lLf1u5ofTrajyLUbLXfuP6VsVyC167Gz8pH4OxU1ti/TVZK6lc2wejTZkSkcM92JW3CwQtd47N7qSW+k7/FDGMF7/rAfRA7YtuL+wJtdxUVudQXrg/CELine truncated
|
||||
@@ -12,10 +12,6 @@ Generative user interfaces (Generative UI) allows agents to go beyond text and g
|
||||
|
||||
LangGraph Platform supports colocating your React components with your graph code. This allows you to focus on building specific UI components for your graph while easily plugging into existing chat interfaces such as [Agent Chat](https://agentchat.vercel.app) and loading the code only when actually needed.
|
||||
|
||||
!!! warning "LangGraph.js only"
|
||||
|
||||
Currently only LangGraph.js supports Generative UI. Support for Python is coming soon.
|
||||
|
||||
## Tutorial
|
||||
|
||||
### 1. Define and configure UI components
|
||||
@@ -74,58 +70,105 @@ CSS and Tailwind 4.x is also supported out of the box, so you can freely use Tai
|
||||
|
||||
### 2. Send the UI components in your graph
|
||||
|
||||
Use the `typedUi` utility to emit UI elements from your agent nodes:
|
||||
=== "Python"
|
||||
|
||||
```typescript title="src/agent/index.ts"
|
||||
import {
|
||||
typedUi,
|
||||
uiMessageReducer,
|
||||
} from "@langchain/langgraph-sdk/react-ui/server";
|
||||
```python title="src/agent.py"
|
||||
import uuid
|
||||
from typing import Annotated, Sequence, TypedDict
|
||||
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
from langchain_core.messages import AIMessage, BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.graph.ui import AnyUIMessage, ui_message_reducer, push_ui_message
|
||||
|
||||
import type ComponentMap from "./ui.js";
|
||||
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
StateGraph,
|
||||
type LangGraphRunnableConfig,
|
||||
} from "@langchain/langgraph";
|
||||
class AgentState(TypedDict): # noqa: D101
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
ui: Annotation({ reducer: uiMessageReducer, default: () => [] }),
|
||||
});
|
||||
|
||||
export const graph = new StateGraph(AgentState)
|
||||
.addNode("weather", async (state, config) => {
|
||||
// Provide the type of the component map to ensure
|
||||
// type safety of `ui.push()` calls as well as
|
||||
// pushing the messages to the `ui` and sending a custom event as well.
|
||||
const ui = typedUi<typeof ComponentMap>(config);
|
||||
async def weather(state: AgentState):
|
||||
class WeatherOutput(TypedDict):
|
||||
city: str
|
||||
|
||||
const weather = await new ChatOpenAI({ model: "gpt-4o-mini" })
|
||||
.withStructuredOutput(z.object({ city: z.string() }))
|
||||
.withConfig({ tags: ["langsmith:nostream"] })
|
||||
.invoke(state.messages);
|
||||
weather: WeatherOutput = (
|
||||
await ChatOpenAI(model="gpt-4o-mini")
|
||||
.with_structured_output(WeatherOutput)
|
||||
.with_config({"tags": ["nostream"]})
|
||||
.ainvoke(state["messages"])
|
||||
)
|
||||
|
||||
const response = {
|
||||
id: uuidv4(),
|
||||
type: "ai",
|
||||
content: `Here's the weather for ${weather.city}`,
|
||||
};
|
||||
message = AIMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
content=f"Here's the weather for {weather['city']}",
|
||||
)
|
||||
|
||||
// Emit UI elements with associated AI message
|
||||
ui.push({ name: "weather", props: weather }, { message: response });
|
||||
# Emit UI elements associated with the message
|
||||
push_ui_message("weather", weather, message=message)
|
||||
return {"messages": [message]}
|
||||
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge("__start__", "weather")
|
||||
.compile();
|
||||
```
|
||||
|
||||
workflow = StateGraph(AgentState)
|
||||
workflow.add_node(weather)
|
||||
workflow.add_edge("__start__", "weather")
|
||||
graph = workflow.compile()
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
Use the `typedUi` utility to emit UI elements from your agent nodes:
|
||||
|
||||
```typescript title="src/agent/index.ts"
|
||||
import {
|
||||
typedUi,
|
||||
uiMessageReducer,
|
||||
} from "@langchain/langgraph-sdk/react-ui/server";
|
||||
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import type ComponentMap from "./ui.js";
|
||||
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
StateGraph,
|
||||
type LangGraphRunnableConfig,
|
||||
} from "@langchain/langgraph";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
ui: Annotation({ reducer: uiMessageReducer, default: () => [] }),
|
||||
});
|
||||
|
||||
export const graph = new StateGraph(AgentState)
|
||||
.addNode("weather", async (state, config) => {
|
||||
// Provide the type of the component map to ensure
|
||||
// type safety of `ui.push()` calls as well as
|
||||
// pushing the messages to the `ui` and sending a custom event as well.
|
||||
const ui = typedUi<typeof ComponentMap>(config);
|
||||
|
||||
const weather = await new ChatOpenAI({ model: "gpt-4o-mini" })
|
||||
.withStructuredOutput(z.object({ city: z.string() }))
|
||||
.withConfig({ tags: ["nostream"] })
|
||||
.invoke(state.messages);
|
||||
|
||||
const response = {
|
||||
id: uuidv4(),
|
||||
type: "ai",
|
||||
content: `Here's the weather for ${weather.city}`,
|
||||
};
|
||||
|
||||
// Emit UI elements associated with the AI message
|
||||
ui.push({ name: "weather", props: weather }, { message: response });
|
||||
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge("__start__", "weather")
|
||||
.compile();
|
||||
```
|
||||
|
||||
### 3. Handle UI elements in your React application
|
||||
|
||||
@@ -294,18 +337,29 @@ const { thread, submit } = useStream({
|
||||
|
||||
### Remove UI messages from state
|
||||
|
||||
Similar to how messages can be removed from the state by appending a RemoveMessage you can remove an UI message from the state by calling `ui.delete` with the ID of the UI message.
|
||||
Similar to how messages can be removed from the state by appending a RemoveMessage you can remove an UI message from the state by calling `remove_ui_message` / `ui.delete` with the ID of the UI message.
|
||||
|
||||
```tsx
|
||||
// pushed message
|
||||
const message = ui.push({ name: "weather", props: { city: "London" } });
|
||||
=== "Python"
|
||||
|
||||
// remove said message
|
||||
ui.delete(message.id);
|
||||
```python
|
||||
from langgraph.graph.ui import push_ui_message, delete_ui_message
|
||||
|
||||
// return new state to persist changes
|
||||
return { ui: ui.items };
|
||||
```
|
||||
# push message
|
||||
message = push_ui_message("weather", {"city": "London"})
|
||||
|
||||
# remove said message
|
||||
delete_ui_message(message["id"])
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```tsx
|
||||
// push message
|
||||
const message = ui.push({ name: "weather", props: { city: "London" } });
|
||||
|
||||
// remove said message
|
||||
ui.delete(message.id);
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ LangGraph has a built-in persistence layer, implemented through checkpointers. W
|
||||
|
||||

|
||||
|
||||
!!! info "LangGraph API handles checkpointing automatically"
|
||||
|
||||
When using the LangGraph API, you don't need to implement or configure checkpointers manually. The API handles all persistence infrastructure for you behind the scenes.
|
||||
|
||||
## Threads
|
||||
|
||||
A thread is a unique ID or [thread identifier](#threads) assigned to each checkpoint saved by a checkpointer. When invoking graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config:
|
||||
@@ -26,7 +30,7 @@ Let's see what checkpoints are saved when a simple graph is invoked as follows:
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from typing import Annotated
|
||||
from typing_extensions import TypedDict
|
||||
from operator import add
|
||||
@@ -49,7 +53,7 @@ workflow.add_edge(START, "node_a")
|
||||
workflow.add_edge("node_a", "node_b")
|
||||
workflow.add_edge("node_b", END)
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
@@ -223,6 +227,10 @@ But, what if we want to retain some information *across threads*? Consider the c
|
||||
|
||||
With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](../reference/store.md#langgraph.store.base.BaseStore) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
|
||||
|
||||
!!! info "LangGraph API handles stores automatically"
|
||||
|
||||
When using the LangGraph API, you don't need to implement or configure stores manually. The API handles all storage infrastructure for you behind the scenes.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
First, let's showcase this in isolation without using LangGraph.
|
||||
@@ -324,10 +332,10 @@ store.put(
|
||||
With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `in_memory_store` allows us to store arbitrary information for access *across* threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# We need this because we want to enable threads (conversations)
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# ... Define the graph ...
|
||||
|
||||
@@ -440,6 +448,7 @@ Under the hood, checkpointing is powered by checkpointer objects that conform to
|
||||
* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately.
|
||||
* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.AsyncPostgresSaver]), used in LangGraph Cloud. Ideal for using in production. Needs to be installed separately.
|
||||
|
||||
|
||||
### Checkpointer interface
|
||||
|
||||
Each checkpointer conforms to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface and implements the following methods:
|
||||
@@ -452,7 +461,7 @@ Each checkpointer conforms to [BaseCheckpointSaver][langgraph.checkpoint.base.Ba
|
||||
If the checkpointer is used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), asynchronous versions of the above methods will be used (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`).
|
||||
|
||||
!!! note Note
|
||||
For running your graph asynchronously, you can use `MemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers.
|
||||
For running your graph asynchronously, you can use `InMemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers.
|
||||
|
||||
### Serializer
|
||||
|
||||
|
||||
@@ -0,0 +1,828 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to manage conversation history in a ReAct Agent\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
"\n",
|
||||
" - [Prebuilt create_react_agent](../create-react-agent)\n",
|
||||
" - [Persistence](../../concepts/persistence)\n",
|
||||
" - [Short-term Memory](../../concepts/memory/#short-term-memory)\n",
|
||||
" - [Trimming Messages](https://python.langchain.com/docs/how_to/trim_messages/)\n",
|
||||
"\n",
|
||||
"Message history can grow quickly and exceed LLM context window size, whether you're building chatbots with many conversation turns or agentic systems with numerous tool calls. There are several strategies for managing the message history:\n",
|
||||
"\n",
|
||||
"* [message trimming](#keep-the-original-message-history-unmodified) - remove first or last N messages in the history\n",
|
||||
"* [summarization](#summarizing-message-history) - summarize earlier messages in the history and replace them with a summary\n",
|
||||
"* custom strategies (e.g., message filtering, etc.)\n",
|
||||
"\n",
|
||||
"To manage message history in `create_react_agent`, you need to define a `pre_model_hook` function or [runnable](https://python.langchain.com/docs/concepts/runnables/) that takes graph state an returns a state update:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"* Trimming example:\n",
|
||||
" ```python\n",
|
||||
" # highlight-next-line\n",
|
||||
" from langchain_core.messages.utils import (\n",
|
||||
" # highlight-next-line\n",
|
||||
" trim_messages, \n",
|
||||
" # highlight-next-line\n",
|
||||
" count_tokens_approximately\n",
|
||||
" # highlight-next-line\n",
|
||||
" )\n",
|
||||
" from langgraph.prebuilt import create_react_agent\n",
|
||||
" \n",
|
||||
" # This function will be called every time before the node that calls LLM\n",
|
||||
" def pre_model_hook(state):\n",
|
||||
" trimmed_messages = trim_messages(\n",
|
||||
" state[\"messages\"],\n",
|
||||
" strategy=\"last\",\n",
|
||||
" token_counter=count_tokens_approximately,\n",
|
||||
" max_tokens=384,\n",
|
||||
" start_on=\"human\",\n",
|
||||
" end_on=(\"human\", \"tool\"),\n",
|
||||
" )\n",
|
||||
" # You can return updated messages either under `llm_input_messages` or \n",
|
||||
" # `messages` key (see the note below)\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"llm_input_messages\": trimmed_messages}\n",
|
||||
"\n",
|
||||
" checkpointer = InMemorySaver()\n",
|
||||
" agent = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools,\n",
|
||||
" # highlight-next-line\n",
|
||||
" pre_model_hook=pre_model_hook,\n",
|
||||
" checkpointer=checkpointer,\n",
|
||||
" )\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"* Summarization example:\n",
|
||||
" ```python\n",
|
||||
" # highlight-next-line\n",
|
||||
" from langmem.short_term import SummarizationNode\n",
|
||||
" from langchain_core.messages.utils import count_tokens_approximately\n",
|
||||
" from langgraph.prebuilt.chat_agent_executor import AgentState\n",
|
||||
" from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
" from typing import Any\n",
|
||||
" \n",
|
||||
" model = ChatOpenAI(model=\"gpt-4o\")\n",
|
||||
" \n",
|
||||
" summarization_node = SummarizationNode(\n",
|
||||
" token_counter=count_tokens_approximately,\n",
|
||||
" model=model,\n",
|
||||
" max_tokens=384,\n",
|
||||
" max_summary_tokens=128,\n",
|
||||
" output_messages_key=\"llm_input_messages\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" class State(AgentState):\n",
|
||||
" # NOTE: we're adding this key to keep track of previous summary information\n",
|
||||
" # to make sure we're not summarizing on every LLM call\n",
|
||||
" # highlight-next-line\n",
|
||||
" context: dict[str, Any]\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" checkpointer = InMemorySaver()\n",
|
||||
" graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools,\n",
|
||||
" # highlight-next-line\n",
|
||||
" pre_model_hook=summarization_node,\n",
|
||||
" # highlight-next-line\n",
|
||||
" state_schema=State,\n",
|
||||
" checkpointer=checkpointer,\n",
|
||||
" )\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"!!! Important\n",
|
||||
" \n",
|
||||
" * To **keep the original message history unmodified** in the graph state and pass the updated history **only as the input to the LLM**, return updated messages under `llm_input_messages` key\n",
|
||||
" * To **overwrite the original message history** in the graph state with the updated history, return updated messages under `messages` key\n",
|
||||
" \n",
|
||||
" To overwrite the `messages` key, you need to do the following:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langchain_core.messages import RemoveMessage\n",
|
||||
" from langgraph.graph.message import REMOVE_ALL_MESSAGES\n",
|
||||
"\n",
|
||||
" def pre_model_hook(state):\n",
|
||||
" updated_messages = ...\n",
|
||||
" return {\n",
|
||||
" \"messages\": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *updated_messages]\n",
|
||||
" ...\n",
|
||||
" }\n",
|
||||
" ```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai langmem"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "87a00ce9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Keep the original message history unmodified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cd6cbd3a-8632-47ae-9ec5-eec8d7b05cae",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's build a ReAct agent with a step that manages the conversation history: when the length of the history exceeds a specified number of tokens, we will call [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) utility that that will reduce the history while satisfying LLM provider constraints.\n",
|
||||
"\n",
|
||||
"There are two ways that the updated message history can be applied inside ReAct agent:\n",
|
||||
"\n",
|
||||
" * [**Keep the original message history unmodified**](#keep-the-original-message-history-unmodified) in the graph state and pass the updated history **only as the input to the LLM**\n",
|
||||
" * [**Overwrite the original message history**](#overwrite-the-original-message-history) in the graph state with the updated history\n",
|
||||
"\n",
|
||||
"Let's start by implementing the first one. We'll need to first define model and tools for our agent:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "eaad19ee-e174-4c6c-b2b8-3530d7acea40",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_weather(location: str) -> str:\n",
|
||||
" \"\"\"Use this to get weather information.\"\"\"\n",
|
||||
" if any([city in location.lower() for city in [\"nyc\", \"new york city\"]]):\n",
|
||||
" return \"It might be cloudy in nyc, with a chance of rain and temperatures up to 80 degrees.\"\n",
|
||||
" elif any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
|
||||
" return \"It's always sunny in sf\"\n",
|
||||
" else:\n",
|
||||
" return f\"I am not sure what the weather is in {location}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tools = [get_weather]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "52402333-61ab-47d3-8549-6a70f6f1cf36",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's implement `pre_model_hook` — a function that will be added as a new node and called every time **before** the node that calls the LLM (the `agent` node).\n",
|
||||
"\n",
|
||||
"Our implementation will wrap the `trim_messages` call and return the trimmed messages under `llm_input_messages`. This will **keep the original message history unmodified** in the graph state and pass the updated history **only as the input to the LLM**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "b507eb58-6e02-4ac6-b48b-ea4defdc11f0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"from langchain_core.messages.utils import (\n",
|
||||
" # highlight-next-line\n",
|
||||
" trim_messages,\n",
|
||||
" # highlight-next-line\n",
|
||||
" count_tokens_approximately,\n",
|
||||
" # highlight-next-line\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# This function will be added as a new node in ReAct agent graph\n",
|
||||
"# that will run every time before the node that calls the LLM.\n",
|
||||
"# The messages returned by this function will be the input to the LLM.\n",
|
||||
"def pre_model_hook(state):\n",
|
||||
" trimmed_messages = trim_messages(\n",
|
||||
" state[\"messages\"],\n",
|
||||
" strategy=\"last\",\n",
|
||||
" token_counter=count_tokens_approximately,\n",
|
||||
" max_tokens=384,\n",
|
||||
" start_on=\"human\",\n",
|
||||
" end_on=(\"human\", \"tool\"),\n",
|
||||
" )\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"llm_input_messages\": trimmed_messages}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools,\n",
|
||||
" # highlight-next-line\n",
|
||||
" pre_model_hook=pre_model_hook,\n",
|
||||
" checkpointer=checkpointer,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "8182ab45-86b3-4d6f-b75e-58862a14fa4e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Image object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import display, Image\n",
|
||||
"\n",
|
||||
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d41e8e76-5d43-44cd-bf01-a39212cedd8d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We'll also define a utility to render the agent outputs nicely:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def print_stream(stream, output_messages_key=\"llm_input_messages\"):\n",
|
||||
" for chunk in stream:\n",
|
||||
" for node, update in chunk.items():\n",
|
||||
" print(f\"Update from node: {node}\")\n",
|
||||
" messages_key = (\n",
|
||||
" output_messages_key if node == \"pre_model_hook\" else \"messages\"\n",
|
||||
" )\n",
|
||||
" for message in update[messages_key]:\n",
|
||||
" if isinstance(message, tuple):\n",
|
||||
" print(message)\n",
|
||||
" else:\n",
|
||||
" message.pretty_print()\n",
|
||||
"\n",
|
||||
" print(\"\\n\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "84448d29-b323-4833-80fc-4fff2f5a0950",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's run the agent with a few different queries to reach the specified max tokens limit:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
|
||||
"result = graph.invoke(inputs, config=config)\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
|
||||
"result = graph.invoke(inputs, config=config)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "fdb186da-b55d-4cb8-a237-e9e157ab0458",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's see how many tokens we have in the message history so far:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "41ba0253-5199-4d29-82ae-258cbbebddb4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"415"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"messages = result[\"messages\"]\n",
|
||||
"count_tokens_approximately(messages)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "812987ac-66ba-4122-8281-469cbdced7c7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can see that we are close to the `max_tokens` threshold, so on the next invocation we should see `pre_model_hook` kick-in and trim the message history. Let's run it again:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "26c53429-90ba-4d0b-abb9-423d9120ad26",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Update from node: pre_model_hook\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"What's it known for?\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"New York City is known for a variety of iconic landmarks, cultural institutions, and vibrant neighborhoods. Some of the most notable features include:\n",
|
||||
"\n",
|
||||
"1. **Statue of Liberty**: A symbol of freedom and democracy, located on Liberty Island.\n",
|
||||
"2. **Times Square**: Known for its bright lights, Broadway theaters, and bustling atmosphere.\n",
|
||||
"3. **Central Park**: A large public park offering a natural retreat in the middle of the city.\n",
|
||||
"4. **Empire State Building**: An iconic skyscraper offering panoramic views of the city.\n",
|
||||
"5. **Broadway**: Famous for its world-class theater productions.\n",
|
||||
"6. **Wall Street**: The financial hub of the United States.\n",
|
||||
"7. **Museums**: Including the Metropolitan Museum of Art, Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
|
||||
"8. **Diverse Cuisine**: A melting pot of cultures offering a wide range of culinary experiences.\n",
|
||||
"9. **Cultural Diversity**: A rich tapestry of cultures and communities from around the world.\n",
|
||||
"10. **Fashion**: A global fashion capital, hosting events like New York Fashion Week.\n",
|
||||
"\n",
|
||||
"These are just a few highlights of what makes New York City a unique and vibrant place.\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"where can i find the best bagel?\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node: agent\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"New York City is famous for its bagels, and there are several places renowned for serving some of the best. Here are a few top spots where you can find excellent bagels in NYC:\n",
|
||||
"\n",
|
||||
"1. **Ess-a-Bagel**: Known for their large, chewy bagels with a variety of spreads and toppings.\n",
|
||||
"2. **Russ & Daughters**: A classic spot offering traditional bagels with high-quality smoked fish and cream cheese.\n",
|
||||
"3. **H&H Bagels**: Famous for their fresh, hand-rolled bagels.\n",
|
||||
"4. **Murray’s Bagels**: Offers a wide selection of bagels and spreads, with a no-toasting policy to preserve freshness.\n",
|
||||
"5. **Absolute Bagels**: Known for their authentic, fluffy bagels and a variety of cream cheese options.\n",
|
||||
"6. **Tompkins Square Bagels**: Offers creative bagel sandwiches and a wide range of spreads.\n",
|
||||
"7. **Bagel Hole**: Known for their smaller, denser bagels with a crispy crust.\n",
|
||||
"\n",
|
||||
"Each of these places has its own unique style and flavor, so it might be worth trying a few to find your personal favorite!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
|
||||
"print_stream(graph.stream(inputs, config=config, stream_mode=\"updates\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "58fe0399-4e7d-4482-a4cb-5301311932d0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can see that the `pre_model_hook` node now only returned the last 3 messages, as expected. However, the existing message history is untouched:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "7ecfc310-8f9e-4aa0-9e58-17e71551639a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"updated_messages = graph.get_state(config).values[\"messages\"]\n",
|
||||
"assert [(m.type, m.content) for m in updated_messages[: len(messages)]] == [\n",
|
||||
" (m.type, m.content) for m in messages\n",
|
||||
"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "035864e3-0083-4dea-bf85-3a702fa5303f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Overwrite the original message history"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0b0a4fd5-a2ba-4eca-91a9-d294f4f2d884",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now change the `pre_model_hook` to **overwrite** the message history in the graph state. To do this, we’ll return the updated messages under `messages` key. We’ll also include a special `RemoveMessage(REMOVE_ALL_MESSAGES)` object, which tells `create_react_agent` to remove previous messages from the graph state:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "48c2a65b-685a-4750-baa6-2d61efe76b5f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import RemoveMessage\n",
|
||||
"from langgraph.graph.message import REMOVE_ALL_MESSAGES\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def pre_model_hook(state):\n",
|
||||
" trimmed_messages = trim_messages(\n",
|
||||
" state[\"messages\"],\n",
|
||||
" strategy=\"last\",\n",
|
||||
" token_counter=count_tokens_approximately,\n",
|
||||
" max_tokens=384,\n",
|
||||
" start_on=\"human\",\n",
|
||||
" end_on=(\"human\", \"tool\"),\n",
|
||||
" )\n",
|
||||
" # NOTE that we're now returning the messages under the `messages` key\n",
|
||||
" # We also remove the existing messages in the history to ensure we're overwriting the history\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"messages\": [RemoveMessage(REMOVE_ALL_MESSAGES)] + trimmed_messages}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools,\n",
|
||||
" # highlight-next-line\n",
|
||||
" pre_model_hook=pre_model_hook,\n",
|
||||
" checkpointer=checkpointer,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cd061682-231c-4487-9c2f-a6820dfbcab7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's run the agent with the same queries as before:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "831be36a-78a1-4885-9a03-8d085dfd7e37",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Update from node: pre_model_hook\n",
|
||||
"================================\u001b[1m Remove Message \u001b[0m================================\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"What's it known for?\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"New York City is known for a variety of iconic landmarks, cultural institutions, and vibrant neighborhoods. Some of the most notable features include:\n",
|
||||
"\n",
|
||||
"1. **Statue of Liberty**: A symbol of freedom and democracy, located on Liberty Island.\n",
|
||||
"2. **Times Square**: Known for its bright lights, Broadway theaters, and bustling atmosphere.\n",
|
||||
"3. **Central Park**: A large public park offering a natural oasis amidst the urban environment.\n",
|
||||
"4. **Empire State Building**: An iconic skyscraper offering panoramic views of the city.\n",
|
||||
"5. **Broadway**: Famous for its world-class theater productions and musicals.\n",
|
||||
"6. **Wall Street**: The financial hub of the United States, located in the Financial District.\n",
|
||||
"7. **Museums**: Including the Metropolitan Museum of Art, Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
|
||||
"8. **Diverse Cuisine**: A melting pot of cultures, offering a wide range of international foods.\n",
|
||||
"9. **Cultural Diversity**: Known for its diverse population and vibrant cultural scene.\n",
|
||||
"10. **Brooklyn Bridge**: An iconic suspension bridge connecting Manhattan and Brooklyn.\n",
|
||||
"\n",
|
||||
"These are just a few highlights, as NYC is a city with endless attractions and activities.\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"where can i find the best bagel?\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node: agent\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"New York City is famous for its bagels, and there are several places renowned for serving some of the best. Here are a few top spots where you can find delicious bagels in NYC:\n",
|
||||
"\n",
|
||||
"1. **Ess-a-Bagel**: Known for its large, chewy bagels and a wide variety of spreads and toppings. Locations in Midtown and the East Village.\n",
|
||||
"\n",
|
||||
"2. **Russ & Daughters**: A historic appetizing store on the Lower East Side, famous for its bagels with lox and cream cheese.\n",
|
||||
"\n",
|
||||
"3. **Absolute Bagels**: Located on the Upper West Side, this spot is popular for its fresh, fluffy bagels.\n",
|
||||
"\n",
|
||||
"4. **Murray’s Bagels**: Known for its traditional, hand-rolled bagels. Located in Greenwich Village.\n",
|
||||
"\n",
|
||||
"5. **Tompkins Square Bagels**: Offers a wide selection of bagels and creative cream cheese flavors. Located in the East Village.\n",
|
||||
"\n",
|
||||
"6. **Bagel Hole**: A small shop in Park Slope, Brooklyn, known for its classic, no-frills bagels.\n",
|
||||
"\n",
|
||||
"7. **Leo’s Bagels**: Located in the Financial District, known for its authentic New York-style bagels.\n",
|
||||
"\n",
|
||||
"Each of these places has its own unique style and flavor, so it might be worth trying a few to find your personal favorite!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
|
||||
"result = graph.invoke(inputs, config=config)\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
|
||||
"result = graph.invoke(inputs, config=config)\n",
|
||||
"messages = result[\"messages\"]\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
|
||||
"print_stream(\n",
|
||||
" graph.stream(inputs, config=config, stream_mode=\"updates\"),\n",
|
||||
" output_messages_key=\"messages\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cc9a0604-3d2b-48ff-9eaf-d16ea351fb30",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can see that the `pre_model_hook` node returned the last 3 messages again. However, this time, the message history is modified in the graph state as well:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"id": "394f72f8-f817-472d-a193-e01509a86132",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"updated_messages = graph.get_state(config).values[\"messages\"]\n",
|
||||
"assert (\n",
|
||||
" # First 2 messages in the new history are the same as last 2 messages in the old\n",
|
||||
" [(m.type, m.content) for m in updated_messages[:2]]\n",
|
||||
" == [(m.type, m.content) for m in messages[-2:]]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ee186d6d-4d07-404f-b236-f662db62339d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summarizing message history"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a6e53e0f-9a1e-4188-8435-c23ad8148b4f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Finally, let's apply a different strategy for managing message history — summarization. Just as with trimming, you can choose to keep original message history unmodified or overwrite it. The example below will only show the former.\n",
|
||||
"\n",
|
||||
"We will use the [`SummarizationNode`](https://langchain-ai.github.io/langmem/guides/summarization/#using-summarizationnode) from the prebuilt `langmem` library. Once the message history reaches the token limit, the summarization node will summarize earlier messages to make sure they fit into `max_tokens`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "b9540c1c-2eba-42da-ba4e-478521161a1f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# highlight-next-line\n",
|
||||
"from langmem.short_term import SummarizationNode\n",
|
||||
"from langgraph.prebuilt.chat_agent_executor import AgentState\n",
|
||||
"from typing import Any\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(model=\"gpt-4o\")\n",
|
||||
"summarization_model = model.bind(max_tokens=128)\n",
|
||||
"\n",
|
||||
"summarization_node = SummarizationNode(\n",
|
||||
" token_counter=count_tokens_approximately,\n",
|
||||
" model=summarization_model,\n",
|
||||
" max_tokens=384,\n",
|
||||
" max_summary_tokens=128,\n",
|
||||
" output_messages_key=\"llm_input_messages\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(AgentState):\n",
|
||||
" # NOTE: we're adding this key to keep track of previous summary information\n",
|
||||
" # to make sure we're not summarizing on every LLM call\n",
|
||||
" # highlight-next-line\n",
|
||||
" context: dict[str, Any]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools,\n",
|
||||
" # highlight-next-line\n",
|
||||
" pre_model_hook=summarization_node,\n",
|
||||
" # highlight-next-line\n",
|
||||
" state_schema=State,\n",
|
||||
" checkpointer=checkpointer,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"id": "8eccaaca-5d9c-4faf-b997-d4b8e84b59ac",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Update from node: pre_model_hook\n",
|
||||
"================================\u001b[1m System Message \u001b[0m================================\n",
|
||||
"\n",
|
||||
"Summary of the conversation so far: The user asked about the current weather in New York City. In response, the assistant provided information that it might be cloudy, with a chance of rain, and temperatures reaching up to 80 degrees.\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"What's it known for?\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"New York City, often referred to as NYC, is known for its:\n",
|
||||
"\n",
|
||||
"1. **Landmarks and Iconic Sites**:\n",
|
||||
" - **Statue of Liberty**: A symbol of freedom and democracy.\n",
|
||||
" - **Central Park**: A vast green oasis in the middle of the city.\n",
|
||||
" - **Empire State Building**: Once the tallest building in the world, offering stunning views of the city.\n",
|
||||
" - **Times Square**: Known for its bright lights and bustling atmosphere.\n",
|
||||
"\n",
|
||||
"2. **Cultural Institutions**:\n",
|
||||
" - **Broadway**: Renowned for theatrical performances and musicals.\n",
|
||||
" - **Metropolitan Museum of Art** and **Museum of Modern Art (MoMA)**: World-class art collections.\n",
|
||||
" - **American Museum of Natural History**: Known for its extensive exhibits ranging from dinosaurs to space exploration.\n",
|
||||
" \n",
|
||||
"3. **Diverse Neighborhoods and Cuisine**:\n",
|
||||
" - NYC is famous for having a melting pot of cultures, reflected in neighborhoods like Chinatown, Little Italy, and Harlem.\n",
|
||||
" - The city offers a wide range of international cuisines, from street food to high-end dining.\n",
|
||||
"\n",
|
||||
"4. **Financial District**:\n",
|
||||
" - Home to Wall Street, the New York Stock Exchange (NYSE), and other major financial institutions.\n",
|
||||
"\n",
|
||||
"5. **Media and Entertainment**:\n",
|
||||
" - Major hub for television, film, and media, with numerous studios and networks based there.\n",
|
||||
"\n",
|
||||
"6. **Fashion**:\n",
|
||||
" - Often referred to as one of the \"Big Four\" fashion capitals, hosting events like New York Fashion Week.\n",
|
||||
"\n",
|
||||
"7. **Sports**:\n",
|
||||
" - Known for its passionate sports culture with teams like the Yankees (MLB), Mets (MLB), Knicks (NBA), and Rangers (NHL).\n",
|
||||
"\n",
|
||||
"These elements, among others, contribute to NYC's reputation as a vibrant and dynamic city.\n",
|
||||
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"where can i find the best bagel?\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node: agent\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Finding the best bagel in New York City can be subjective, as there are many beloved spots across the city. However, here are some renowned bagel shops you might want to try:\n",
|
||||
"\n",
|
||||
"1. **Ess-a-Bagel**: Known for its chewy and flavorful bagels, located in Midtown and Stuyvesant Town.\n",
|
||||
"\n",
|
||||
"2. **Bagel Hole**: A favorite for traditionalists, offering classic and dense bagels, located in Park Slope, Brooklyn.\n",
|
||||
"\n",
|
||||
"3. **Russ & Daughters**: A legendary appetizing store on the Lower East Side, famous for their bagels with lox.\n",
|
||||
"\n",
|
||||
"4. **Murray’s Bagels**: Located in Greenwich Village, known for their fresh and authentic New York bagels.\n",
|
||||
"\n",
|
||||
"5. **Absolute Bagels**: Located on the Upper West Side, they’re known for their fresh, fluffy bagels with a variety of spreads.\n",
|
||||
"\n",
|
||||
"6. **Tompkins Square Bagels**: In the East Village, famous for their creative cream cheese options and fresh bagels.\n",
|
||||
"\n",
|
||||
"7. **Zabar’s**: A landmark on the Upper West Side known for their classic bagels and smoked fish.\n",
|
||||
"\n",
|
||||
"Each of these spots offers a unique take on the classic New York bagel experience, and trying several might be the best way to discover your personal favorite!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
|
||||
"\n",
|
||||
"result = graph.invoke(inputs, config=config)\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
|
||||
"result = graph.invoke(inputs, config=config)\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"where can i find the best bagel?\")]}\n",
|
||||
"print_stream(graph.stream(inputs, config=config, stream_mode=\"updates\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7caaf2f7-281a-4421-bf98-c745d950c56f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can see that the earlier messages have now been replaced with the summary of the earlier conversation!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -163,6 +163,7 @@ These guides show how to use the prebuilt ReAct agent:
|
||||
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
|
||||
- [How to return structured output from a ReAct agent](create-react-agent-structured-output.ipynb)
|
||||
- [How to add semantic search for long-term memory to a ReAct agent](memory/semantic-search.ipynb#using-in-create-react-agent)
|
||||
- [How to manage message history in a ReAct agent](create-react-agent-manage-message-history.ipynb)
|
||||
|
||||
Interested in further customizing the ReAct agent? This guide provides an
|
||||
overview of its underlying implementation to help you customize for your own needs:
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
" - [Memory](../../concepts/memory/)\n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
|
||||
"\n",
|
||||
"!!! info \"Not needed for LangGraph API users\"\n",
|
||||
"\n",
|
||||
" If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n",
|
||||
"\n",
|
||||
"Many AI applications need memory to share context across multiple interactions on the same [thread](../../concepts/persistence#threads) (e.g., multiple turns of a conversation). In LangGraph functional API, this kind of memory can be added to any [entrypoint()][langgraph.func.entrypoint] workflow using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence).\n",
|
||||
"\n",
|
||||
"When creating a LangGraph workflow, you can set it up to persist its results by using a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver):\n",
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
" </p>\n",
|
||||
"</div> \n",
|
||||
"\n",
|
||||
"!!! info \"Not needed for LangGraph API users\"\n",
|
||||
"\n",
|
||||
" If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n",
|
||||
"\n",
|
||||
"Many AI applications need memory to share context across multiple interactions. In LangGraph, this kind of memory can be added to any [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence) .\n",
|
||||
"\n",
|
||||
"When creating any LangGraph graph, you can set it up to persist its state by adding a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver) when compiling the graph:\n",
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
" </p>\n",
|
||||
"</div> \n",
|
||||
"\n",
|
||||
"!!! info \"Not needed for LangGraph API users\"\n",
|
||||
"\n",
|
||||
" If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n",
|
||||
"\n",
|
||||
"When creating LangGraph agents, you can also set them up so that they persist their state. This allows you to do things like interact with an agent multiple times and have it remember previous interactions.\n",
|
||||
"\n",
|
||||
"This how-to guide shows how to use `Postgres` as the backend for persisting checkpoint state using the [`langgraph-checkpoint-postgres`](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres) library.\n",
|
||||
@@ -44,7 +48,7 @@
|
||||
"...\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"!!! info \"Setup\"",
|
||||
"!!! info \"Setup\"\n",
|
||||
"\n",
|
||||
" You need to run `.setup()` once on your checkpointer to initialize the database before you can use it."
|
||||
]
|
||||
|
||||
@@ -185,6 +185,7 @@ nav:
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
- how-tos/create-react-agent-structured-output.ipynb
|
||||
- how-tos/create-react-agent-manage-message-history.ipynb
|
||||
- how-tos/react-agent-from-scratch.ipynb
|
||||
- how-tos/react-agent-from-scratch-functional.ipynb
|
||||
- LangGraph Platform:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
<meta name="algolia-site-verification" content="165B7E7C89E49946" />
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Public+Sans&display=swap");
|
||||
:root {
|
||||
|
||||
Generated
+240
-695
File diff suppressed because it is too large.
Load diff
+2
-1
@@ -50,7 +50,8 @@ langchain-community = "^0.3.0"
|
||||
langchain-experimental = "^0.3.2"
|
||||
langchain-mistralai = "^0.2.6"
|
||||
langgraph-checkpoint-mongodb = "^0.1.0"
|
||||
langsmith = "^0.2.0"
|
||||
langmem = "^0.0.19"
|
||||
langsmith = "^0.3.0"
|
||||
chromadb = "^0.5.5"
|
||||
gpt4all = "^2.8.2"
|
||||
scikit-learn = "^1.5.2"
|
||||
|
||||
Generated
+34
-14
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -51,7 +51,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
@@ -181,7 +181,7 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
|
||||
hard-encoding-detection = ["chardet"]
|
||||
toml = ["tomli"]
|
||||
toml = ["tomli ; python_version < \"3.11\""]
|
||||
types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
|
||||
|
||||
[[package]]
|
||||
@@ -267,7 +267,7 @@ idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
@@ -326,26 +326,26 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.0"
|
||||
version = "0.3.15"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "langchain_core-0.3.0-py3-none-any.whl", hash = "sha256:bee6dae2366d037ef0c5b87401fed14b5497cad26f97724e8c9ca7bc9239e847"},
|
||||
{file = "langchain_core-0.3.0.tar.gz", hash = "sha256:1249149ea3ba24c9c761011483c14091573a5eb1a773aa0db9c8ad155dd4a69d"},
|
||||
{file = "langchain_core-0.3.15-py3-none-any.whl", hash = "sha256:3d4ca6dbb8ed396a6ee061063832a2451b0ce8c345570f7b086ffa7288e4fa29"},
|
||||
{file = "langchain_core-0.3.15.tar.gz", hash = "sha256:b1a29787a4ffb7ec2103b4e97d435287201da7809b369740dd1e32f176325aba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonpatch = ">=1.33,<2.0"
|
||||
langsmith = ">=0.1.117,<0.2.0"
|
||||
langsmith = ">=0.1.125,<0.2.0"
|
||||
packaging = ">=23.2,<25"
|
||||
pydantic = [
|
||||
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
|
||||
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
|
||||
]
|
||||
PyYAML = ">=5.3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
|
||||
typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
@@ -368,24 +368,28 @@ url = "../checkpoint"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.1.120"
|
||||
version = "0.1.147"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "langsmith-0.1.120-py3-none-any.whl", hash = "sha256:54d2785e301646c0988e0a69ebe4d976488c87b41928b358cb153b6ddd8db62b"},
|
||||
{file = "langsmith-0.1.120.tar.gz", hash = "sha256:25499ca187b41bd89d784b272b97a8d76f60e0e21bdf20336e8a2aa6a9b23ac9"},
|
||||
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
|
||||
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
httpx = ">=0.23.0,<1"
|
||||
orjson = ">=3.9.14,<4.0.0"
|
||||
orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""}
|
||||
pydantic = [
|
||||
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
|
||||
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
|
||||
]
|
||||
requests = ">=2,<3"
|
||||
requests-toolbelt = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
@@ -454,6 +458,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
|
||||
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
|
||||
@@ -858,6 +863,21 @@ urllib3 = ">=1.21.1,<3"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.0.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.2"
|
||||
@@ -952,7 +972,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
@@ -38,6 +38,8 @@ class InMemorySaver(
|
||||
Only use `InMemorySaver` for debugging or testing purposes.
|
||||
For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
|
||||
|
||||
If you are using the LangGraph Platform, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
|
||||
|
||||
Args:
|
||||
serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to None.
|
||||
|
||||
|
||||
Generated
+124
-9
@@ -12,6 +12,29 @@ files = [
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
|
||||
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.7.4"
|
||||
@@ -177,7 +200,7 @@ version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
@@ -187,6 +210,65 @@ files = [
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
|
||||
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
@@ -240,19 +322,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.2.38"
|
||||
version = "0.2.43"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a"},
|
||||
{file = "langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3"},
|
||||
{file = "langchain_core-0.2.43-py3-none-any.whl", hash = "sha256:619601235113298ebf8252a349754b7c28d3cf7166c7c922da24944b78a9363a"},
|
||||
{file = "langchain_core-0.2.43.tar.gz", hash = "sha256:42c2ef6adedb911f4254068b6adc9eb4c4075f6c8cb3d83590d3539a815695f5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonpatch = ">=1.33,<2.0"
|
||||
langsmith = ">=0.1.75,<0.2.0"
|
||||
langsmith = ">=0.1.112,<0.2.0"
|
||||
packaging = ">=23.2,<25"
|
||||
pydantic = [
|
||||
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
|
||||
@@ -264,23 +346,28 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.1.93"
|
||||
version = "0.1.147"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "langsmith-0.1.93-py3-none-any.whl", hash = "sha256:811210b9d5f108f36431bd7b997eb9476a9ecf5a2abd7ddbb606c1cdcf0f43ce"},
|
||||
{file = "langsmith-0.1.93.tar.gz", hash = "sha256:285b6ad3a54f50fa8eb97b5f600acc57d0e37e139dd8cf2111a117d0435ba9b4"},
|
||||
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
|
||||
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
orjson = ">=3.9.14,<4.0.0"
|
||||
httpx = ">=0.23.0,<1"
|
||||
orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""}
|
||||
pydantic = [
|
||||
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
|
||||
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
|
||||
]
|
||||
requests = ">=2,<3"
|
||||
requests-toolbelt = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
@@ -369,6 +456,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
|
||||
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
|
||||
@@ -773,6 +861,21 @@ urllib3 = ">=1.21.1,<3"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.0.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.2"
|
||||
@@ -801,6 +904,18 @@ files = [
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "8.5.0"
|
||||
|
||||
@@ -665,6 +665,8 @@ def dev(
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
ui=config_json.get("ui"),
|
||||
ui_config=config_json.get("ui_config"),
|
||||
studio_url=studio_url,
|
||||
allow_blocking=allow_blocking,
|
||||
)
|
||||
|
||||
@@ -778,7 +778,22 @@ def _update_graph_paths(
|
||||
FileNotFoundError: If the local file (module) does not actually exist on disk.
|
||||
IsADirectoryError: If `module_str` points to a directory instead of a file.
|
||||
"""
|
||||
for graph_id, import_str in config["graphs"].items():
|
||||
for graph_id, data in config["graphs"].items():
|
||||
if isinstance(data, dict):
|
||||
# Then we're looking for a 'path' key
|
||||
if "path" not in data:
|
||||
raise ValueError(
|
||||
f"Graph '{graph_id}' must contain a 'path' key if "
|
||||
f" it is a dictionary."
|
||||
)
|
||||
import_str = data["path"]
|
||||
elif isinstance(data, str):
|
||||
import_str = data
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Graph '{graph_id}' must be a string or a dictionary with a 'path' key."
|
||||
)
|
||||
|
||||
module_str, _, attr_str = import_str.partition(":")
|
||||
if not module_str or not attr_str:
|
||||
message = (
|
||||
@@ -818,7 +833,10 @@ def _update_graph_paths(
|
||||
"Add its containing package to 'dependencies' list."
|
||||
)
|
||||
# update the config
|
||||
config["graphs"][graph_id] = f"{module_str}:{attr_str}"
|
||||
if isinstance(data, dict):
|
||||
config["graphs"][graph_id]["path"] = f"{module_str}:{attr_str}"
|
||||
else:
|
||||
config["graphs"][graph_id] = f"{module_str}:{attr_str}"
|
||||
|
||||
|
||||
def _update_auth_path(
|
||||
@@ -915,6 +933,66 @@ def _update_http_app_path(
|
||||
http_config["app"] = f"{module_str}:{attr_str}"
|
||||
|
||||
|
||||
def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
|
||||
def test_file(file_name):
|
||||
full_path = config_path.parent / file_name
|
||||
try:
|
||||
return full_path.is_file()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# inspired by `package-manager-detector`
|
||||
def get_pkg_manager_name():
|
||||
try:
|
||||
with open(config_path.parent / "package.json") as f:
|
||||
pkg = json.load(f)
|
||||
|
||||
if (pkg_manager_name := pkg.get("packageManager")) and isinstance(
|
||||
pkg_manager_name, str
|
||||
):
|
||||
return pkg_manager_name.lstrip("^").split("@")[0]
|
||||
|
||||
if (
|
||||
dev_engine_name := (
|
||||
(pkg.get("devEngines") or {}).get("packageManager") or {}
|
||||
).get("name")
|
||||
) and isinstance(dev_engine_name, str):
|
||||
return dev_engine_name
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
npm, yarn, pnpm, bun = [
|
||||
test_file("package-lock.json"),
|
||||
test_file("yarn.lock"),
|
||||
test_file("pnpm-lock.yaml"),
|
||||
test_file("bun.lockb"),
|
||||
]
|
||||
|
||||
if yarn:
|
||||
install_cmd = "yarn install --frozen-lockfile"
|
||||
elif pnpm:
|
||||
install_cmd = "pnpm i --frozen-lockfile"
|
||||
elif npm:
|
||||
install_cmd = "npm ci"
|
||||
elif bun:
|
||||
install_cmd = "bun i"
|
||||
else:
|
||||
pkg_manager_name = get_pkg_manager_name()
|
||||
|
||||
if pkg_manager_name == "yarn":
|
||||
install_cmd = "yarn install"
|
||||
elif pkg_manager_name == "pnpm":
|
||||
install_cmd = "pnpm i"
|
||||
elif pkg_manager_name == "bun":
|
||||
install_cmd = "bun i"
|
||||
else:
|
||||
install_cmd = "npm i"
|
||||
|
||||
return install_cmd
|
||||
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
@@ -995,10 +1073,32 @@ ADD {relpath} /deps/{name}
|
||||
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
|
||||
)
|
||||
|
||||
ui_inst_str: str = ""
|
||||
install_node_str: str = ""
|
||||
|
||||
if config.get("ui") and local_deps.working_dir:
|
||||
install_node_str = "RUN /storage/install-node.sh"
|
||||
|
||||
ui_inst: list[str] = []
|
||||
ui_inst.append(f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'")
|
||||
if config.get("ui_config"):
|
||||
ui_inst.append(
|
||||
f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(config['ui_config'])}'"
|
||||
)
|
||||
|
||||
ui_inst.append(
|
||||
f"RUN cd {local_deps.working_dir} && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
|
||||
)
|
||||
|
||||
ui_inst_str = f"""# -- Installing UI dependencies --
|
||||
{os.linesep.join(ui_inst)}
|
||||
# -- End of UI dependencies install --"""
|
||||
|
||||
installs = f"{os.linesep}{os.linesep}".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
install_node_str,
|
||||
pip_config_file_str,
|
||||
pip_pkgs_str,
|
||||
pip_reqs_str,
|
||||
@@ -1039,6 +1139,8 @@ ADD {relpath} /deps/{name}
|
||||
"# -- End of local dependencies install --",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
ui_inst_str,
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
|
||||
@@ -1059,62 +1161,7 @@ def node_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
def test_file(file_name):
|
||||
full_path = config_path.parent / file_name
|
||||
try:
|
||||
return full_path.is_file()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# inspired by `package-manager-detector`
|
||||
def get_pkg_manager_name():
|
||||
try:
|
||||
with open(config_path.parent / "package.json") as f:
|
||||
pkg = json.load(f)
|
||||
|
||||
if (pkg_manager_name := pkg.get("packageManager")) and isinstance(
|
||||
pkg_manager_name, str
|
||||
):
|
||||
return pkg_manager_name.lstrip("^").split("@")[0]
|
||||
|
||||
if (
|
||||
dev_engine_name := (
|
||||
(pkg.get("devEngines") or {}).get("packageManager") or {}
|
||||
).get("name")
|
||||
) and isinstance(dev_engine_name, str):
|
||||
return dev_engine_name
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
npm, yarn, pnpm, bun = [
|
||||
test_file("package-lock.json"),
|
||||
test_file("yarn.lock"),
|
||||
test_file("pnpm-lock.yaml"),
|
||||
test_file("bun.lockb"),
|
||||
]
|
||||
|
||||
if yarn:
|
||||
install_cmd = "yarn install --frozen-lockfile"
|
||||
elif pnpm:
|
||||
install_cmd = "pnpm i --frozen-lockfile"
|
||||
elif npm:
|
||||
install_cmd = "npm ci"
|
||||
elif bun:
|
||||
install_cmd = "bun i"
|
||||
else:
|
||||
pkg_manager_name = get_pkg_manager_name()
|
||||
|
||||
if pkg_manager_name == "yarn":
|
||||
install_cmd = "yarn install"
|
||||
elif pkg_manager_name == "pnpm":
|
||||
install_cmd = "pnpm i"
|
||||
elif pkg_manager_name == "bun":
|
||||
install_cmd = "bun i"
|
||||
else:
|
||||
install_cmd = "npm i"
|
||||
install_cmd = _get_node_pm_install_cmd(config_path, config)
|
||||
store_config = config.get("store")
|
||||
env_additional_config = (
|
||||
""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.83"
|
||||
version = "0.1.89"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -196,6 +196,50 @@ def test_dockerfile_command_basic() -> None:
|
||||
assert save_path.exists()
|
||||
|
||||
|
||||
def test_dockerfile_command_new_style_config() -> None:
|
||||
"""Test `dockerfile` command with a new style config.
|
||||
|
||||
This config format allows specifying agent data as a dictionary.
|
||||
{
|
||||
"graphs": {
|
||||
"agent1": {
|
||||
"path": ... # path to graph definition,
|
||||
... # other fields
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"dependencies": ["./my_agent"],
|
||||
"graphs": {
|
||||
"agent": {
|
||||
"path": "./my_agent/agent.py:graph",
|
||||
"description": "This is a test agent",
|
||||
}
|
||||
},
|
||||
"env": ".env",
|
||||
}
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
# Add agent.py file
|
||||
agent_path = temp_dir / "my_agent" / "agent.py"
|
||||
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
agent_path.touch()
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
|
||||
# Check if Dockerfile was created
|
||||
assert save_path.exists()
|
||||
|
||||
|
||||
def test_dockerfile_command_with_docker_compose() -> None:
|
||||
"""Test the 'dockerfile' command with Docker Compose configuration."""
|
||||
runner = CliRunner()
|
||||
|
||||
@@ -494,6 +494,49 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_gen_ui_python():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": graphs,
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||
RUN /storage/install-node.sh
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "unit_tests"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
# -- Installing UI dependencies --
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
|
||||
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of UI dependencies install --
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
# config_to_compose
|
||||
def test_config_to_compose_simple_config():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
|
||||
@@ -9,6 +9,7 @@ from bench.fanout_to_subgraph import fanout_to_subgraph, fanout_to_subgraph_sync
|
||||
from bench.pydantic_state import pydantic_state
|
||||
from bench.react_agent import react_agent
|
||||
from bench.sequential import create_sequential
|
||||
from bench.wide_dict import wide_dict
|
||||
from bench.wide_state import wide_state
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
@@ -251,6 +252,102 @@ benchmarks = (
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_25x300",
|
||||
wide_dict(300).compile(checkpointer=None),
|
||||
wide_dict(300).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(5)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_25x300_checkpoint",
|
||||
wide_dict(300).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(300).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(5)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_15x600",
|
||||
wide_dict(600).compile(checkpointer=None),
|
||||
wide_dict(600).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_15x600_checkpoint",
|
||||
wide_dict(600).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(600).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_9x1200",
|
||||
wide_dict(1200).compile(checkpointer=None),
|
||||
wide_dict(1200).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(3)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wide_dict_9x1200_checkpoint",
|
||||
wide_dict(1200).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(1200).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(3)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"sequential_10",
|
||||
create_sequential(10).compile(),
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import operator
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
|
||||
def wide_dict(n: int) -> StateGraph:
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
trigger_events: Annotated[list, operator.add]
|
||||
"""The external events that are converted by the graph."""
|
||||
primary_issue_medium: Annotated[str, lambda x, y: y or x]
|
||||
autoresponse: Annotated[Optional[dict], lambda _, y: y] # Always overwrite
|
||||
issue: Annotated[dict | None, lambda x, y: y if y else x]
|
||||
relevant_rules: Optional[list[dict]]
|
||||
"""SOPs fetched from the rulebook that are relevant to the current conversation."""
|
||||
memory_docs: Optional[list[dict]]
|
||||
"""Memory docs fetched from the memory service that are relevant to the current conversation."""
|
||||
categorizations: Annotated[list[dict], operator.add]
|
||||
"""The issue categorizations auto-generated by the AI."""
|
||||
responses: Annotated[list[dict], operator.add]
|
||||
"""The draft responses recommended by the AI."""
|
||||
|
||||
user_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x]
|
||||
"""The current user state (by email)."""
|
||||
crm_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x]
|
||||
"""The CRM information for organization the current user is from."""
|
||||
email_thread_id: Annotated[
|
||||
Optional[str], lambda x, y: y if y is not None else x
|
||||
]
|
||||
"""The current email thread ID."""
|
||||
slack_participants: Annotated[dict, operator.or_]
|
||||
"""The growing list of current slack participants."""
|
||||
bot_id: Optional[str]
|
||||
"""The ID of the bot user in the slack channel."""
|
||||
notified_assignees: Annotated[dict, operator.or_]
|
||||
|
||||
list_fields = {
|
||||
"messages",
|
||||
"trigger_events",
|
||||
"categorizations",
|
||||
"responses",
|
||||
"memory_docs",
|
||||
"relevant_rules",
|
||||
}
|
||||
dict_fields = {
|
||||
"user_info",
|
||||
"crm_info",
|
||||
"slack_participants",
|
||||
"notified_assignees",
|
||||
"autoresponse",
|
||||
"issue",
|
||||
}
|
||||
|
||||
def read_write(read: str, write: Sequence[str], input: State) -> dict:
|
||||
val = input.get(read)
|
||||
val = {val: val} if isinstance(val, str) else val
|
||||
val_single = val[-1] if isinstance(val, list) else val
|
||||
val_list = val if isinstance(val, list) else [val]
|
||||
return {
|
||||
k: val_list
|
||||
if k in list_fields
|
||||
else val_single
|
||||
if k in dict_fields
|
||||
else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
|
||||
for k in write
|
||||
}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_edge(START, "one")
|
||||
builder.add_node(
|
||||
"one",
|
||||
partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]),
|
||||
)
|
||||
builder.add_edge("one", "two")
|
||||
builder.add_node(
|
||||
"two",
|
||||
partial(read_write, "trigger_events", ["autoresponse", "issue"]),
|
||||
)
|
||||
builder.add_edge("two", "three")
|
||||
builder.add_edge("two", "four")
|
||||
builder.add_node(
|
||||
"three",
|
||||
partial(read_write, "autoresponse", ["relevant_rules"]),
|
||||
)
|
||||
builder.add_node(
|
||||
"four",
|
||||
partial(
|
||||
read_write,
|
||||
"trigger_events",
|
||||
["categorizations", "responses", "memory_docs"],
|
||||
),
|
||||
)
|
||||
builder.add_node(
|
||||
"five",
|
||||
partial(
|
||||
read_write,
|
||||
"categorizations",
|
||||
[
|
||||
"user_info",
|
||||
"crm_info",
|
||||
"email_thread_id",
|
||||
"slack_participants",
|
||||
"bot_id",
|
||||
"notified_assignees",
|
||||
],
|
||||
),
|
||||
)
|
||||
builder.add_edge(["three", "four"], "five")
|
||||
builder.add_edge("five", "six")
|
||||
builder.add_node(
|
||||
"six",
|
||||
partial(read_write, "responses", ["messages"]),
|
||||
)
|
||||
builder.add_conditional_edges(
|
||||
"six", lambda state: END if len(state["messages"]) > n else "one"
|
||||
)
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
graph = wide_dict(1000).compile(checkpointer=MemorySaver())
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(50)
|
||||
}
|
||||
for i in range(50)
|
||||
}
|
||||
]
|
||||
}
|
||||
config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
|
||||
|
||||
async def run():
|
||||
async for c in graph.astream(input, config=config):
|
||||
print(c.keys())
|
||||
|
||||
uvloop.install()
|
||||
asyncio.run(run())
|
||||
@@ -1,6 +1,7 @@
|
||||
import operator
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
|
||||
from langgraph.constants import END, START
|
||||
@@ -49,12 +50,34 @@ def wide_state(n: int) -> StateGraph:
|
||||
"""The ID of the bot user in the slack channel."""
|
||||
notified_assignees: Annotated[dict, operator.or_] = field(default_factory=dict)
|
||||
|
||||
list_fields = {
|
||||
"messages",
|
||||
"trigger_events",
|
||||
"categorizations",
|
||||
"responses",
|
||||
"memory_docs",
|
||||
"relevant_rules",
|
||||
}
|
||||
dict_fields = {
|
||||
"user_info",
|
||||
"crm_info",
|
||||
"slack_participants",
|
||||
"notified_assignees",
|
||||
"autoresponse",
|
||||
"issue",
|
||||
}
|
||||
|
||||
def read_write(read: str, write: Sequence[str], input: State) -> dict:
|
||||
val = getattr(input, read)
|
||||
val = {val: val} if isinstance(val, str) else val
|
||||
val_single = val[-1] if isinstance(val, list) else val
|
||||
val_list = val if isinstance(val, list) else [val]
|
||||
return {
|
||||
k: val_list if isinstance(getattr(input, k), list) else val_single
|
||||
k: val_list
|
||||
if k in list_fields
|
||||
else val_single
|
||||
if k in dict_fields
|
||||
else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
|
||||
for k in write
|
||||
}
|
||||
|
||||
|
||||
@@ -1,210 +1,55 @@
|
||||
import logging
|
||||
import weakref
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Annotated
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], "SchemaCoercionMapper"] = (
|
||||
weakref.WeakKeyDictionary()
|
||||
)
|
||||
|
||||
|
||||
class SchemaCoercionMapper:
|
||||
__slots__ = ("_inited", "schema", "_fields", "_construct", "_field_coercers")
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
schema: Type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
max_depth: int = 12,
|
||||
**kwargs: Any,
|
||||
) -> "SchemaCoercionMapper":
|
||||
if schema not in _cache:
|
||||
_cache[schema] = {}
|
||||
if max_depth in _cache[schema]:
|
||||
return _cache[schema][max_depth]
|
||||
|
||||
if schema in _cache:
|
||||
return _cache[schema]
|
||||
inst = super().__new__(cls)
|
||||
_cache[schema][max_depth] = inst
|
||||
_cache[schema] = inst
|
||||
return inst
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: Type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
max_depth: int = 12,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if hasattr(self, "_inited"):
|
||||
return
|
||||
self._inited = True
|
||||
self.schema = schema
|
||||
self.type_hints = (
|
||||
type_hints
|
||||
if type_hints is not None
|
||||
else get_type_hints(schema, localns={schema.__name__: schema})
|
||||
)
|
||||
self.max_depth = max_depth
|
||||
if issubclass(schema, BaseModelV1):
|
||||
self._construct: Callable[..., Any] = schema.parse_obj
|
||||
|
||||
if issubclass(schema, BaseModel):
|
||||
self._fields = {
|
||||
n: self.type_hints.get(n, f.annotation)
|
||||
for n, f in schema.model_fields.items()
|
||||
}
|
||||
self._construct: Callable[..., Any] = schema.model_construct
|
||||
elif issubclass(schema, BaseModel):
|
||||
self._construct = schema.model_validate
|
||||
|
||||
elif issubclass(schema, BaseModelV1):
|
||||
self._fields = {
|
||||
n: self.type_hints.get(n, f.annotation)
|
||||
for n, f in schema.__fields__.items()
|
||||
}
|
||||
self._construct = schema.construct
|
||||
else:
|
||||
raise TypeError("Schema is neither valid Pydantic v1 nor v2 model.")
|
||||
self._field_coercers: Optional[dict[str, Callable[[Any, Any], Any]]] = None
|
||||
|
||||
def __call__(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
return self.coerce(input_data, depth)
|
||||
|
||||
def coerce(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
if depth is None:
|
||||
depth = self.max_depth
|
||||
if not isinstance(input_data, dict) or depth <= 0:
|
||||
if not isinstance(input_data, dict):
|
||||
return input_data
|
||||
processed = {}
|
||||
if self._field_coercers is None:
|
||||
self._field_coercers = {
|
||||
n: self._build_coercer(t, depth - 1) for n, t in self._fields.items()
|
||||
}
|
||||
for k, v in input_data.items():
|
||||
fn = self._field_coercers.get(k)
|
||||
processed[k] = fn(v, depth - 1) if fn else v
|
||||
return self._construct(**processed)
|
||||
|
||||
def _build_coercer(
|
||||
self, field_type: Any, depth: int, throw: bool = False
|
||||
) -> Callable[[Any, Any], Any]:
|
||||
if depth == 0:
|
||||
return self._passthrough
|
||||
origin = get_origin(field_type)
|
||||
|
||||
if origin is Annotated:
|
||||
real_type, *_ = get_args(field_type)
|
||||
sub = self._build_coercer(real_type, depth - 1)
|
||||
return lambda v, d: sub(v, d)
|
||||
if isclass(field_type):
|
||||
is_class_ = True
|
||||
try:
|
||||
is_base_model = issubclass(field_type, BaseModel)
|
||||
except TypeError:
|
||||
is_class_ = False
|
||||
is_base_model = False
|
||||
|
||||
if is_base_model:
|
||||
mapper = SchemaCoercionMapper(field_type, max_depth=depth - 1)
|
||||
return lambda v, d: mapper.coerce(v, d) if isinstance(v, dict) else v
|
||||
if is_class_ and issubclass(field_type, BaseModelV1):
|
||||
mapper = SchemaCoercionMapper(field_type, max_depth=depth - 1)
|
||||
return lambda v, d: mapper.coerce(v, d) if isinstance(v, dict) else v
|
||||
if origin is list or field_type is list:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 1:
|
||||
return lambda v, d: v
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
|
||||
def list_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple)):
|
||||
return v
|
||||
return [sub(x, d - 1) for x in v]
|
||||
|
||||
return list_coercer
|
||||
if origin is set or field_type is set:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 1:
|
||||
return lambda v, d: v
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
|
||||
def set_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple, set)):
|
||||
return v
|
||||
return {sub(x, d - 1) for x in v}
|
||||
|
||||
return set_coercer
|
||||
if origin is dict or field_type is dict:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 2:
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
return v
|
||||
|
||||
return dict_coercer
|
||||
k_sub = self._build_coercer(args[0], depth - 1)
|
||||
v_sub = self._build_coercer(args[1], depth - 1)
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
return v
|
||||
return {k_sub(k, d - 1): v_sub(val, d - 1) for k, val in v.items()}
|
||||
|
||||
return dict_coercer
|
||||
|
||||
if origin is tuple:
|
||||
targs = get_args(field_type)
|
||||
if not targs:
|
||||
return lambda v, d: v
|
||||
subs = [self._build_coercer(a, depth - 1) for a in targs]
|
||||
|
||||
def tuple_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple)):
|
||||
return v
|
||||
out = []
|
||||
for i, sp in enumerate(subs):
|
||||
out.append(sp(v[i] if i < len(v) else None, d - 1))
|
||||
return tuple(out)
|
||||
|
||||
return tuple_coercer
|
||||
if origin is Union:
|
||||
uargs = get_args(field_type)
|
||||
subs, none_in_union = [], False
|
||||
for ix, arg in enumerate(uargs):
|
||||
if arg is type(None):
|
||||
none_in_union = True
|
||||
else:
|
||||
subs.append(
|
||||
self._build_coercer(arg, depth - 1, throw=ix < len(uargs) - 1)
|
||||
)
|
||||
|
||||
def union_coercer(v: Any, d: Any) -> Any:
|
||||
if v is None and none_in_union:
|
||||
return None
|
||||
err = None
|
||||
for sp in subs:
|
||||
try:
|
||||
return sp(v, d - 1)
|
||||
except TypeError as e:
|
||||
err = e
|
||||
if err:
|
||||
raise err
|
||||
return v
|
||||
|
||||
return union_coercer
|
||||
return self._passthrough
|
||||
|
||||
def _passthrough(self, v: Any, d: Any) -> Any:
|
||||
return v
|
||||
return self._construct(input_data)
|
||||
@@ -1060,7 +1060,7 @@ def _pick_mapper(
|
||||
if issubclass(schema, dict):
|
||||
return None
|
||||
if issubclass(schema, (BaseModel, BaseModelV1)):
|
||||
return SchemaCoercionMapper(schema, type_hints)
|
||||
return SchemaCoercionMapper(schema)
|
||||
return partial(_coerce_state, schema)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from langchain_core.messages import AnyMessage
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_SEND
|
||||
from langgraph.utils.config import get_config, get_stream_writer
|
||||
|
||||
|
||||
class UIMessage(TypedDict):
|
||||
"""A message type for UI updates in LangGraph.
|
||||
|
||||
This TypedDict represents a UI message that can be sent to update the UI state.
|
||||
It contains information about the UI component to render and its properties.
|
||||
|
||||
Attributes:
|
||||
type: Literal type indicating this is a UI message.
|
||||
id: Unique identifier for the UI message.
|
||||
name: Name of the UI component to render.
|
||||
props: Properties to pass to the UI component.
|
||||
metadata: Additional metadata about the UI message.
|
||||
"""
|
||||
|
||||
type: Literal["ui"]
|
||||
id: str
|
||||
name: str
|
||||
props: dict[str, Any]
|
||||
metadata: dict[str, Any]
|
||||
|
||||
|
||||
class RemoveUIMessage(TypedDict):
|
||||
"""A message type for removing UI components in LangGraph.
|
||||
|
||||
This TypedDict represents a message that can be sent to remove a UI component
|
||||
from the current state.
|
||||
|
||||
Attributes:
|
||||
type: Literal type indicating this is a remove-ui message.
|
||||
id: Unique identifier of the UI message to remove.
|
||||
"""
|
||||
|
||||
type: Literal["remove-ui"]
|
||||
id: str
|
||||
|
||||
|
||||
AnyUIMessage = Union[UIMessage, RemoveUIMessage]
|
||||
|
||||
|
||||
def push_ui_message(
|
||||
name: str,
|
||||
props: dict[str, Any],
|
||||
*,
|
||||
id: Optional[str] = None,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
message: Optional[AnyMessage] = None,
|
||||
state_key: str = "ui",
|
||||
) -> UIMessage:
|
||||
"""Push a new UI message to update the UI state.
|
||||
|
||||
This function creates and sends a UI message that will be rendered in the UI.
|
||||
It also updates the graph state with the new UI message.
|
||||
|
||||
Args:
|
||||
name: Name of the UI component to render.
|
||||
props: Properties to pass to the UI component.
|
||||
id: Optional unique identifier for the UI message.
|
||||
If not provided, a random UUID will be generated.
|
||||
metadata: Optional additional metadata about the UI message.
|
||||
message: Optional message object to associate with the UI message.
|
||||
state_key: Key in the graph state where the UI messages are stored.
|
||||
Defaults to "ui".
|
||||
|
||||
Returns:
|
||||
The created UI message.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
push_ui_message(
|
||||
name="component-name",
|
||||
props={"content": "Hello world"},
|
||||
)
|
||||
|
||||
"""
|
||||
writer = get_stream_writer()
|
||||
config = get_config()
|
||||
|
||||
message_id = None
|
||||
if message:
|
||||
if isinstance(message, dict) and "id" in message:
|
||||
message_id = message.get("id")
|
||||
elif hasattr(message, "id"):
|
||||
message_id = message.id
|
||||
|
||||
evt: UIMessage = {
|
||||
"type": "ui",
|
||||
"id": id or str(uuid4()),
|
||||
"name": name,
|
||||
"props": props,
|
||||
"metadata": {
|
||||
**(config.get("metadata") or {}),
|
||||
"tags": config.get("tags", None),
|
||||
"name": config.get("run_name", None),
|
||||
"run_id": config.get("run_id", None),
|
||||
**(metadata or {}),
|
||||
**({"message_id": message_id} if message_id else {}),
|
||||
},
|
||||
}
|
||||
|
||||
writer(evt)
|
||||
config[CONF][CONFIG_KEY_SEND]([(state_key, evt)])
|
||||
|
||||
return evt
|
||||
|
||||
|
||||
def delete_ui_message(id: str, *, state_key: str = "ui") -> RemoveUIMessage:
|
||||
"""Delete a UI message by ID from the UI state.
|
||||
|
||||
This function creates and sends a message to remove a UI component from the current state.
|
||||
It also updates the graph state to remove the UI message.
|
||||
|
||||
Args:
|
||||
id: Unique identifier of the UI component to remove.
|
||||
state_key: Key in the graph state where the UI messages are stored. Defaults to "ui".
|
||||
|
||||
Returns:
|
||||
The remove UI message.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
delete_ui_message("message-123")
|
||||
|
||||
"""
|
||||
writer = get_stream_writer()
|
||||
config = get_config()
|
||||
|
||||
evt: RemoveUIMessage = {"type": "remove-ui", "id": id}
|
||||
|
||||
writer(evt)
|
||||
config[CONF][CONFIG_KEY_SEND]([(state_key, evt)])
|
||||
|
||||
return evt
|
||||
|
||||
|
||||
def ui_message_reducer(
|
||||
left: Union[list[AnyUIMessage], AnyUIMessage],
|
||||
right: Union[list[AnyUIMessage], AnyUIMessage],
|
||||
) -> list[AnyUIMessage]:
|
||||
"""Merge two lists of UI messages, supporting removing UI messages.
|
||||
|
||||
This function combines two lists of UI messages, handling both regular UI messages
|
||||
and `remove-ui` messages. When a `remove-ui` message is encountered, it removes any
|
||||
UI message with the matching ID from the current state.
|
||||
|
||||
Args:
|
||||
left: First list of UI messages or single UI message.
|
||||
right: Second list of UI messages or single UI message.
|
||||
|
||||
Returns:
|
||||
Combined list of UI messages with removals applied.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
messages = ui_message_reducer(
|
||||
[{"type": "ui", "id": "1", "name": "Chat", "props": {}}],
|
||||
{"type": "remove-ui", "id": "1"}
|
||||
)
|
||||
|
||||
"""
|
||||
if not isinstance(left, list):
|
||||
left = [left]
|
||||
|
||||
if not isinstance(right, list):
|
||||
right = [right]
|
||||
|
||||
# merge messages
|
||||
merged = left.copy()
|
||||
merged_by_id = {m.get("id"): i for i, m in enumerate(merged)}
|
||||
ids_to_remove = set()
|
||||
|
||||
for msg in right:
|
||||
msg_id = msg.get("id")
|
||||
|
||||
if (existing_idx := merged_by_id.get(msg_id)) is not None:
|
||||
if msg.get("type") == "remove-ui":
|
||||
ids_to_remove.add(msg_id)
|
||||
else:
|
||||
ids_to_remove.discard(msg_id)
|
||||
merged[existing_idx] = msg
|
||||
else:
|
||||
if msg.get("type") == "remove-ui":
|
||||
raise ValueError(
|
||||
f"Attempting to delete an UI message with an ID that doesn't exist ('{msg_id}')"
|
||||
)
|
||||
|
||||
merged_by_id[msg_id] = len(merged)
|
||||
merged.append(msg)
|
||||
|
||||
merged = [m for m in merged if m.get("id") not in ids_to_remove]
|
||||
return merged
|
||||
@@ -39,7 +39,6 @@ from langchain_core.runnables.utils import (
|
||||
ConfigurableFieldSpec,
|
||||
get_unique_config_specs,
|
||||
)
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -125,6 +124,11 @@ from langgraph.utils.fields import get_enhanced_type_hints
|
||||
from langgraph.utils.pydantic import create_model, is_supported_by_pydantic
|
||||
from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined]
|
||||
|
||||
try:
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
except ImportError:
|
||||
_StreamingCallbackHandler = None # type: ignore
|
||||
|
||||
WriteValue = Union[Callable[[Input], Output], Any]
|
||||
|
||||
|
||||
@@ -2529,13 +2533,17 @@ class Pregel(PregelProtocol):
|
||||
run_id=config.get("run_id"),
|
||||
)
|
||||
# if running from astream_log() run each proc with streaming
|
||||
do_stream = next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
do_stream = (
|
||||
next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if _StreamingCallbackHandler is not None
|
||||
else False
|
||||
)
|
||||
try:
|
||||
# assign defaults
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -15,11 +16,16 @@ from uuid import UUID, uuid4
|
||||
from langchain_core.callbacks import BaseCallbackHandler
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, LLMResult
|
||||
from langchain_core.tracers._streaming import T, _StreamingCallbackHandler
|
||||
|
||||
from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM
|
||||
from langgraph.types import StreamChunk
|
||||
|
||||
try:
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
except ImportError:
|
||||
_StreamingCallbackHandler = object # type: ignore
|
||||
|
||||
T = TypeVar("T")
|
||||
Meta = tuple[tuple[str, ...], dict[str, Any]]
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
import orjson
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.graph import (
|
||||
Edge as DrawableEdge,
|
||||
@@ -35,6 +34,8 @@ from typing_extensions import Self
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_STREAM,
|
||||
INTERRUPT,
|
||||
@@ -46,6 +47,14 @@ from langgraph.pregel.types import All, PregelTask, StateSnapshot, StreamMode
|
||||
from langgraph.types import Command, Interrupt, StreamProtocol
|
||||
from langgraph.utils.config import merge_configs
|
||||
|
||||
CONF_DROPLIST = frozenset(
|
||||
(
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RemoteException(Exception):
|
||||
"""Exception raised when an error occurs in the remote graph."""
|
||||
@@ -290,47 +299,26 @@ class RemoteGraph(PregelProtocol):
|
||||
}
|
||||
|
||||
def _sanitize_config(self, config: RunnableConfig) -> RunnableConfig:
|
||||
reserved_configurable_keys = frozenset(
|
||||
[
|
||||
"callbacks",
|
||||
"checkpoint_map",
|
||||
"checkpoint_id",
|
||||
"checkpoint_ns",
|
||||
]
|
||||
)
|
||||
|
||||
def _sanitize_obj(obj: Any) -> Any:
|
||||
"""Remove non-JSON serializable fields from the given object."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sanitize_obj(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [_sanitize_obj(v) for v in obj]
|
||||
else:
|
||||
try:
|
||||
orjson.dumps(obj)
|
||||
return obj
|
||||
except orjson.JSONEncodeError:
|
||||
return None
|
||||
|
||||
# Remove non-JSON serializable fields from the config.
|
||||
config = _sanitize_obj(config)
|
||||
|
||||
# Only include configurable keys that are not reserved and
|
||||
# not starting with "__pregel_" prefix.
|
||||
new_configurable = {
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
|
||||
}
|
||||
|
||||
sanitized: RunnableConfig = {
|
||||
"tags": config.get("tags") or [],
|
||||
"metadata": config.get("metadata") or {},
|
||||
"configurable": new_configurable,
|
||||
}
|
||||
"""Sanitize the config to remove non-serializable fields."""
|
||||
sanitized: RunnableConfig = {}
|
||||
if "recursion_limit" in config:
|
||||
sanitized["recursion_limit"] = config["recursion_limit"]
|
||||
|
||||
if "tags" in config:
|
||||
sanitized["tags"] = [tag for tag in config["tags"] if isinstance(tag, str)]
|
||||
if "metadata" in config:
|
||||
sanitized["metadata"] = {}
|
||||
for k, v in config["metadata"].items():
|
||||
if isinstance(k, str) and isinstance(v, (str, int, float, bool)):
|
||||
sanitized["metadata"][k] = v
|
||||
if "configurable" in config:
|
||||
sanitized["configurable"] = {}
|
||||
for k, v in config["configurable"].items():
|
||||
if (
|
||||
isinstance(k, str)
|
||||
and k not in CONF_DROPLIST
|
||||
and isinstance(v, (str, int, float, bool))
|
||||
):
|
||||
sanitized["configurable"][k] = v
|
||||
return sanitized
|
||||
|
||||
def get_state(
|
||||
@@ -654,9 +642,10 @@ class RemoteGraph(PregelProtocol):
|
||||
# raise interrupt or errors
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
if caller_ns:
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
# filter for what was actually requested
|
||||
@@ -748,9 +737,10 @@ class RemoteGraph(PregelProtocol):
|
||||
# raise interrupt or errors
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
if caller_ns:
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
# filter for what was actually requested
|
||||
|
||||
@@ -36,7 +36,6 @@ from langchain_core.runnables.config import (
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph.constants import (
|
||||
@@ -54,6 +53,11 @@ from langgraph.utils.config import (
|
||||
patch_config,
|
||||
)
|
||||
|
||||
try:
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
except ImportError:
|
||||
_StreamingCallbackHandler = None # type: ignore
|
||||
|
||||
|
||||
def _set_config_context(
|
||||
config: RunnableConfig,
|
||||
@@ -683,13 +687,15 @@ class RunnableSeq(Runnable):
|
||||
iterator = step.stream(input, config, **kwargs)
|
||||
else:
|
||||
iterator = step.transform(iterator, config)
|
||||
if stream_handler := next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
if _StreamingCallbackHandler is not None and (
|
||||
stream_handler := next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
)
|
||||
):
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
iterator = stream_handler.tap_output_iter(run_manager.run_id, iterator)
|
||||
@@ -749,13 +755,15 @@ class RunnableSeq(Runnable):
|
||||
aiterator = step.atransform(aiterator, config)
|
||||
if hasattr(aiterator, "aclose"):
|
||||
stack.push_async_callback(aiterator.aclose)
|
||||
if stream_handler := next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
if _StreamingCallbackHandler is not None and (
|
||||
stream_handler := next(
|
||||
(
|
||||
cast(_StreamingCallbackHandler, h)
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
)
|
||||
):
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
aiterator = stream_handler.tap_output_aiter(
|
||||
|
||||
Generated
+4
-4
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -946,14 +946,14 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
version = "3.1.6"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
|
||||
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
|
||||
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
||||
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.3.24"
|
||||
version = "0.3.26"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -437,15 +437,17 @@ def test_stream():
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
# test raising graph interrupt if invoked as a subgraph
|
||||
with pytest.raises(GraphInterrupt) as exc:
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
# pretend we invoked this as a subgraph
|
||||
config={
|
||||
"configurable": {"thread_id": "thread_1", "checkpoint_ns": "some_ns"}
|
||||
},
|
||||
stream_mode="values",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
pass
|
||||
|
||||
assert exc.value.args[0] == [
|
||||
Interrupt(
|
||||
@@ -456,6 +458,15 @@ def test_stream():
|
||||
)
|
||||
]
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode="values",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data1"},
|
||||
{"chunk": "data2"},
|
||||
@@ -470,62 +481,62 @@ def test_stream():
|
||||
|
||||
# default stream_mode is updates
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data3"},
|
||||
{"chunk": "data4"},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
|
||||
# list stream_mode includes mode names
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
((), "updates", {"chunk": "data3"}),
|
||||
((), "updates", {"chunk": "data4"}),
|
||||
((), "updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + single mode
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
((), {"chunk": "data3"}),
|
||||
((), {"chunk": "data4"}),
|
||||
((), {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
|
||||
@@ -561,15 +572,17 @@ async def test_astream():
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
# test raising graph interrupt if invoked as a subgraph
|
||||
with pytest.raises(GraphInterrupt) as exc:
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
# pretend we invoked this as a subgraph
|
||||
config={
|
||||
"configurable": {"thread_id": "thread_1", "checkpoint_ns": "some_ns"}
|
||||
},
|
||||
stream_mode="values",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
pass
|
||||
|
||||
assert exc.value.args[0] == [
|
||||
Interrupt(
|
||||
@@ -580,6 +593,15 @@ async def test_astream():
|
||||
)
|
||||
]
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode="values",
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data1"},
|
||||
{"chunk": "data2"},
|
||||
@@ -596,62 +618,62 @@ async def test_astream():
|
||||
|
||||
# default stream_mode is updates
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data3"},
|
||||
{"chunk": "data4"},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
|
||||
# list stream_mode includes mode names
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
((), "updates", {"chunk": "data3"}),
|
||||
((), "updates", {"chunk": "data4"}),
|
||||
((), "updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + single mode
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
((), {"chunk": "data3"}),
|
||||
((), {"chunk": "data4"}),
|
||||
((), {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
async_iter = MagicMock()
|
||||
@@ -664,33 +686,33 @@ async def test_astream():
|
||||
|
||||
# subgraphs + list modes
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
stream_mode=["updates"],
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
(("my", "subgraph"), "updates", {"chunk": "data3"}),
|
||||
(("hello", "subgraph"), "updates", {"chunk": "data4"}),
|
||||
(("bye", "subgraph"), "updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + single mode
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
subgraphs=True,
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
(("my", "subgraph"), {"chunk": "data3"}),
|
||||
(("hello", "subgraph"), {"chunk": "data4"}),
|
||||
(("bye", "subgraph"), {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.63",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
@@ -340,6 +340,7 @@ export class AssistantsClient extends BaseClient {
|
||||
assistantId?: string;
|
||||
ifExists?: OnConflictBehavior;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}): Promise<Assistant> {
|
||||
return this.fetch<Assistant>("/assistants", {
|
||||
method: "POST",
|
||||
@@ -350,6 +351,7 @@ export class AssistantsClient extends BaseClient {
|
||||
assistant_id: payload.assistantId,
|
||||
if_exists: payload.ifExists,
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -367,6 +369,7 @@ export class AssistantsClient extends BaseClient {
|
||||
config?: Config;
|
||||
metadata?: Metadata;
|
||||
name?: string;
|
||||
description?: string;
|
||||
},
|
||||
): Promise<Assistant> {
|
||||
return this.fetch<Assistant>(`/assistants/${assistantId}`, {
|
||||
@@ -376,6 +379,7 @@ export class AssistantsClient extends BaseClient {
|
||||
config: payload.config,
|
||||
metadata: payload.metadata,
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,6 +113,9 @@ export interface AssistantBase {
|
||||
|
||||
/** The name of the assistant */
|
||||
name: string;
|
||||
|
||||
/** The description of the assistant */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AssistantVersion extends AssistantBase {}
|
||||
|
||||
@@ -19,7 +19,7 @@ export type StreamEvent =
|
||||
|
||||
export interface Send {
|
||||
node: string;
|
||||
input: Record<string, unknown> | null;
|
||||
input: unknown | null;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
|
||||
Reference in new issue
Block a user