copilotkit 0.1.86a0__tar.gz → 0.1.87__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/PKG-INFO +2 -2
- copilotkit-0.1.87/copilotkit/a2ui.py +237 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/copilotkit_lg_middleware.py +18 -4
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/langgraph.py +12 -2
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/langgraph_agent.py +58 -6
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/langgraph_agui_agent.py +2 -2
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/pyproject.toml +4 -2
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/README.md +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/__init__.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/action.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/agent.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/crewai/__init__.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/crewai/copilotkit_integration.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/crewai/crewai_agent.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/crewai/crewai_sdk.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/exc.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/html.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/integrations/__init__.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/integrations/fastapi.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/langchain.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/logging.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/parameter.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/protocol.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/runloop.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/sdk.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/types.py +0 -0
- {copilotkit-0.1.86a0 → copilotkit-0.1.87}/copilotkit/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: copilotkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.87
|
|
4
4
|
Summary: CopilotKit python SDK
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: copilot,copilotkit,langgraph,langchain,ai,langsmith,langserve
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Provides-Extra: crewai
|
|
16
|
-
Requires-Dist: ag-ui-langgraph[fastapi] (>=0.0.
|
|
16
|
+
Requires-Dist: ag-ui-langgraph[fastapi] (>=0.0.29)
|
|
17
17
|
Requires-Dist: ag-ui-protocol (>=0.1.11)
|
|
18
18
|
Requires-Dist: crewai (==0.118.0) ; extra == "crewai"
|
|
19
19
|
Requires-Dist: fastapi (>=0.115.0,<1.0.0)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2UI helpers — build v0.9 A2UI operations from schema + data.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from copilotkit import a2ui
|
|
6
|
+
|
|
7
|
+
schema = a2ui.load_schema("flight_card.json")
|
|
8
|
+
|
|
9
|
+
@tool
|
|
10
|
+
def search_flights(flights: list[Flight]) -> str:
|
|
11
|
+
return a2ui.render([
|
|
12
|
+
a2ui.create_surface("my-surface"),
|
|
13
|
+
a2ui.update_components("my-surface", schema),
|
|
14
|
+
a2ui.update_data_model("my-surface", {"flights": flights}),
|
|
15
|
+
])
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_schema(path: str | Path) -> list[dict[str, Any]]:
|
|
26
|
+
"""Load an A2UI component schema from a JSON file."""
|
|
27
|
+
with open(path) as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def update_components(
|
|
32
|
+
surface_id: str,
|
|
33
|
+
components: list[dict[str, Any]],
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Build a v0.9 updateComponents operation."""
|
|
36
|
+
return {
|
|
37
|
+
"version": "v0.9",
|
|
38
|
+
"updateComponents": {
|
|
39
|
+
"surfaceId": surface_id,
|
|
40
|
+
"components": components,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_data_model(
|
|
46
|
+
surface_id: str,
|
|
47
|
+
data: Any,
|
|
48
|
+
path: str = "/",
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""Build a v0.9 updateDataModel operation with plain JSON value."""
|
|
51
|
+
return {
|
|
52
|
+
"version": "v0.9",
|
|
53
|
+
"updateDataModel": {
|
|
54
|
+
"surfaceId": surface_id,
|
|
55
|
+
"path": path,
|
|
56
|
+
"value": data,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/basic_catalog.json"
|
|
62
|
+
"""The catalog ID for the standard v0.9 basic catalog."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_surface(
|
|
66
|
+
surface_id: str,
|
|
67
|
+
catalog_id: str = BASIC_CATALOG_ID,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""Build a v0.9 createSurface operation."""
|
|
70
|
+
return {
|
|
71
|
+
"version": "v0.9",
|
|
72
|
+
"createSurface": {
|
|
73
|
+
"surfaceId": surface_id,
|
|
74
|
+
"catalogId": catalog_id,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
A2UI_OPERATIONS_KEY = "a2ui_operations"
|
|
80
|
+
"""The container key used to wrap A2UI operations for explicit detection."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render(
|
|
84
|
+
operations: list[dict[str, Any]]
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Wrap operations in the a2ui_operations container and serialize to JSON.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
operations: The A2UI v0.9 operations (createSurface, updateComponents, updateDataModel).
|
|
90
|
+
|
|
91
|
+
Example::
|
|
92
|
+
render(
|
|
93
|
+
operations=[...],
|
|
94
|
+
)
|
|
95
|
+
"""
|
|
96
|
+
result: dict[str, Any] = {A2UI_OPERATIONS_KEY: operations}
|
|
97
|
+
return json.dumps(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Dynamic A2UI prompt builder
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
DEFAULT_GENERATION_GUIDELINES = """\
|
|
105
|
+
Generate A2UI v0.9 JSON.
|
|
106
|
+
|
|
107
|
+
## A2UI Protocol Instructions
|
|
108
|
+
|
|
109
|
+
A2UI (Agent to UI) is a protocol for rendering rich UI surfaces from agent responses.
|
|
110
|
+
|
|
111
|
+
CRITICAL: You MUST call the render_a2ui tool with ALL of these arguments:
|
|
112
|
+
- surfaceId: A unique ID for the surface (e.g. "product-comparison")
|
|
113
|
+
- components: REQUIRED — the A2UI component array. NEVER omit this. Use a List with
|
|
114
|
+
children: { componentId: "card-id", path: "/items" } for repeating cards.
|
|
115
|
+
- data: OPTIONAL — a JSON object written to the root of the surface data model.
|
|
116
|
+
Use for pre-filling form values or providing data for path-bound components.
|
|
117
|
+
- every component must have the "component" field specifying the component type (e.g. "Text", "Image", "Row", "Column", "List", "Button", etc.)
|
|
118
|
+
|
|
119
|
+
COMPONENT ID RULES:
|
|
120
|
+
- Every component ID must be unique within the surface.
|
|
121
|
+
- A component MUST NOT reference itself as child/children. This causes a
|
|
122
|
+
circular dependency error. For example, if a component has id="avatar",
|
|
123
|
+
its child must be a DIFFERENT id (e.g. "avatar-img"), never "avatar".
|
|
124
|
+
- The child/children tree must be a DAG — no cycles allowed.
|
|
125
|
+
|
|
126
|
+
PATH RULES FOR TEMPLATES:
|
|
127
|
+
Components inside a repeating List use RELATIVE paths (no leading slash).
|
|
128
|
+
The path is resolved relative to each array item automatically.
|
|
129
|
+
If List has children: { componentId: "card", path: "/items" } and item has key "name",
|
|
130
|
+
use { "path": "name" } (NO leading slash — relative to item).
|
|
131
|
+
CRITICAL: Do NOT use "/name" (absolute) inside templates — use "name" (relative).
|
|
132
|
+
The List's own path ("/items") uses a leading slash (absolute), but all
|
|
133
|
+
components INSIDE the template card use paths WITHOUT leading slash.
|
|
134
|
+
Do NOT use "/items/0/name" or "/items/{@key}/name" — just "name".
|
|
135
|
+
|
|
136
|
+
COMPONENT VALUES — DEFAULT RULE:
|
|
137
|
+
Use inline literal values for ALL component properties. Pass strings, numbers,
|
|
138
|
+
arrays, and objects directly on the component. Do NOT use { "path": "..." }
|
|
139
|
+
objects unless the property's schema explicitly allows it (see exception below).
|
|
140
|
+
CRITICAL: USING { "path": "..." } ON A PROPERTY THAT DOES NOT DECLARE PATH
|
|
141
|
+
SUPPORT IN ITS SCHEMA WILL CAUSE A RUNTIME CRASH AND BREAK THE ENTIRE UI.
|
|
142
|
+
ALWAYS CHECK THE COMPONENT SCHEMA FIRST — IF THE PROPERTY ONLY ACCEPTS A
|
|
143
|
+
PLAIN TYPE, YOU MUST USE A LITERAL VALUE.
|
|
144
|
+
VERY IMPORTANT: THE APPLICATION WILL BREAK IF YOU DO NOT FOLLOW THIS RULE!
|
|
145
|
+
|
|
146
|
+
For example, a chart's "data" must always be an inline array:
|
|
147
|
+
"data": [{"label": "Jan", "value": 100}, {"label": "Feb", "value": 200}]
|
|
148
|
+
A metric's "value" must always be an inline string:
|
|
149
|
+
"value": "$1,200"
|
|
150
|
+
|
|
151
|
+
PATH BINDING EXCEPTION — SCHEMA-DRIVEN:
|
|
152
|
+
A few properties accept { "path": "/some/path" } as an alternative to a literal
|
|
153
|
+
value. You can identify these in the Available Components schema: the property
|
|
154
|
+
will list BOTH a literal type AND an object-with-path option. If a property only
|
|
155
|
+
shows a single type (string, number, array, etc.), it does NOT support path
|
|
156
|
+
binding — use a literal value only.
|
|
157
|
+
|
|
158
|
+
Path binding is typically used for editable form inputs so the client can write
|
|
159
|
+
user input back to the data model. When building forms:
|
|
160
|
+
- Bind input "value" to a data model path: "value": { "path": "/form/name" }
|
|
161
|
+
- Pre-fill via the "data" tool argument: "data": { "form": { "name": "Alice" } }
|
|
162
|
+
- Capture values on submit via button action context:
|
|
163
|
+
"action": { "event": { "name": "submit", "context": { "name": { "path": "/form/name" } } } }
|
|
164
|
+
|
|
165
|
+
REPEATING CONTENT uses a structural children format (not the same as value binding):
|
|
166
|
+
children: { componentId: "card-id", path: "/items" }
|
|
167
|
+
Components inside templates use RELATIVE paths (no leading slash): { "path": "name" }."""
|
|
168
|
+
|
|
169
|
+
DEFAULT_DESIGN_GUIDELINES = """\
|
|
170
|
+
Create polished, visually appealing interfaces:
|
|
171
|
+
- Always include a title heading (h2) for the surface, outside the List.
|
|
172
|
+
Wrap in a Column: [title, list] as root.
|
|
173
|
+
- For card templates, create clear visual hierarchy:
|
|
174
|
+
- h3 for primary text (names, titles)
|
|
175
|
+
- h2 for featured numbers (prices, scores) — makes them stand out
|
|
176
|
+
- caption for secondary info (ratings, categories, metadata)
|
|
177
|
+
- body for descriptions
|
|
178
|
+
- Use Divider between logical sections within cards.
|
|
179
|
+
- Use Row with justify="spaceBetween" for label-value pairs
|
|
180
|
+
(e.g. "Rating" on left, "4.5/5" on right).
|
|
181
|
+
- Include images when relevant (logos, icons, product photos):
|
|
182
|
+
- Use Image component with variant="smallFeature" or "avatar"
|
|
183
|
+
- Prefer company logos for branded products — Google favicons are reliable:
|
|
184
|
+
https://www.google.com/s2/favicons?domain=sony.com&sz=128
|
|
185
|
+
https://www.google.com/s2/favicons?domain=bose.com&sz=128
|
|
186
|
+
- For generic icons: https://placehold.co/128x128/EEE/999?text=🎧
|
|
187
|
+
- Do NOT invent Unsplash photo-IDs — they will 404. Only use real, known URLs.
|
|
188
|
+
- Use horizontal List direction for side-by-side comparison cards.
|
|
189
|
+
- Keep cards clean — avoid clutter. Whitespace is good.
|
|
190
|
+
- Use consistent surfaceIds (lowercase, hyphenated).
|
|
191
|
+
- NEVER use the same ID for a component and its child — this creates a
|
|
192
|
+
circular dependency. E.g. if id="avatar", child must NOT be "avatar".
|
|
193
|
+
- Both Row and Column support "justify" and "align".
|
|
194
|
+
- Add Button for interactivity. Button needs child (Text ID) + action.
|
|
195
|
+
Action MUST use this exact nested format:
|
|
196
|
+
"action": { "event": { "name": "myAction", "context": { "key": "value" } } }
|
|
197
|
+
The "event" key holds an OBJECT with "name" (required) and "context" (optional).
|
|
198
|
+
Do NOT use a flat format like {"event": "name"} — "event" must be an object.
|
|
199
|
+
Use variant="primary" for main action buttons, variant="borderless" for links.
|
|
200
|
+
- For forms: wrap fields in a Card with a Column. Place the submit button in a
|
|
201
|
+
Row with justify="end". Every input MUST use path binding on the "value" property
|
|
202
|
+
(e.g. "value": { "path": "/form/name" }) to be editable. The submit button's action
|
|
203
|
+
context MUST reference the same paths to capture the user's input.
|
|
204
|
+
|
|
205
|
+
Use the SAME surfaceId as the main surface. Match action names to Button action event names."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def a2ui_prompt(
|
|
209
|
+
component_schema: str,
|
|
210
|
+
generation_guidelines: str = DEFAULT_GENERATION_GUIDELINES,
|
|
211
|
+
design_guidelines: str = DEFAULT_DESIGN_GUIDELINES,
|
|
212
|
+
) -> str:
|
|
213
|
+
"""Build the system prompt for dynamic A2UI generation.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
component_schema: JSON string of available components and their props.
|
|
217
|
+
Read from state["ag-ui"]["a2ui_schema"].
|
|
218
|
+
generation_guidelines: Instructions for how to call the render_a2ui
|
|
219
|
+
tool, path rules, and data format.
|
|
220
|
+
design_guidelines: Visual design rules, component hierarchy tips,
|
|
221
|
+
and action handler patterns.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Complete system prompt string.
|
|
225
|
+
"""
|
|
226
|
+
return f"""\
|
|
227
|
+
{generation_guidelines}
|
|
228
|
+
|
|
229
|
+
## DESIGN GUIDELINES:
|
|
230
|
+
{design_guidelines}
|
|
231
|
+
|
|
232
|
+
## AVAILABLE COMPONENTS:
|
|
233
|
+
The following components are available for building UI surfaces.
|
|
234
|
+
Use ONLY these components with the specified props.
|
|
235
|
+
|
|
236
|
+
{component_schema}
|
|
237
|
+
"""
|
|
@@ -26,7 +26,6 @@ from langchain.agents.middleware import (
|
|
|
26
26
|
ModelResponse,
|
|
27
27
|
)
|
|
28
28
|
from langgraph.runtime import Runtime
|
|
29
|
-
from ag_ui_langgraph import make_json_safe
|
|
30
29
|
|
|
31
30
|
from .langgraph import CopilotKitProperties
|
|
32
31
|
|
|
@@ -263,7 +262,15 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
263
262
|
if isinstance(app_context, str):
|
|
264
263
|
context_content = app_context
|
|
265
264
|
else:
|
|
266
|
-
|
|
265
|
+
# Handle Pydantic models (e.g. ag_ui Context)
|
|
266
|
+
if hasattr(app_context, "model_dump"):
|
|
267
|
+
app_context = app_context.model_dump()
|
|
268
|
+
elif isinstance(app_context, list):
|
|
269
|
+
app_context = [
|
|
270
|
+
item.model_dump() if hasattr(item, "model_dump") else item
|
|
271
|
+
for item in app_context
|
|
272
|
+
]
|
|
273
|
+
context_content = json.dumps(app_context, indent=2)
|
|
267
274
|
|
|
268
275
|
context_message_content = f"App Context:\n{context_content}"
|
|
269
276
|
context_message_prefix = "App Context:\n"
|
|
@@ -301,8 +308,15 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
301
308
|
existing_context_index = i
|
|
302
309
|
break
|
|
303
310
|
|
|
304
|
-
# Create the context message
|
|
305
|
-
|
|
311
|
+
# Create the context message.
|
|
312
|
+
# When replacing an existing context message, reuse its ID so the
|
|
313
|
+
# add_messages reducer updates in-place instead of appending a
|
|
314
|
+
# duplicate at the end of the message list.
|
|
315
|
+
if existing_context_index != -1:
|
|
316
|
+
existing_id = getattr(messages[existing_context_index], "id", None)
|
|
317
|
+
context_message = SystemMessage(content=context_message_content, id=existing_id)
|
|
318
|
+
else:
|
|
319
|
+
context_message = SystemMessage(content=context_message_content)
|
|
306
320
|
|
|
307
321
|
if existing_context_index != -1:
|
|
308
322
|
# Replace existing context message
|
|
@@ -142,9 +142,19 @@ def langchain_messages_to_copilotkit(
|
|
|
142
142
|
if hasattr(message, "content"):
|
|
143
143
|
content = message.content
|
|
144
144
|
|
|
145
|
-
#
|
|
145
|
+
# Content can be a list of content blocks (e.g. Anthropic models).
|
|
146
|
+
# Extract and concatenate all text parts instead of only taking
|
|
147
|
+
# the first element.
|
|
146
148
|
if isinstance(content, list):
|
|
147
|
-
|
|
149
|
+
text_parts = []
|
|
150
|
+
for part in content:
|
|
151
|
+
if isinstance(part, str):
|
|
152
|
+
text_parts.append(part)
|
|
153
|
+
elif isinstance(part, dict) and part.get("type") == "text":
|
|
154
|
+
text_parts.append(part.get("text", ""))
|
|
155
|
+
elif isinstance(part, dict) and "text" in part:
|
|
156
|
+
text_parts.append(part.get("text", ""))
|
|
157
|
+
content = "".join(text_parts)
|
|
148
158
|
|
|
149
159
|
# Anthropic models return a dict with a "text" key
|
|
150
160
|
if isinstance(content, dict):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
4
|
import json
|
|
5
|
+
import math
|
|
5
6
|
from typing import Optional, List, Callable, Any, cast, Union, TypedDict, Literal
|
|
6
7
|
|
|
7
8
|
from langgraph.graph.state import CompiledStateGraph
|
|
@@ -18,6 +19,22 @@ except ImportError:
|
|
|
18
19
|
from langchain_core.messages import BaseMessage, SystemMessage
|
|
19
20
|
|
|
20
21
|
from langchain_core.runnables import RunnableConfig, ensure_config
|
|
22
|
+
|
|
23
|
+
def _serialize_state(state):
|
|
24
|
+
"""Recursively convert Pydantic BaseModel instances to dicts for serialization."""
|
|
25
|
+
try:
|
|
26
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
27
|
+
except ImportError:
|
|
28
|
+
return state
|
|
29
|
+
|
|
30
|
+
if isinstance(state, PydanticBaseModel):
|
|
31
|
+
return state.model_dump()
|
|
32
|
+
elif isinstance(state, dict):
|
|
33
|
+
return {k: _serialize_state(v) for k, v in state.items()}
|
|
34
|
+
elif isinstance(state, (list, tuple)):
|
|
35
|
+
return type(state)(_serialize_state(item) for item in state)
|
|
36
|
+
return state
|
|
37
|
+
|
|
21
38
|
from langchain_core.messages import HumanMessage
|
|
22
39
|
|
|
23
40
|
from partialjson.json_parser import JSONParser
|
|
@@ -31,6 +48,20 @@ from .logging import get_logger
|
|
|
31
48
|
|
|
32
49
|
logger = get_logger(__name__)
|
|
33
50
|
|
|
51
|
+
|
|
52
|
+
def _sanitize_for_json(obj):
|
|
53
|
+
"""Replace NaN and Infinity float values with None for valid JSON serialization."""
|
|
54
|
+
if isinstance(obj, float):
|
|
55
|
+
if math.isnan(obj) or math.isinf(obj):
|
|
56
|
+
return None
|
|
57
|
+
return obj
|
|
58
|
+
if isinstance(obj, dict):
|
|
59
|
+
return {k: _sanitize_for_json(v) for k, v in obj.items()}
|
|
60
|
+
if isinstance(obj, (list, tuple)):
|
|
61
|
+
return [_sanitize_for_json(item) for item in obj]
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
|
|
34
65
|
class CopilotKitConfig(TypedDict):
|
|
35
66
|
"""
|
|
36
67
|
CopilotKit config for LangGraphAgent
|
|
@@ -236,7 +267,12 @@ class LangGraphAgent(Agent):
|
|
|
236
267
|
actions=actions,
|
|
237
268
|
agent_name=self.name
|
|
238
269
|
)
|
|
239
|
-
|
|
270
|
+
# Only update graph state with keys that merge_state explicitly produced,
|
|
271
|
+
# not keys that were simply passed through from state_input unchanged.
|
|
272
|
+
# This preserves graph-owned state keys that the frontend may have sent stale values for.
|
|
273
|
+
for key, value in state.items():
|
|
274
|
+
if key not in state_input or value is not state_input.get(key):
|
|
275
|
+
current_graph_state[key] = value
|
|
240
276
|
lg_interrupt_meta_event = next((ev for ev in (meta_events or []) if ev.get("name") == "LangGraphInterruptEvent"), None)
|
|
241
277
|
has_active_interrupts = active_interrupts is not None and len(active_interrupts) > 0
|
|
242
278
|
|
|
@@ -435,7 +471,9 @@ class LangGraphAgent(Agent):
|
|
|
435
471
|
manually_emitted_state = None
|
|
436
472
|
|
|
437
473
|
if manually_emit_intermediate_state:
|
|
438
|
-
manually_emitted_state = cast(Any, event["data"])
|
|
474
|
+
manually_emitted_state = _merge_emit_state(current_graph_state, cast(Any, event["data"]))
|
|
475
|
+
if isinstance(manually_emitted_state, dict):
|
|
476
|
+
current_graph_state.update(manually_emitted_state)
|
|
439
477
|
yield self._emit_state_sync_event(
|
|
440
478
|
thread_id=thread_id,
|
|
441
479
|
run_id=run_id,
|
|
@@ -489,7 +527,7 @@ class LangGraphAgent(Agent):
|
|
|
489
527
|
active=not exiting_node
|
|
490
528
|
) + "\n"
|
|
491
529
|
|
|
492
|
-
yield langchain_dumps(event) + "\n"
|
|
530
|
+
yield langchain_dumps(_sanitize_for_json(event)) + "\n"
|
|
493
531
|
except Exception as error:
|
|
494
532
|
# Emit error information through streaming protocol before terminating
|
|
495
533
|
# This preserves the semantic error details that would otherwise be lost
|
|
@@ -591,6 +629,9 @@ class LangGraphAgent(Agent):
|
|
|
591
629
|
# Filter by schema keys if available
|
|
592
630
|
state = self.filter_state_on_schema_keys(state, 'output')
|
|
593
631
|
|
|
632
|
+
# Convert Pydantic BaseModel instances to dicts for serialization
|
|
633
|
+
state = _serialize_state(state)
|
|
634
|
+
|
|
594
635
|
return langchain_dumps({
|
|
595
636
|
"event": "on_copilotkit_state_sync",
|
|
596
637
|
"thread_id": thread_id,
|
|
@@ -598,7 +639,7 @@ class LangGraphAgent(Agent):
|
|
|
598
639
|
"agent_name": self.name,
|
|
599
640
|
"node_name": node_name,
|
|
600
641
|
"active": active,
|
|
601
|
-
"state": state,
|
|
642
|
+
"state": _sanitize_for_json(state),
|
|
602
643
|
"running": running,
|
|
603
644
|
"role": "assistant"
|
|
604
645
|
})
|
|
@@ -636,10 +677,13 @@ class LangGraphAgent(Agent):
|
|
|
636
677
|
state_copy = state.copy()
|
|
637
678
|
state_copy.pop("messages", None)
|
|
638
679
|
|
|
680
|
+
# Convert Pydantic BaseModel instances to dicts for serialization
|
|
681
|
+
state_copy = _serialize_state(state_copy)
|
|
682
|
+
|
|
639
683
|
return {
|
|
640
684
|
"threadId": thread_id,
|
|
641
685
|
"threadExists": True,
|
|
642
|
-
"state": state_copy,
|
|
686
|
+
"state": _sanitize_for_json(state_copy),
|
|
643
687
|
"messages": messages
|
|
644
688
|
}
|
|
645
689
|
|
|
@@ -687,7 +731,8 @@ class LangGraphAgent(Agent):
|
|
|
687
731
|
if hasattr(self, schema_keys_name) and getattr(self, schema_keys_name):
|
|
688
732
|
return filter_by_schema_keys(state, getattr(self, schema_keys_name))
|
|
689
733
|
except Exception:
|
|
690
|
-
|
|
734
|
+
pass
|
|
735
|
+
return state
|
|
691
736
|
|
|
692
737
|
def get_interrupt_event(self, value):
|
|
693
738
|
if not isinstance(value, str) and "__copilotkit_interrupt_value__" in value:
|
|
@@ -724,6 +769,13 @@ class LangGraphAgent(Agent):
|
|
|
724
769
|
|
|
725
770
|
raise ValueError("Message ID not found in history")
|
|
726
771
|
|
|
772
|
+
|
|
773
|
+
def _merge_emit_state(current_state: dict, emitted_state: Any) -> dict:
|
|
774
|
+
"""Merge emitted state on top of current graph state instead of replacing it."""
|
|
775
|
+
if isinstance(emitted_state, dict):
|
|
776
|
+
return {**current_state, **emitted_state}
|
|
777
|
+
return cast(Any, emitted_state)
|
|
778
|
+
|
|
727
779
|
class _StreamingStateExtractor:
|
|
728
780
|
def __init__(self, emit_intermediate_state: List[dict]):
|
|
729
781
|
self.emit_intermediate_state = emit_intermediate_state
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import Dict, Any, List, Optional, Union, AsyncGenerator
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from ag_ui_langgraph import LangGraphAgent
|
|
4
|
+
from ag_ui_langgraph import LangGraphAgent
|
|
5
5
|
from ag_ui.core import (
|
|
6
6
|
EventType,
|
|
7
7
|
CustomEvent,
|
|
@@ -108,7 +108,7 @@ class LangGraphAGUIAgent(LangGraphAgent):
|
|
|
108
108
|
type=EventType.TOOL_CALL_ARGS,
|
|
109
109
|
tool_call_id=custom_event.value["id"],
|
|
110
110
|
delta=custom_event.value["args"] if isinstance(custom_event.value["args"], str) else json.dumps(
|
|
111
|
-
|
|
111
|
+
custom_event.value["args"]),
|
|
112
112
|
raw_event=event,
|
|
113
113
|
)
|
|
114
114
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "copilotkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.87"
|
|
4
4
|
description = "CopilotKit python SDK"
|
|
5
5
|
authors = ["Markus Ecker <markus.ecker@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -16,13 +16,15 @@ keywords = [
|
|
|
16
16
|
"langsmith",
|
|
17
17
|
"langserve",
|
|
18
18
|
]
|
|
19
|
+
packages = [{ include = "copilotkit" }]
|
|
20
|
+
include = ["copilotkit/*.json"]
|
|
19
21
|
|
|
20
22
|
[tool.poetry.dependencies]
|
|
21
23
|
python = ">=3.10,<3.13"
|
|
22
24
|
langgraph = { version = ">=0.3.25,<2" }
|
|
23
25
|
langchain = { version = ">=0.3.0" }
|
|
24
26
|
crewai = { version = "0.118.0", optional = true }
|
|
25
|
-
ag-ui-langgraph = { version = ">=0.0.
|
|
27
|
+
ag-ui-langgraph = { version = ">=0.0.29", extras = ["fastapi"] }
|
|
26
28
|
ag-ui-protocol = ">=0.1.11"
|
|
27
29
|
fastapi = ">=0.115.0,<1.0.0"
|
|
28
30
|
partialjson = "^0.0.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|