copilotkit 0.1.93__tar.gz → 0.1.94__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.93 → copilotkit-0.1.94}/PKG-INFO +3 -3
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/copilotkit_lg_middleware.py +234 -7
- {copilotkit-0.1.93 → copilotkit-0.1.94}/pyproject.toml +2 -2
- {copilotkit-0.1.93 → copilotkit-0.1.94}/README.md +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/__init__.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/a2ui.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/action.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/agent.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/crewai/__init__.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/crewai/copilotkit_integration.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/crewai/crewai_agent.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/crewai/crewai_sdk.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/exc.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/header_propagation.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/html.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/integrations/__init__.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/integrations/fastapi.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/langchain.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/langgraph.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/langgraph_agui_agent.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/logging.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/parameter.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/protocol.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/py.typed +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/runloop.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/sdk.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/types.py +0 -0
- {copilotkit-0.1.93 → copilotkit-0.1.94}/copilotkit/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: copilotkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.94
|
|
4
4
|
Summary: CopilotKit python SDK
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: copilot,copilotkit,langgraph,langchain,ai,langsmith,langserve
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Provides-Extra: crewai
|
|
18
|
-
Requires-Dist: ag-ui-langgraph[fastapi] (>=0.0.
|
|
18
|
+
Requires-Dist: ag-ui-langgraph[fastapi] (>=0.0.38)
|
|
19
19
|
Requires-Dist: ag-ui-protocol (>=0.1.15)
|
|
20
20
|
Requires-Dist: crewai (>=0.118.0) ; (python_version >= "3.10" and python_version < "3.14") and (extra == "crewai")
|
|
21
21
|
Requires-Dist: fastapi (>=0.115.0,<1.0.0)
|
|
@@ -30,10 +30,51 @@ from langgraph.runtime import Runtime
|
|
|
30
30
|
from .header_propagation import install_httpx_hook, set_forwarded_headers
|
|
31
31
|
from .langgraph import CopilotKitProperties
|
|
32
32
|
|
|
33
|
+
# Optional dependency: the A2UI subagent-tool factory ships in ag-ui-langgraph.
|
|
34
|
+
# Guarded so an older/skewed version without the factory degrades to
|
|
35
|
+
# "no auto-A2UI" instead of breaking the whole middleware import.
|
|
36
|
+
try: # pragma: no cover - exercised indirectly via the a2ui injection path
|
|
37
|
+
from ag_ui_langgraph import get_a2ui_tools
|
|
38
|
+
except Exception: # noqa: BLE001 - any import failure means the feature is off
|
|
39
|
+
get_a2ui_tools = None
|
|
40
|
+
|
|
33
41
|
# Track which httpx clients already have the header-propagation hook installed
|
|
34
42
|
# (by object id) so we never double-install on repeated model calls.
|
|
35
43
|
_hooked_clients: set[int] = set()
|
|
36
44
|
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Auto-A2UI: bridge the inferred model from the model-call hook to the
|
|
47
|
+
# tool-call hook
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# The generate_a2ui tool drives a structured-output subagent and so needs a
|
|
50
|
+
# chat model. We "infer" that model from ``request.model`` in
|
|
51
|
+
# ``wrap_model_call`` (the only hook that exposes the bound model) and reuse it.
|
|
52
|
+
# But the tool actually *executes* later in ``wrap_tool_call``, whose request
|
|
53
|
+
# does NOT carry the model. ContextVars do not reliably survive LangGraph node
|
|
54
|
+
# boundaries, so we bridge the built tool across nodes via a module-level map
|
|
55
|
+
# keyed by the run's thread id.
|
|
56
|
+
_a2ui_tools_by_thread: dict[str, Any] = {}
|
|
57
|
+
|
|
58
|
+
# Fallback key for runs without a thread id (e.g. an in-memory invoke with no
|
|
59
|
+
# checkpointer). Collisions across concurrent context-less runs are an
|
|
60
|
+
# acceptable edge — the deployed path always carries a thread id.
|
|
61
|
+
_DEFAULT_THREAD_KEY = "__copilotkit_a2ui_default__"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _current_thread_id() -> "str | None":
|
|
65
|
+
"""Best-effort read of the active run's thread id from the LangGraph config.
|
|
66
|
+
|
|
67
|
+
Returns ``None`` outside a runnable context (e.g. unit tests); callers then
|
|
68
|
+
fall back to ``_DEFAULT_THREAD_KEY``.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
from langgraph.config import get_config
|
|
72
|
+
|
|
73
|
+
cfg = get_config() or {}
|
|
74
|
+
return (cfg.get("configurable") or {}).get("thread_id")
|
|
75
|
+
except Exception: # noqa: BLE001 - no active context / older langgraph
|
|
76
|
+
return None
|
|
77
|
+
|
|
37
78
|
|
|
38
79
|
def _extract_forwarded_headers_from_config() -> None:
|
|
39
80
|
"""Extract raw ``x-*`` headers from the current LangGraph RunnableConfig and
|
|
@@ -273,7 +314,131 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
273
314
|
system_message=SystemMessage(content=f"{base}\n\n{note}")
|
|
274
315
|
)
|
|
275
316
|
|
|
276
|
-
#
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
# Auto-A2UI tool injection
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _resolve_a2ui_catalog(state: dict) -> "tuple[str | None, str | None] | None":
|
|
323
|
+
"""Find the frontend-registered A2UI catalog wherever it was passed.
|
|
324
|
+
|
|
325
|
+
Returns ``(component_schema, catalog_id)`` when a catalog is present,
|
|
326
|
+
else ``None`` (so the tool is never advertised when the client can't
|
|
327
|
+
render A2UI). Two delivery paths are supported, because the catalog
|
|
328
|
+
lands in different places depending on how the agent is served:
|
|
329
|
+
|
|
330
|
+
- **AG-UI native endpoint** → ``state["ag-ui"]["a2ui_schema"]``, a JSON
|
|
331
|
+
string ``{"catalogId": ..., "components": [...]}``.
|
|
332
|
+
- **CopilotKit runtime proxy** → a ``state["copilotkit"]["context"]``
|
|
333
|
+
entry describing the A2UI catalog (catalog id + component schemas as
|
|
334
|
+
text).
|
|
335
|
+
|
|
336
|
+
``component_schema`` is the text/JSON the subagent should compose from;
|
|
337
|
+
``catalog_id`` binds generated surfaces to the frontend's catalog (so
|
|
338
|
+
BYOC custom catalogs render their own components, not the basic one).
|
|
339
|
+
"""
|
|
340
|
+
# AG-UI native path.
|
|
341
|
+
ag_ui = state.get("ag-ui") or {}
|
|
342
|
+
a2ui_schema = ag_ui.get("a2ui_schema")
|
|
343
|
+
if a2ui_schema:
|
|
344
|
+
catalog_id = None
|
|
345
|
+
try:
|
|
346
|
+
parsed = (
|
|
347
|
+
json.loads(a2ui_schema)
|
|
348
|
+
if isinstance(a2ui_schema, str)
|
|
349
|
+
else a2ui_schema
|
|
350
|
+
)
|
|
351
|
+
if isinstance(parsed, dict):
|
|
352
|
+
catalog_id = parsed.get("catalogId")
|
|
353
|
+
except (TypeError, ValueError):
|
|
354
|
+
pass
|
|
355
|
+
# Native path: the toolkit reads ``a2ui_schema`` from state itself,
|
|
356
|
+
# so no composition_guide is needed — just surface the catalog id.
|
|
357
|
+
return None, catalog_id
|
|
358
|
+
|
|
359
|
+
# CopilotKit runtime-proxy path: the catalog arrives as a context entry.
|
|
360
|
+
context = (state.get("copilotkit") or {}).get("context") or []
|
|
361
|
+
for entry in context:
|
|
362
|
+
if not isinstance(entry, dict):
|
|
363
|
+
continue
|
|
364
|
+
description = entry.get("description") or ""
|
|
365
|
+
value = entry.get("value") or ""
|
|
366
|
+
if "A2UI catalog" not in description or not value:
|
|
367
|
+
continue
|
|
368
|
+
# The value lists catalogs as "- <catalogId>" lines; the first is
|
|
369
|
+
# the custom catalog the client registered.
|
|
370
|
+
match = re.search(r"(?m)^\s*-\s+(\S+)", value)
|
|
371
|
+
catalog_id = match.group(1) if match else None
|
|
372
|
+
return value, catalog_id
|
|
373
|
+
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _a2ui_inject_decision(state: dict) -> "bool | str | None":
|
|
378
|
+
"""Return the A2UI ``injectA2UITool`` decision, or ``None``.
|
|
379
|
+
|
|
380
|
+
The ``@ag-ui/a2ui-middleware`` forwards its ``injectA2UITool`` setting on
|
|
381
|
+
``forwardedProps``, which ``ag-ui-langgraph`` surfaces into agent state at
|
|
382
|
+
``state["ag-ui"]["inject_a2ui_tool"]`` — present only when the host turned
|
|
383
|
+
the runtime A2UI tool on (truthy or a custom tool-name string). ``None``
|
|
384
|
+
means no signal at all (off, or no A2UI middleware in the pipeline), in
|
|
385
|
+
which case we do not auto-inject.
|
|
386
|
+
"""
|
|
387
|
+
return (state.get("ag-ui") or {}).get("inject_a2ui_tool")
|
|
388
|
+
|
|
389
|
+
def _maybe_build_a2ui_tool(self, request: ModelRequest) -> Any | None:
|
|
390
|
+
"""Build a ``generate_a2ui`` tool bound to the agent's own model when
|
|
391
|
+
A2UI tool injection is turned on for this run.
|
|
392
|
+
|
|
393
|
+
Gating, in order:
|
|
394
|
+
|
|
395
|
+
1. **Opt-in.** Only inject when the A2UI ``injectA2UITool`` flag is
|
|
396
|
+
truthy (forwarded by ``@ag-ui/a2ui-middleware`` and surfaced at
|
|
397
|
+
``state["ag-ui"]["inject_a2ui_tool"]``). No flag → no injection. This
|
|
398
|
+
is the whole contract: "no injectA2UITool, no A2UI tool injection."
|
|
399
|
+
2. **No double-inject.** If the agent already exposes a tool with the
|
|
400
|
+
same name (e.g. a backend-defined ``generate_a2ui``), don't inject —
|
|
401
|
+
the host owns it, and a duplicate would show the model two tools with
|
|
402
|
+
one name.
|
|
403
|
+
|
|
404
|
+
The model is inferred from ``request.model`` (the bound agent model); the
|
|
405
|
+
component schema and catalog id come from the registered catalog (when
|
|
406
|
+
present) so the subagent composes the right components and surfaces bind
|
|
407
|
+
to the frontend's catalog — otherwise the toolkit's basic catalog is
|
|
408
|
+
used. The built tool is stashed for the tool-call hook to execute.
|
|
409
|
+
Returns the tool or ``None`` when A2UI is not applicable.
|
|
410
|
+
"""
|
|
411
|
+
if get_a2ui_tools is None:
|
|
412
|
+
return None
|
|
413
|
+
state = request.state or {}
|
|
414
|
+
|
|
415
|
+
# (1) Opt-in: only inject when the host turned the A2UI tool on.
|
|
416
|
+
if not self._a2ui_inject_decision(state):
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
# Bind to the frontend's catalog when one was registered (optional).
|
|
420
|
+
resolved = self._resolve_a2ui_catalog(state)
|
|
421
|
+
component_schema, catalog_id = resolved if resolved else (None, None)
|
|
422
|
+
|
|
423
|
+
kwargs: dict[str, Any] = {}
|
|
424
|
+
if catalog_id:
|
|
425
|
+
kwargs["default_catalog_id"] = catalog_id
|
|
426
|
+
# Feed the registered component schema to the subagent so it composes
|
|
427
|
+
# only catalog components (the toolkit appends this to its prompt).
|
|
428
|
+
if component_schema:
|
|
429
|
+
kwargs["composition_guide"] = component_schema
|
|
430
|
+
|
|
431
|
+
tool = get_a2ui_tools(request.model, **kwargs)
|
|
432
|
+
|
|
433
|
+
# (2) Don't double-inject if the agent already defines this tool.
|
|
434
|
+
existing_names = {getattr(t, "name", None) for t in (request.tools or [])}
|
|
435
|
+
if tool.name in existing_names:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
_a2ui_tools_by_thread[_current_thread_id() or _DEFAULT_THREAD_KEY] = tool
|
|
439
|
+
return tool
|
|
440
|
+
|
|
441
|
+
# Inject frontend + A2UI tools and surface user state before model call
|
|
277
442
|
def wrap_model_call(
|
|
278
443
|
self,
|
|
279
444
|
request: ModelRequest,
|
|
@@ -282,13 +447,25 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
282
447
|
_extract_forwarded_headers_from_config()
|
|
283
448
|
_ensure_httpx_hook(request.model)
|
|
284
449
|
request = self._apply_state_note(request)
|
|
450
|
+
|
|
451
|
+
a2ui_tool = self._maybe_build_a2ui_tool(request)
|
|
285
452
|
frontend_tools = request.state.get("copilotkit", {}).get("actions", [])
|
|
453
|
+
if a2ui_tool is not None:
|
|
454
|
+
# Our generate_a2ui replaces the runtime's render tool — don't
|
|
455
|
+
# advertise both. Drop the render tool the A2UI middleware injected.
|
|
456
|
+
decision = self._a2ui_inject_decision(request.state or {})
|
|
457
|
+
drop = decision if isinstance(decision, str) else "render_a2ui"
|
|
458
|
+
frontend_tools = [
|
|
459
|
+
t
|
|
460
|
+
for t in frontend_tools
|
|
461
|
+
if ((t.get("function") or {}).get("name") or t.get("name")) != drop
|
|
462
|
+
]
|
|
286
463
|
|
|
287
|
-
if not frontend_tools:
|
|
464
|
+
if not frontend_tools and a2ui_tool is None:
|
|
288
465
|
return handler(request)
|
|
289
466
|
|
|
290
|
-
|
|
291
|
-
merged_tools = [*request.tools, *frontend_tools]
|
|
467
|
+
extra_tools = [a2ui_tool] if a2ui_tool is not None else []
|
|
468
|
+
merged_tools = [*request.tools, *extra_tools, *frontend_tools]
|
|
292
469
|
|
|
293
470
|
return handler(request.override(tools=merged_tools))
|
|
294
471
|
|
|
@@ -474,16 +651,62 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
474
651
|
self._fix_messages_for_bedrock(request.messages)
|
|
475
652
|
request = self._apply_state_note(request)
|
|
476
653
|
|
|
654
|
+
a2ui_tool = self._maybe_build_a2ui_tool(request)
|
|
477
655
|
frontend_tools = request.state.get("copilotkit", {}).get("actions", [])
|
|
656
|
+
if a2ui_tool is not None:
|
|
657
|
+
# Our generate_a2ui replaces the runtime's render tool — don't
|
|
658
|
+
# advertise both. Drop the render tool the A2UI middleware injected.
|
|
659
|
+
decision = self._a2ui_inject_decision(request.state or {})
|
|
660
|
+
drop = decision if isinstance(decision, str) else "render_a2ui"
|
|
661
|
+
frontend_tools = [
|
|
662
|
+
t
|
|
663
|
+
for t in frontend_tools
|
|
664
|
+
if ((t.get("function") or {}).get("name") or t.get("name")) != drop
|
|
665
|
+
]
|
|
478
666
|
|
|
479
|
-
if not frontend_tools:
|
|
667
|
+
if not frontend_tools and a2ui_tool is None:
|
|
480
668
|
return await handler(request)
|
|
481
669
|
|
|
482
|
-
|
|
483
|
-
merged_tools = [*request.tools, *frontend_tools]
|
|
670
|
+
extra_tools = [a2ui_tool] if a2ui_tool is not None else []
|
|
671
|
+
merged_tools = [*request.tools, *extra_tools, *frontend_tools]
|
|
484
672
|
|
|
485
673
|
return await handler(request.override(tools=merged_tools))
|
|
486
674
|
|
|
675
|
+
# ------------------------------------------------------------------
|
|
676
|
+
# Auto-A2UI tool execution
|
|
677
|
+
# ------------------------------------------------------------------
|
|
678
|
+
# The generate_a2ui tool is advertised dynamically in wrap_model_call and is
|
|
679
|
+
# NOT in create_agent's static tool registry, so the tool node cannot
|
|
680
|
+
# execute it on its own. These hooks supply the implementation (built with
|
|
681
|
+
# the inferred model) for that one tool; their presence also disables
|
|
682
|
+
# create_agent's "unknown tool" guard for dynamically-advertised tools.
|
|
683
|
+
|
|
684
|
+
def _resolve_a2ui_request(self, request: Any) -> Any:
|
|
685
|
+
"""Return a request overridden with the stashed A2UI tool when this
|
|
686
|
+
tool call targets it, else the original request unchanged."""
|
|
687
|
+
tool = _a2ui_tools_by_thread.get(_current_thread_id() or _DEFAULT_THREAD_KEY)
|
|
688
|
+
if (
|
|
689
|
+
tool is not None
|
|
690
|
+
and getattr(request, "tool", None) is None
|
|
691
|
+
and request.tool_call.get("name") == tool.name
|
|
692
|
+
):
|
|
693
|
+
return request.override(tool=tool)
|
|
694
|
+
return request
|
|
695
|
+
|
|
696
|
+
def wrap_tool_call(
|
|
697
|
+
self,
|
|
698
|
+
request: Any,
|
|
699
|
+
handler: Callable[[Any], Any],
|
|
700
|
+
) -> Any:
|
|
701
|
+
return handler(self._resolve_a2ui_request(request))
|
|
702
|
+
|
|
703
|
+
async def awrap_tool_call(
|
|
704
|
+
self,
|
|
705
|
+
request: Any,
|
|
706
|
+
handler: Callable[[Any], Awaitable[Any]],
|
|
707
|
+
) -> Any:
|
|
708
|
+
return await handler(self._resolve_a2ui_request(request))
|
|
709
|
+
|
|
487
710
|
# Inject app context before agent runs
|
|
488
711
|
def before_agent(
|
|
489
712
|
self,
|
|
@@ -678,6 +901,10 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
678
901
|
state: StateSchema,
|
|
679
902
|
runtime: Runtime[Any],
|
|
680
903
|
) -> dict[str, Any] | None:
|
|
904
|
+
# Drop the bridged A2UI tool for this run — all tool calls for the turn
|
|
905
|
+
# have executed by now; the next model call re-stashes if needed.
|
|
906
|
+
_a2ui_tools_by_thread.pop(_current_thread_id() or _DEFAULT_THREAD_KEY, None)
|
|
907
|
+
|
|
681
908
|
copilotkit_state = state.get("copilotkit", {})
|
|
682
909
|
intercepted_tool_calls = copilotkit_state.get("intercepted_tool_calls")
|
|
683
910
|
original_message_id = copilotkit_state.get("original_ai_message_id")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "copilotkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.94"
|
|
4
4
|
description = "CopilotKit python SDK"
|
|
5
5
|
authors = ["Markus Ecker <markus.ecker@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -31,7 +31,7 @@ python = ">=3.10,<3.15"
|
|
|
31
31
|
langgraph = { version = ">=0.3.25,<2" }
|
|
32
32
|
langchain = { version = ">=0.3.0" }
|
|
33
33
|
crewai = { version = ">=0.118.0", optional = true, python = ">=3.10,<3.14" }
|
|
34
|
-
ag-ui-langgraph = { version = ">=0.0.
|
|
34
|
+
ag-ui-langgraph = { version = ">=0.0.38", extras = ["fastapi"] }
|
|
35
35
|
ag-ui-protocol = ">=0.1.15"
|
|
36
36
|
fastapi = ">=0.115.0,<1.0.0"
|
|
37
37
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|