copilotkit 0.1.90__tar.gz → 0.1.92__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.90 → copilotkit-0.1.92}/PKG-INFO +2 -2
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/__init__.py +14 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/copilotkit_lg_middleware.py +97 -2
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/crewai/crewai_sdk.py +56 -15
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/exc.py +23 -4
- copilotkit-0.1.92/copilotkit/header_propagation.py +201 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/langgraph.py +52 -9
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/langgraph_agui_agent.py +86 -26
- {copilotkit-0.1.90 → copilotkit-0.1.92}/pyproject.toml +2 -2
- copilotkit-0.1.90/copilotkit/header_propagation.py +0 -59
- {copilotkit-0.1.90 → copilotkit-0.1.92}/README.md +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/a2ui.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/action.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/agent.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/crewai/__init__.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/crewai/copilotkit_integration.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/crewai/crewai_agent.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/html.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/integrations/__init__.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/integrations/fastapi.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/langchain.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/logging.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/parameter.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/protocol.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/py.typed +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/runloop.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/sdk.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/types.py +0 -0
- {copilotkit-0.1.90 → copilotkit-0.1.92}/copilotkit/utils.py +0 -0
|
@@ -7,6 +7,14 @@ from .sdk import (
|
|
|
7
7
|
CopilotKitSDKContext,
|
|
8
8
|
)
|
|
9
9
|
from .action import Action
|
|
10
|
+
from .exc import (
|
|
11
|
+
CopilotKitError,
|
|
12
|
+
CopilotKitMisuseError,
|
|
13
|
+
ActionNotFoundException,
|
|
14
|
+
AgentNotFoundException,
|
|
15
|
+
ActionExecutionException,
|
|
16
|
+
AgentExecutionException,
|
|
17
|
+
)
|
|
10
18
|
from .langgraph import CopilotKitState
|
|
11
19
|
from .parameter import Parameter
|
|
12
20
|
from .agent import Agent
|
|
@@ -32,6 +40,12 @@ __all__ = [
|
|
|
32
40
|
"Agent",
|
|
33
41
|
"CopilotKitContext",
|
|
34
42
|
"CopilotKitSDKContext",
|
|
43
|
+
"CopilotKitError",
|
|
44
|
+
"CopilotKitMisuseError",
|
|
45
|
+
"ActionNotFoundException",
|
|
46
|
+
"AgentNotFoundException",
|
|
47
|
+
"ActionExecutionException",
|
|
48
|
+
"AgentExecutionException",
|
|
35
49
|
"CrewAIAgent", # pyright: ignore[reportUnsupportedDunderAll] pylint: disable=undefined-all-variable
|
|
36
50
|
"LangGraphAGUIAgent",
|
|
37
51
|
"CopilotKitMiddleware",
|
|
@@ -16,7 +16,7 @@ Example:
|
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
18
|
import re
|
|
19
|
-
from typing import Any, Callable, Awaitable, ClassVar, Iterable,
|
|
19
|
+
from typing import Any, Callable, Awaitable, ClassVar, Iterable, Union
|
|
20
20
|
|
|
21
21
|
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
22
22
|
from langchain.agents.middleware import (
|
|
@@ -27,7 +27,7 @@ from langchain.agents.middleware import (
|
|
|
27
27
|
)
|
|
28
28
|
from langgraph.runtime import Runtime
|
|
29
29
|
|
|
30
|
-
from .header_propagation import install_httpx_hook
|
|
30
|
+
from .header_propagation import install_httpx_hook, set_forwarded_headers
|
|
31
31
|
from .langgraph import CopilotKitProperties
|
|
32
32
|
|
|
33
33
|
# Track which httpx clients already have the header-propagation hook installed
|
|
@@ -35,6 +35,99 @@ from .langgraph import CopilotKitProperties
|
|
|
35
35
|
_hooked_clients: set[int] = set()
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _extract_forwarded_headers_from_config() -> None:
|
|
39
|
+
"""Extract raw ``x-*`` headers from the current LangGraph RunnableConfig and
|
|
40
|
+
push them into the header-propagation ContextVar so the httpx hook can
|
|
41
|
+
forward them on outgoing LLM requests.
|
|
42
|
+
|
|
43
|
+
When an agent runs inside **langgraph-api** with
|
|
44
|
+
``LANGGRAPH_HTTP={"configurable_headers":{"include":["x-*"]}}``,
|
|
45
|
+
the server copies inbound HTTP ``x-*`` headers into
|
|
46
|
+
``config["configurable"]`` as individual keys (e.g.
|
|
47
|
+
``configurable["x-aimock-context"] = "value"``). This function reads those
|
|
48
|
+
keys and calls :func:`set_forwarded_headers` so they propagate to the
|
|
49
|
+
underlying LLM provider SDK via the httpx event hook.
|
|
50
|
+
|
|
51
|
+
Precedence: the wrapper dict ``copilotkit_forwarded_headers`` (if present)
|
|
52
|
+
takes priority over raw ``x-*`` keys. Raw keys are only used when the
|
|
53
|
+
wrapper dict is absent or does not contain a given header.
|
|
54
|
+
|
|
55
|
+
Safe to call outside a runnable context (e.g. in unit tests) — silently
|
|
56
|
+
returns without doing anything if ``get_config()`` raises.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
from langgraph.config import (
|
|
60
|
+
get_config,
|
|
61
|
+
) # local import to avoid hard dep at module level
|
|
62
|
+
|
|
63
|
+
config = get_config()
|
|
64
|
+
except ImportError:
|
|
65
|
+
return
|
|
66
|
+
except RuntimeError:
|
|
67
|
+
# No active runnable context — clear the ContextVar so stale headers
|
|
68
|
+
# from a prior request in the same async context do not leak through.
|
|
69
|
+
set_forwarded_headers({})
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
headers: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
# Sources to scan: config["context"] (LangGraph >=0.6.0) and
|
|
76
|
+
# config["configurable"] (all versions).
|
|
77
|
+
context = config.get("context") or {}
|
|
78
|
+
configurable = config.get("configurable") or {}
|
|
79
|
+
|
|
80
|
+
# 1) Wrapper-dict path (highest priority): these are headers that
|
|
81
|
+
# CopilotKit explicitly bundled under a known key. Process context
|
|
82
|
+
# first with first-write-wins so context takes precedence over
|
|
83
|
+
# configurable (LangGraph >=0.6.0 introduced context as the newer
|
|
84
|
+
# preferred mechanism).
|
|
85
|
+
for src in (context, configurable):
|
|
86
|
+
if not isinstance(src, dict):
|
|
87
|
+
continue
|
|
88
|
+
wrapper = src.get("copilotkit_forwarded_headers")
|
|
89
|
+
if isinstance(wrapper, dict):
|
|
90
|
+
for k, v in wrapper.items():
|
|
91
|
+
lk = k.lower() if isinstance(k, str) else k
|
|
92
|
+
if isinstance(k, str) and isinstance(v, str) and lk not in headers:
|
|
93
|
+
headers[lk] = v
|
|
94
|
+
|
|
95
|
+
# 2) Raw x-* keys directly on context and configurable. These appear
|
|
96
|
+
# when langgraph-api's configurable_headers mechanism forwards inbound
|
|
97
|
+
# HTTP headers as individual configurable entries.
|
|
98
|
+
for src in (context, configurable):
|
|
99
|
+
if not isinstance(src, dict):
|
|
100
|
+
continue
|
|
101
|
+
for k, v in src.items():
|
|
102
|
+
if (
|
|
103
|
+
isinstance(k, str)
|
|
104
|
+
and k.lower().startswith("x-")
|
|
105
|
+
and isinstance(v, str)
|
|
106
|
+
):
|
|
107
|
+
# Don't overwrite wrapper-dict values (wrapper > raw).
|
|
108
|
+
# Lowercase at insertion so precedence checks are
|
|
109
|
+
# deterministic regardless of source casing.
|
|
110
|
+
lk = k.lower()
|
|
111
|
+
if lk not in headers:
|
|
112
|
+
headers[lk] = v
|
|
113
|
+
|
|
114
|
+
# Always set the ContextVar — even with an empty dict — so stale
|
|
115
|
+
# headers from previous calls in the same async context do not leak
|
|
116
|
+
# into this one.
|
|
117
|
+
set_forwarded_headers(headers)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
# Header forwarding is best-effort. Never block the LLM call.
|
|
120
|
+
# Clear the ContextVar so stale headers from a prior request do not
|
|
121
|
+
# leak through on failure.
|
|
122
|
+
set_forwarded_headers({})
|
|
123
|
+
import logging
|
|
124
|
+
|
|
125
|
+
logging.getLogger(__name__).debug(
|
|
126
|
+
"Header forwarding extraction failed; continuing without forwarded headers: %s",
|
|
127
|
+
e,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
38
131
|
def _ensure_httpx_hook(model: Any) -> None:
|
|
39
132
|
"""Install the header-propagation httpx hook on a LangChain chat model's
|
|
40
133
|
underlying HTTP client(s), if present. No-op for models that don't expose
|
|
@@ -172,6 +265,7 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
172
265
|
request: ModelRequest,
|
|
173
266
|
handler: Callable[[ModelRequest], ModelResponse],
|
|
174
267
|
) -> ModelResponse:
|
|
268
|
+
_extract_forwarded_headers_from_config()
|
|
175
269
|
_ensure_httpx_hook(request.model)
|
|
176
270
|
request = self._apply_state_note(request)
|
|
177
271
|
frontend_tools = request.state.get("copilotkit", {}).get("actions", [])
|
|
@@ -361,6 +455,7 @@ class CopilotKitMiddleware(AgentMiddleware[StateSchema, Any]):
|
|
|
361
455
|
request: ModelRequest,
|
|
362
456
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
363
457
|
) -> ModelResponse:
|
|
458
|
+
_extract_forwarded_headers_from_config()
|
|
364
459
|
_ensure_httpx_hook(request.model)
|
|
365
460
|
self._fix_messages_for_bedrock(request.messages)
|
|
366
461
|
request = self._apply_state_note(request)
|
|
@@ -5,7 +5,8 @@ CrewAI integration for CopilotKit
|
|
|
5
5
|
import uuid
|
|
6
6
|
import json
|
|
7
7
|
import asyncio
|
|
8
|
-
from typing_extensions import Any, Dict, List, Literal
|
|
8
|
+
from typing_extensions import Any, Dict, List, Literal, Optional
|
|
9
|
+
from copilotkit.exc import CopilotKitMisuseError
|
|
9
10
|
from pydantic import BaseModel, Field
|
|
10
11
|
from litellm.types.utils import (
|
|
11
12
|
ModelResponse,
|
|
@@ -226,14 +227,19 @@ async def copilotkit_emit_message(message: str) -> str:
|
|
|
226
227
|
return message_id
|
|
227
228
|
|
|
228
229
|
|
|
229
|
-
async def copilotkit_emit_tool_call(
|
|
230
|
+
async def copilotkit_emit_tool_call(
|
|
231
|
+
*, name: str, args: Dict[str, Any], tool_call_id: Optional[str] = None
|
|
232
|
+
) -> str:
|
|
230
233
|
"""
|
|
231
234
|
Manually emits a tool call to CopilotKit.
|
|
232
235
|
|
|
233
236
|
```python
|
|
234
237
|
from copilotkit.crewai import copilotkit_emit_tool_call
|
|
235
238
|
|
|
236
|
-
await copilotkit_emit_tool_call(name="SearchTool", args={"steps": 10})
|
|
239
|
+
auto_id = await copilotkit_emit_tool_call(name="SearchTool", args={"steps": 10})
|
|
240
|
+
|
|
241
|
+
# With a custom ID for correlation/idempotency:
|
|
242
|
+
custom_id = await copilotkit_emit_tool_call(name="SearchTool", args={"steps": 10}, tool_call_id="my-custom-id")
|
|
237
243
|
```
|
|
238
244
|
|
|
239
245
|
Parameters
|
|
@@ -242,22 +248,57 @@ async def copilotkit_emit_tool_call(*, name: str, args: Dict[str, Any]) -> str:
|
|
|
242
248
|
The name of the tool to emit.
|
|
243
249
|
args : Dict[str, Any]
|
|
244
250
|
The arguments to emit.
|
|
251
|
+
tool_call_id : Optional[str]
|
|
252
|
+
Optional tool call ID. If not provided, a random UUID is generated.
|
|
253
|
+
When provided, this ID is used as both the toolCallId and
|
|
254
|
+
parentMessageId in AG-UI protocol events.
|
|
255
|
+
The caller is responsible for ensuring uniqueness.
|
|
245
256
|
|
|
246
257
|
Returns
|
|
247
258
|
-------
|
|
248
|
-
|
|
249
|
-
|
|
259
|
+
str
|
|
260
|
+
The tool call ID used for the emitted tool call.
|
|
250
261
|
"""
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
if not isinstance(name, str) or not name.strip():
|
|
263
|
+
raise CopilotKitMisuseError(
|
|
264
|
+
"Tool name must be a non-empty string for copilotkit_emit_tool_call"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if tool_call_id is not None:
|
|
268
|
+
if not isinstance(tool_call_id, str) or not tool_call_id.strip():
|
|
269
|
+
raise CopilotKitMisuseError(
|
|
270
|
+
"Tool call id must be a non-empty string when provided for copilotkit_emit_tool_call"
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
args_json = json.dumps(args)
|
|
274
|
+
except (TypeError, ValueError) as e:
|
|
275
|
+
raise CopilotKitMisuseError(
|
|
276
|
+
f"Tool arguments for '{name}' are not JSON-serializable: {e}"
|
|
277
|
+
) from e
|
|
278
|
+
|
|
279
|
+
message_id = tool_call_id if tool_call_id is not None else str(uuid.uuid4())
|
|
280
|
+
try:
|
|
281
|
+
await queue_put(
|
|
282
|
+
action_execution_start(
|
|
283
|
+
action_execution_id=message_id,
|
|
284
|
+
action_name=name,
|
|
285
|
+
parent_message_id=message_id,
|
|
286
|
+
),
|
|
287
|
+
action_execution_args(action_execution_id=message_id, args=args_json),
|
|
288
|
+
action_execution_end(action_execution_id=message_id),
|
|
289
|
+
)
|
|
290
|
+
except Exception:
|
|
291
|
+
try:
|
|
292
|
+
await queue_put(
|
|
293
|
+
action_execution_end(action_execution_id=message_id),
|
|
294
|
+
)
|
|
295
|
+
except Exception:
|
|
296
|
+
logger.error(
|
|
297
|
+
"Failed to emit compensating action_execution_end for %s",
|
|
298
|
+
message_id,
|
|
299
|
+
exc_info=True,
|
|
300
|
+
)
|
|
301
|
+
raise
|
|
261
302
|
|
|
262
303
|
return message_id
|
|
263
304
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"""Exceptions for CopilotKit."""
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class CopilotKitError(Exception):
|
|
5
|
+
"""Base exception for all CopilotKit errors.
|
|
6
|
+
|
|
7
|
+
Catch this to handle any CopilotKit-specific exception.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActionNotFoundException(CopilotKitError):
|
|
5
14
|
"""Exception raised when an action or agent is not found."""
|
|
6
15
|
|
|
7
16
|
def __init__(self, name: str):
|
|
@@ -9,7 +18,7 @@ class ActionNotFoundException(Exception):
|
|
|
9
18
|
super().__init__(f"Action '{name}' not found.")
|
|
10
19
|
|
|
11
20
|
|
|
12
|
-
class AgentNotFoundException(
|
|
21
|
+
class AgentNotFoundException(CopilotKitError):
|
|
13
22
|
"""Exception raised when an agent is not found."""
|
|
14
23
|
|
|
15
24
|
def __init__(self, name: str):
|
|
@@ -17,7 +26,7 @@ class AgentNotFoundException(Exception):
|
|
|
17
26
|
super().__init__(f"Agent '{name}' not found.")
|
|
18
27
|
|
|
19
28
|
|
|
20
|
-
class ActionExecutionException(
|
|
29
|
+
class ActionExecutionException(CopilotKitError):
|
|
21
30
|
"""Exception raised when an action fails to execute."""
|
|
22
31
|
|
|
23
32
|
def __init__(self, name: str, error: Exception):
|
|
@@ -26,10 +35,20 @@ class ActionExecutionException(Exception):
|
|
|
26
35
|
super().__init__(f"Action '{name}' failed to execute: {error}")
|
|
27
36
|
|
|
28
37
|
|
|
29
|
-
class AgentExecutionException(
|
|
38
|
+
class AgentExecutionException(CopilotKitError):
|
|
30
39
|
"""Exception raised when an agent fails to execute."""
|
|
31
40
|
|
|
32
41
|
def __init__(self, name: str, error: Exception):
|
|
33
42
|
self.name = name
|
|
34
43
|
self.error = error
|
|
35
44
|
super().__init__(f"Agent '{name}' failed to execute: {error}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CopilotKitMisuseError(CopilotKitError, ValueError):
|
|
48
|
+
"""Exception raised when CopilotKit detects incorrect usage of its APIs.
|
|
49
|
+
|
|
50
|
+
Inherits from both CopilotKitError (for ``except CopilotKitError``) and
|
|
51
|
+
ValueError (for backward compatibility with ``except ValueError`` handlers).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
pass
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Forward CopilotKit request-context headers onto outbound LLM/provider HTTP calls
|
|
2
|
+
so downstream services (e.g. the aimock test server, proxies, request routing /
|
|
3
|
+
fixture-matching infrastructure) can correlate the outbound provider call with the
|
|
4
|
+
original inbound request.
|
|
5
|
+
|
|
6
|
+
What this module does
|
|
7
|
+
---------------------
|
|
8
|
+
On each inbound request the application stores a small set of ``x-*`` prefixed
|
|
9
|
+
headers (for example ``x-aimock-context``, ``x-aimock-session``, ``x-request-id``,
|
|
10
|
+
``x-trace-id``) on a per-request ``contextvars.ContextVar``. When the application
|
|
11
|
+
later makes an outbound HTTP call to an LLM provider (OpenAI, Anthropic, or any
|
|
12
|
+
client that wraps ``httpx``), an httpx request event hook reads that ContextVar
|
|
13
|
+
and copies those same headers onto the outbound request so downstream services
|
|
14
|
+
can correlate the two.
|
|
15
|
+
|
|
16
|
+
This is plain header propagation, not data collection. Scope and limits:
|
|
17
|
+
|
|
18
|
+
* Only headers the application itself set on the request context via
|
|
19
|
+
``set_forwarded_headers`` are forwarded. The module never reads request
|
|
20
|
+
bodies, cookies, user data, credentials, or anything off the inbound
|
|
21
|
+
request beyond the headers explicitly handed to it.
|
|
22
|
+
* Only ``x-*`` prefixed headers pass the filter; ``authorization``,
|
|
23
|
+
``content-type``, and any other non ``x-*`` headers are dropped.
|
|
24
|
+
* Nothing is collected, persisted, logged, or sent anywhere by this module
|
|
25
|
+
itself — it only attaches headers to an HTTP request that the caller was
|
|
26
|
+
already going to make. There is no telemetry, no out-of-band channel, and
|
|
27
|
+
no end-user data flow.
|
|
28
|
+
|
|
29
|
+
Mechanics
|
|
30
|
+
---------
|
|
31
|
+
``install_httpx_hook`` does two small things:
|
|
32
|
+
|
|
33
|
+
1. It walks the ``._client`` chain on the given object (modern provider SDKs
|
|
34
|
+
wrap their httpx client behind several layers of ``._client``) to find the
|
|
35
|
+
first object that exposes an httpx-style ``event_hooks`` mapping.
|
|
36
|
+
2. It attaches a request event hook to that mapping. The hook flavor matches
|
|
37
|
+
the client: an async coroutine hook for ``httpx.AsyncClient`` (httpx awaits
|
|
38
|
+
request hooks on async clients), and a plain sync hook for ``httpx.Client``.
|
|
39
|
+
Installation is idempotent via a marker attribute on the installed callable.
|
|
40
|
+
|
|
41
|
+
This mirrors the CopilotKit runtime's ``extractForwardableHeaders()`` behavior
|
|
42
|
+
on the Node side so the Python SDK forwards the same set of context headers.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import contextvars
|
|
46
|
+
import warnings
|
|
47
|
+
from typing import Any, Dict, Optional
|
|
48
|
+
|
|
49
|
+
# Per-request storage for the set of headers the application has asked to forward
|
|
50
|
+
# onto outbound LLM/provider calls (populated by ``set_forwarded_headers``).
|
|
51
|
+
_forwarded_headers: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
|
|
52
|
+
"copilotkit_forwarded_headers"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Marker used to identify hooks we have already installed, so install_httpx_hook
|
|
56
|
+
# is idempotent across repeated calls on the same client.
|
|
57
|
+
_HOOK_MARKER = "_copilotkit_forwarded_header_hook"
|
|
58
|
+
|
|
59
|
+
# Bound on how deep we'll walk a ``._client`` chain looking for event_hooks.
|
|
60
|
+
# The modern OpenAI SDK shape is:
|
|
61
|
+
# ChatOpenAI.client -> Completions/AsyncCompletions resource
|
|
62
|
+
# -> ._client = openai.OpenAI / AsyncOpenAI (no event_hooks)
|
|
63
|
+
# -> ._client._client = httpx wrapper (HAS event_hooks)
|
|
64
|
+
# 5 hops is plenty of headroom for similar SDKs without risking pathological loops.
|
|
65
|
+
_MAX_CHAIN_DEPTH = 5
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_forwarded_headers(headers: Dict[str, str]) -> None:
|
|
69
|
+
"""Record the set of headers to forward onto outbound LLM/provider calls
|
|
70
|
+
made later in this request context.
|
|
71
|
+
|
|
72
|
+
Only ``x-*`` prefixed headers are kept; everything else is dropped.
|
|
73
|
+
"""
|
|
74
|
+
filtered = {k.lower(): v for k, v in headers.items() if k.lower().startswith("x-")}
|
|
75
|
+
_forwarded_headers.set(filtered)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_forwarded_headers() -> Dict[str, str]:
|
|
79
|
+
"""Return the headers the application has asked to forward onto outbound
|
|
80
|
+
LLM/provider calls in the current request context."""
|
|
81
|
+
return _forwarded_headers.get({})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _find_event_hooks_target(client: Any) -> Optional[Any]:
|
|
85
|
+
"""Walk the ``._client`` chain looking for the first object that exposes
|
|
86
|
+
an httpx-style ``event_hooks`` mapping.
|
|
87
|
+
|
|
88
|
+
Returns the target object, or ``None`` if no such object is found within
|
|
89
|
+
``_MAX_CHAIN_DEPTH`` hops.
|
|
90
|
+
"""
|
|
91
|
+
current = client
|
|
92
|
+
for _ in range(_MAX_CHAIN_DEPTH + 1):
|
|
93
|
+
if current is None:
|
|
94
|
+
return None
|
|
95
|
+
if hasattr(current, "event_hooks"):
|
|
96
|
+
return current
|
|
97
|
+
nxt = getattr(current, "_client", None)
|
|
98
|
+
if nxt is current or nxt is None:
|
|
99
|
+
return None
|
|
100
|
+
current = nxt
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def install_httpx_hook(client: Any) -> None:
|
|
105
|
+
"""Attach a request event hook to ``client``'s underlying httpx client so
|
|
106
|
+
that headers recorded via ``set_forwarded_headers`` are copied onto
|
|
107
|
+
outbound requests.
|
|
108
|
+
|
|
109
|
+
Works with OpenAI and Anthropic Python SDKs (both wrap httpx internally,
|
|
110
|
+
sometimes via several layers of ``._client`` indirection), as well as raw
|
|
111
|
+
``httpx.Client`` / ``httpx.AsyncClient`` instances.
|
|
112
|
+
|
|
113
|
+
For ``httpx.AsyncClient`` an async hook is attached (httpx awaits request
|
|
114
|
+
hooks on async clients); for sync clients a sync hook is attached.
|
|
115
|
+
|
|
116
|
+
Idempotent: a marker attribute on the installed callable prevents double
|
|
117
|
+
installation on the same target.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
client : object
|
|
122
|
+
An OpenAI/Anthropic client instance, or a raw httpx.Client/AsyncClient.
|
|
123
|
+
"""
|
|
124
|
+
target = _find_event_hooks_target(client)
|
|
125
|
+
|
|
126
|
+
if target is None:
|
|
127
|
+
warnings.warn(
|
|
128
|
+
f"install_httpx_hook: client of type {type(client).__name__} has no "
|
|
129
|
+
"recognized event_hooks attribute; x-* headers will not be forwarded",
|
|
130
|
+
stacklevel=2,
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
request_hooks = target.event_hooks.get("request", [])
|
|
135
|
+
|
|
136
|
+
# Idempotency: don't double-install on the same target.
|
|
137
|
+
for existing in request_hooks:
|
|
138
|
+
if getattr(existing, _HOOK_MARKER, False):
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Choose sync vs async hook flavor based on the target class.
|
|
142
|
+
# httpx.AsyncClient awaits request hooks; a sync hook returning None would
|
|
143
|
+
# raise "TypeError: object NoneType can't be used in 'await' expression",
|
|
144
|
+
# which surfaces as APIConnectionError to the caller.
|
|
145
|
+
is_async = _is_async_httpx_target(target)
|
|
146
|
+
|
|
147
|
+
if is_async:
|
|
148
|
+
|
|
149
|
+
async def _inject_headers_async(request):
|
|
150
|
+
headers = get_forwarded_headers()
|
|
151
|
+
for key, value in headers.items():
|
|
152
|
+
request.headers[key] = value
|
|
153
|
+
|
|
154
|
+
setattr(_inject_headers_async, _HOOK_MARKER, True)
|
|
155
|
+
request_hooks.append(_inject_headers_async)
|
|
156
|
+
else:
|
|
157
|
+
|
|
158
|
+
def _inject_headers(request):
|
|
159
|
+
headers = get_forwarded_headers()
|
|
160
|
+
for key, value in headers.items():
|
|
161
|
+
request.headers[key] = value
|
|
162
|
+
|
|
163
|
+
setattr(_inject_headers, _HOOK_MARKER, True)
|
|
164
|
+
request_hooks.append(_inject_headers)
|
|
165
|
+
|
|
166
|
+
# In case ``event_hooks`` returned a fresh list (defensive), make sure the
|
|
167
|
+
# mutation is reflected on the target.
|
|
168
|
+
target.event_hooks["request"] = request_hooks
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_async_httpx_target(target: Any) -> bool:
|
|
172
|
+
"""Best-effort detection: is this object an httpx async client?
|
|
173
|
+
|
|
174
|
+
Tries ``isinstance`` against the real ``httpx.AsyncClient`` / ``httpx.Client``
|
|
175
|
+
first (the authoritative answer for real clients). If httpx is not
|
|
176
|
+
importable, or the target is neither of those (e.g. a wrapped or
|
|
177
|
+
duck-typed client used in tests), falls back to an EXACT MRO class-name
|
|
178
|
+
match against ``"AsyncClient"``. Avoids a broad ``startswith("Async")``
|
|
179
|
+
check, which would misclassify a sync client whose MRO happens to
|
|
180
|
+
include an ``Async*``-named base (e.g. ``AsyncContextManager``) as
|
|
181
|
+
async — attaching an async hook that httpx calls synchronously would
|
|
182
|
+
leave the coroutine unawaited and the forwarded headers would not be
|
|
183
|
+
attached to the outbound request.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
import httpx # local import keeps httpx an optional concern at import time
|
|
187
|
+
|
|
188
|
+
if isinstance(target, httpx.AsyncClient):
|
|
189
|
+
return True
|
|
190
|
+
if isinstance(target, httpx.Client):
|
|
191
|
+
return False
|
|
192
|
+
except (
|
|
193
|
+
ImportError
|
|
194
|
+
): # pragma: no cover - httpx should always be importable in practice
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
# Fall back to exact class-name match for wrapped/duck-typed clients.
|
|
198
|
+
for cls in type(target).__mro__:
|
|
199
|
+
if cls.__name__ == "AsyncClient":
|
|
200
|
+
return True
|
|
201
|
+
return False
|
|
@@ -23,6 +23,7 @@ from langchain_core.callbacks.manager import adispatch_custom_event
|
|
|
23
23
|
from langgraph.types import interrupt
|
|
24
24
|
|
|
25
25
|
from .types import Message, IntermediateStateConfig
|
|
26
|
+
from .exc import CopilotKitMisuseError
|
|
26
27
|
from .logging import get_logger
|
|
27
28
|
|
|
28
29
|
logger = get_logger(__name__)
|
|
@@ -368,21 +369,28 @@ async def copilotkit_emit_message(config: RunnableConfig, message: str):
|
|
|
368
369
|
{"message": message, "message_id": str(uuid.uuid4()), "role": "assistant"},
|
|
369
370
|
config=config,
|
|
370
371
|
)
|
|
371
|
-
await asyncio.sleep(0.02)
|
|
372
|
+
await asyncio.shield(asyncio.sleep(0.02))
|
|
372
373
|
|
|
373
374
|
return True
|
|
374
375
|
|
|
375
376
|
|
|
376
377
|
async def copilotkit_emit_tool_call(
|
|
377
|
-
config: RunnableConfig,
|
|
378
|
-
|
|
378
|
+
config: RunnableConfig,
|
|
379
|
+
*,
|
|
380
|
+
name: str,
|
|
381
|
+
args: Dict[str, Any],
|
|
382
|
+
tool_call_id: Optional[str] = None,
|
|
383
|
+
) -> str:
|
|
379
384
|
"""
|
|
380
385
|
Manually emits a tool call to CopilotKit.
|
|
381
386
|
|
|
382
387
|
```python
|
|
383
388
|
from copilotkit.langgraph import copilotkit_emit_tool_call
|
|
384
389
|
|
|
385
|
-
await copilotkit_emit_tool_call(config, name="SearchTool", args={"steps": 10})
|
|
390
|
+
auto_id = await copilotkit_emit_tool_call(config, name="SearchTool", args={"steps": 10})
|
|
391
|
+
|
|
392
|
+
# With a custom ID for correlation/idempotency:
|
|
393
|
+
custom_id = await copilotkit_emit_tool_call(config, name="SearchTool", args={"steps": 10}, tool_call_id="my-custom-id")
|
|
386
394
|
```
|
|
387
395
|
|
|
388
396
|
Parameters
|
|
@@ -393,21 +401,56 @@ async def copilotkit_emit_tool_call(
|
|
|
393
401
|
The name of the tool to emit.
|
|
394
402
|
args : Dict[str, Any]
|
|
395
403
|
The arguments to emit.
|
|
404
|
+
tool_call_id : Optional[str]
|
|
405
|
+
Optional tool call ID. If not provided, a random UUID is generated.
|
|
406
|
+
When provided, this ID is used as the toolCallId and parentMessageId
|
|
407
|
+
in AG-UI protocol events. The caller is responsible for ensuring uniqueness.
|
|
396
408
|
|
|
397
409
|
Returns
|
|
398
410
|
-------
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
str
|
|
412
|
+
The tool call ID used for the emitted tool call.
|
|
401
413
|
"""
|
|
414
|
+
if not isinstance(name, str) or not name.strip():
|
|
415
|
+
raise CopilotKitMisuseError(
|
|
416
|
+
"Tool name must be a non-empty string for copilotkit_emit_tool_call"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if tool_call_id is not None:
|
|
420
|
+
if not isinstance(tool_call_id, str) or not tool_call_id.strip():
|
|
421
|
+
raise CopilotKitMisuseError(
|
|
422
|
+
"Tool call id must be a non-empty string when provided for copilotkit_emit_tool_call"
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
tool_call_id = str(uuid.uuid4())
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
json.dumps(args)
|
|
429
|
+
except (TypeError, ValueError) as e:
|
|
430
|
+
raise CopilotKitMisuseError(
|
|
431
|
+
f"Tool arguments for '{name}' are not JSON-serializable: {e}"
|
|
432
|
+
) from e
|
|
402
433
|
|
|
403
434
|
await adispatch_custom_event(
|
|
404
435
|
"copilotkit_manually_emit_tool_call",
|
|
405
|
-
{"name": name, "args": args, "id":
|
|
436
|
+
{"name": name, "args": args, "id": tool_call_id},
|
|
406
437
|
config=config,
|
|
407
438
|
)
|
|
408
|
-
|
|
439
|
+
# LangGraph's adispatch_custom_event is async but does not guarantee the event
|
|
440
|
+
# has been flushed to the SSE stream before it returns. Without this sleep,
|
|
441
|
+
# a subsequent emit can interleave and corrupt event ordering on the client.
|
|
442
|
+
# Shielded so that task cancellation doesn't prevent us from returning the ID.
|
|
443
|
+
try:
|
|
444
|
+
await asyncio.shield(asyncio.sleep(0.02))
|
|
445
|
+
except asyncio.CancelledError:
|
|
446
|
+
logger.warning(
|
|
447
|
+
"copilotkit_emit_tool_call cancelled during post-dispatch flush for "
|
|
448
|
+
"tool_call_id=%s; event was already dispatched",
|
|
449
|
+
tool_call_id,
|
|
450
|
+
)
|
|
451
|
+
raise
|
|
409
452
|
|
|
410
|
-
return
|
|
453
|
+
return tool_call_id
|
|
411
454
|
|
|
412
455
|
|
|
413
456
|
def copilotkit_interrupt(
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
from typing import Dict, Any, List, Optional, Union, AsyncGenerator
|
|
3
4
|
from enum import Enum
|
|
5
|
+
from .exc import CopilotKitMisuseError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
4
8
|
from ag_ui_langgraph import LangGraphAgent
|
|
5
9
|
from ag_ui.core import (
|
|
6
10
|
EventType,
|
|
@@ -104,33 +108,86 @@ class LangGraphAGUIAgent(LangGraphAgent):
|
|
|
104
108
|
return super()._dispatch_event(event)
|
|
105
109
|
|
|
106
110
|
if custom_event.name == CustomEventNames.ManuallyEmitToolCall.value:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type
|
|
111
|
-
tool_call_id=custom_event.value["id"],
|
|
112
|
-
tool_call_name=custom_event.value["name"],
|
|
113
|
-
parent_message_id=custom_event.value["id"],
|
|
114
|
-
raw_event=event,
|
|
111
|
+
value = custom_event.value
|
|
112
|
+
if not isinstance(value, dict):
|
|
113
|
+
raise CopilotKitMisuseError(
|
|
114
|
+
f"ManuallyEmitToolCall event 'value' must be a dict, got {type(value).__name__}"
|
|
115
115
|
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
raw_event=event,
|
|
116
|
+
|
|
117
|
+
tool_call_id = value.get("id")
|
|
118
|
+
tool_call_name = value.get("name")
|
|
119
|
+
tool_call_args = value.get("args")
|
|
120
|
+
|
|
121
|
+
if not isinstance(tool_call_id, str) or not tool_call_id.strip():
|
|
122
|
+
raise CopilotKitMisuseError(
|
|
123
|
+
f"ManuallyEmitToolCall event missing valid 'id': got {type(tool_call_id).__name__}"
|
|
125
124
|
)
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
type=EventType.TOOL_CALL_END,
|
|
130
|
-
tool_call_id=custom_event.value["id"],
|
|
131
|
-
raw_event=event,
|
|
125
|
+
if not isinstance(tool_call_name, str) or not tool_call_name.strip():
|
|
126
|
+
raise CopilotKitMisuseError(
|
|
127
|
+
f"ManuallyEmitToolCall event missing valid 'name': got {type(tool_call_name).__name__}"
|
|
132
128
|
)
|
|
133
|
-
|
|
129
|
+
if tool_call_args is None:
|
|
130
|
+
raise CopilotKitMisuseError(
|
|
131
|
+
f"ManuallyEmitToolCall event missing 'args' for tool_call_id={tool_call_id}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
delta = (
|
|
136
|
+
tool_call_args
|
|
137
|
+
if isinstance(tool_call_args, str)
|
|
138
|
+
else json.dumps(tool_call_args)
|
|
139
|
+
)
|
|
140
|
+
except (TypeError, ValueError) as e:
|
|
141
|
+
raise CopilotKitMisuseError(
|
|
142
|
+
f"ManuallyEmitToolCall 'args' is not JSON-serializable for tool_call_id={tool_call_id}: {e}"
|
|
143
|
+
) from e
|
|
144
|
+
|
|
145
|
+
dispatched_start = False
|
|
146
|
+
end_dispatched = False
|
|
147
|
+
try:
|
|
148
|
+
super()._dispatch_event(
|
|
149
|
+
ToolCallStartEvent(
|
|
150
|
+
type=EventType.TOOL_CALL_START,
|
|
151
|
+
tool_call_id=tool_call_id,
|
|
152
|
+
tool_call_name=tool_call_name,
|
|
153
|
+
parent_message_id=tool_call_id,
|
|
154
|
+
raw_event=event,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
dispatched_start = True
|
|
158
|
+
super()._dispatch_event(
|
|
159
|
+
ToolCallArgsEvent(
|
|
160
|
+
type=EventType.TOOL_CALL_ARGS,
|
|
161
|
+
tool_call_id=tool_call_id,
|
|
162
|
+
delta=delta,
|
|
163
|
+
raw_event=event,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
super()._dispatch_event(
|
|
167
|
+
ToolCallEndEvent(
|
|
168
|
+
type=EventType.TOOL_CALL_END,
|
|
169
|
+
tool_call_id=tool_call_id,
|
|
170
|
+
raw_event=event,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
end_dispatched = True
|
|
174
|
+
except Exception:
|
|
175
|
+
if dispatched_start and not end_dispatched:
|
|
176
|
+
try:
|
|
177
|
+
super()._dispatch_event(
|
|
178
|
+
ToolCallEndEvent(
|
|
179
|
+
type=EventType.TOOL_CALL_END,
|
|
180
|
+
tool_call_id=tool_call_id,
|
|
181
|
+
raw_event=event,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
except Exception:
|
|
185
|
+
logger.error(
|
|
186
|
+
"Failed to emit compensating TOOL_CALL_END for %s",
|
|
187
|
+
tool_call_id,
|
|
188
|
+
exc_info=True,
|
|
189
|
+
)
|
|
190
|
+
raise
|
|
134
191
|
return super()._dispatch_event(event)
|
|
135
192
|
|
|
136
193
|
if custom_event.name == CustomEventNames.ManuallyEmitState.value:
|
|
@@ -234,5 +291,8 @@ class LangGraphAGUIAgent(LangGraphAgent):
|
|
|
234
291
|
|
|
235
292
|
def dict_repr(self):
|
|
236
293
|
"""Return dictionary representation of the agent"""
|
|
237
|
-
|
|
238
|
-
|
|
294
|
+
return {
|
|
295
|
+
"name": self.name,
|
|
296
|
+
"description": self.description or "",
|
|
297
|
+
"type": "langgraph_agui",
|
|
298
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "copilotkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.92"
|
|
4
4
|
description = "CopilotKit python SDK"
|
|
5
5
|
authors = ["Markus Ecker <markus.ecker@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -48,4 +48,4 @@ requires = ["poetry-core"]
|
|
|
48
48
|
build-backend = "poetry.core.masonry.api"
|
|
49
49
|
|
|
50
50
|
[dependency-groups]
|
|
51
|
-
dev = ["pytest (>=9.0.2,<10.0.0)"]
|
|
51
|
+
dev = ["pytest (>=9.0.2,<10.0.0)", "pytest-asyncio (>=1.0.0,<2.0.0)"]
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""Header propagation for forwarding x-* prefixed headers to outgoing LLM calls.
|
|
2
|
-
|
|
3
|
-
Uses Python contextvars for per-request ambient state in async FastAPI handlers.
|
|
4
|
-
An httpx event hook reads the ContextVar and injects headers on outgoing requests.
|
|
5
|
-
Matches the CopilotKit runtime's extractForwardableHeaders() behavior.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import contextvars
|
|
9
|
-
import warnings
|
|
10
|
-
from typing import Dict
|
|
11
|
-
|
|
12
|
-
# Ambient per-request state for headers to forward to LLM calls
|
|
13
|
-
_forwarded_headers: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
|
|
14
|
-
"copilotkit_forwarded_headers"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def set_forwarded_headers(headers: Dict[str, str]) -> None:
|
|
19
|
-
"""Store headers to forward to outgoing LLM calls.
|
|
20
|
-
Filters to x-* prefixed headers only."""
|
|
21
|
-
filtered = {k.lower(): v for k, v in headers.items() if k.lower().startswith("x-")}
|
|
22
|
-
_forwarded_headers.set(filtered)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_forwarded_headers() -> Dict[str, str]:
|
|
26
|
-
"""Get headers that should be forwarded to outgoing LLM calls."""
|
|
27
|
-
return _forwarded_headers.get({})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def install_httpx_hook(client) -> None:
|
|
31
|
-
"""Append an event hook to an httpx client that injects forwarded headers.
|
|
32
|
-
|
|
33
|
-
Works with OpenAI and Anthropic Python SDKs (both use httpx internally).
|
|
34
|
-
No-op when no headers are set (demo traffic).
|
|
35
|
-
|
|
36
|
-
Parameters
|
|
37
|
-
----------
|
|
38
|
-
client : object
|
|
39
|
-
An OpenAI/Anthropic client instance, or a raw httpx.Client/AsyncClient.
|
|
40
|
-
For SDK clients the underlying transport lives at ``client._client``.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def _inject_headers(request):
|
|
44
|
-
headers = get_forwarded_headers()
|
|
45
|
-
for key, value in headers.items():
|
|
46
|
-
request.headers[key] = value
|
|
47
|
-
|
|
48
|
-
# OpenAI / Anthropic SDKs wrap an httpx client at client._client
|
|
49
|
-
if hasattr(client, "_client") and hasattr(client._client, "event_hooks"):
|
|
50
|
-
client._client.event_hooks["request"].append(_inject_headers)
|
|
51
|
-
elif hasattr(client, "event_hooks"):
|
|
52
|
-
# Raw httpx.Client / httpx.AsyncClient
|
|
53
|
-
client.event_hooks["request"].append(_inject_headers)
|
|
54
|
-
else:
|
|
55
|
-
warnings.warn(
|
|
56
|
-
f"install_httpx_hook: client of type {type(client).__name__} has no "
|
|
57
|
-
"recognized event_hooks attribute; x-* headers will not be forwarded",
|
|
58
|
-
stacklevel=2,
|
|
59
|
-
)
|
|
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
|