mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-27 01:52:25 +02:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb781c0f8 | ||
|
|
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 |
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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:
|
||||
|
||||
@@ -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)"]
|
||||
|
||||
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"}
|
||||
|
||||
@@ -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
|
||||
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.25"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -19,7 +19,7 @@ export type StreamEvent =
|
||||
|
||||
export interface Send {
|
||||
node: string;
|
||||
input: Record<string, unknown> | null;
|
||||
input: unknown | null;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
|
||||
@@ -162,6 +162,13 @@ def get_client(
|
||||
transport = httpx.ASGITransport(app, root_path="/noauth")
|
||||
except Exception:
|
||||
url = "http://localhost:8123"
|
||||
try:
|
||||
from langgraph_api.config import HTTP_CONFIG
|
||||
|
||||
if prefix := HTTP_CONFIG.get("mount_prefix"):
|
||||
url = f"{url}{prefix}"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if transport is None:
|
||||
transport = httpx.AsyncHTTPTransport(retries=5)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.61"
|
||||
version = "0.1.62"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user