axonpush 0.0.1__py3-none-any.whl
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.
- axonpush/__init__.py +48 -0
- axonpush/_auth.py +24 -0
- axonpush/_http.py +121 -0
- axonpush/_tracing.py +52 -0
- axonpush/_version.py +1 -0
- axonpush/client.py +102 -0
- axonpush/exceptions.py +37 -0
- axonpush/integrations/__init__.py +7 -0
- axonpush/integrations/anthropic.py +184 -0
- axonpush/integrations/crewai.py +132 -0
- axonpush/integrations/langchain.py +250 -0
- axonpush/integrations/openai_agents.py +133 -0
- axonpush/models/__init__.py +26 -0
- axonpush/models/apps.py +19 -0
- axonpush/models/channels.py +20 -0
- axonpush/models/common.py +8 -0
- axonpush/models/events.py +49 -0
- axonpush/models/traces.py +33 -0
- axonpush/models/webhooks.py +51 -0
- axonpush/py.typed +0 -0
- axonpush/realtime/__init__.py +3 -0
- axonpush/realtime/sse.py +86 -0
- axonpush/realtime/websocket.py +210 -0
- axonpush/resources/__init__.py +18 -0
- axonpush/resources/apps.py +69 -0
- axonpush/resources/channels.py +107 -0
- axonpush/resources/events.py +109 -0
- axonpush/resources/traces.py +52 -0
- axonpush/resources/webhooks.py +96 -0
- axonpush-0.0.1.dist-info/METADATA +258 -0
- axonpush-0.0.1.dist-info/RECORD +33 -0
- axonpush-0.0.1.dist-info/WHEEL +4 -0
- axonpush-0.0.1.dist-info/licenses/LICENSE +21 -0
axonpush/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""AxonPush — Python SDK for real-time event infrastructure for AI agent systems."""
|
|
2
|
+
|
|
3
|
+
from axonpush._tracing import TraceContext, get_or_create_trace
|
|
4
|
+
from axonpush._version import __version__
|
|
5
|
+
from axonpush.client import AsyncAxonPush, AxonPush
|
|
6
|
+
from axonpush.exceptions import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
AxonPushError,
|
|
9
|
+
ForbiddenError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ServerError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from axonpush.models.apps import App
|
|
16
|
+
from axonpush.models.channels import Channel
|
|
17
|
+
from axonpush.models.events import Event, EventType
|
|
18
|
+
from axonpush.models.traces import TraceListItem, TraceSummary
|
|
19
|
+
from axonpush.models.webhooks import DeliveryStatus, WebhookDelivery, WebhookEndpoint
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Clients
|
|
23
|
+
"AxonPush",
|
|
24
|
+
"AsyncAxonPush",
|
|
25
|
+
# Models
|
|
26
|
+
"App",
|
|
27
|
+
"Channel",
|
|
28
|
+
"DeliveryStatus",
|
|
29
|
+
"Event",
|
|
30
|
+
"EventType",
|
|
31
|
+
"TraceListItem",
|
|
32
|
+
"TraceSummary",
|
|
33
|
+
"WebhookDelivery",
|
|
34
|
+
"WebhookEndpoint",
|
|
35
|
+
# Tracing
|
|
36
|
+
"TraceContext",
|
|
37
|
+
"get_or_create_trace",
|
|
38
|
+
# Exceptions
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"AxonPushError",
|
|
41
|
+
"ForbiddenError",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
"RateLimitError",
|
|
44
|
+
"ServerError",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
# Meta
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
axonpush/_auth.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthConfig:
|
|
5
|
+
"""Immutable auth configuration. Thread-safe (read-only after construction)."""
|
|
6
|
+
|
|
7
|
+
__slots__ = ("api_key", "tenant_id", "base_url")
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
api_key: str,
|
|
12
|
+
tenant_id: str,
|
|
13
|
+
base_url: str,
|
|
14
|
+
) -> None:
|
|
15
|
+
self.api_key = api_key
|
|
16
|
+
self.tenant_id = tenant_id
|
|
17
|
+
self.base_url = base_url.rstrip("/")
|
|
18
|
+
|
|
19
|
+
def headers(self) -> dict[str, str]:
|
|
20
|
+
return {
|
|
21
|
+
"X-API-Key": self.api_key,
|
|
22
|
+
"x-tenant-id": self.tenant_id,
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
}
|
axonpush/_http.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Any, Dict, Generator, Iterator, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from httpx_sse import EventSource, connect_sse, aconnect_sse
|
|
8
|
+
|
|
9
|
+
from axonpush._auth import AuthConfig
|
|
10
|
+
from axonpush.exceptions import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
AxonPushError,
|
|
13
|
+
ForbiddenError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_ERROR_MAP: Dict[int, type] = {
|
|
21
|
+
400: ValidationError,
|
|
22
|
+
401: AuthenticationError,
|
|
23
|
+
403: ForbiddenError,
|
|
24
|
+
404: NotFoundError,
|
|
25
|
+
429: RateLimitError,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
30
|
+
if response.is_success:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
status = response.status_code
|
|
34
|
+
try:
|
|
35
|
+
body = response.json()
|
|
36
|
+
message = body.get("message", response.text)
|
|
37
|
+
if isinstance(message, list):
|
|
38
|
+
message = "; ".join(str(m) for m in message)
|
|
39
|
+
except Exception:
|
|
40
|
+
message = response.text or f"HTTP {status}"
|
|
41
|
+
|
|
42
|
+
if status == 429:
|
|
43
|
+
retry_after_raw = response.headers.get("Retry-After")
|
|
44
|
+
retry_after = float(retry_after_raw) if retry_after_raw else None
|
|
45
|
+
raise RateLimitError(str(message), retry_after=retry_after)
|
|
46
|
+
|
|
47
|
+
exc_cls = _ERROR_MAP.get(status)
|
|
48
|
+
if exc_cls is not None:
|
|
49
|
+
raise exc_cls(str(message), status_code=status)
|
|
50
|
+
|
|
51
|
+
if status >= 500:
|
|
52
|
+
raise ServerError(str(message), status_code=status)
|
|
53
|
+
|
|
54
|
+
raise AxonPushError(str(message), status_code=status)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SyncTransport:
|
|
58
|
+
"""Synchronous HTTP transport backed by httpx.Client."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, auth: AuthConfig, timeout: float = 30.0) -> None:
|
|
61
|
+
self._auth = auth
|
|
62
|
+
self._client = httpx.Client(
|
|
63
|
+
base_url=auth.base_url,
|
|
64
|
+
headers=auth.headers(),
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
*,
|
|
73
|
+
json: Any = None,
|
|
74
|
+
params: Optional[Dict[str, Any]] = None,
|
|
75
|
+
) -> Any:
|
|
76
|
+
response = self._client.request(method, path, json=json, params=params)
|
|
77
|
+
_raise_for_status(response)
|
|
78
|
+
if not response.content:
|
|
79
|
+
return None
|
|
80
|
+
return response.json()
|
|
81
|
+
|
|
82
|
+
@contextmanager
|
|
83
|
+
def stream_sse(
|
|
84
|
+
self, path: str, params: Optional[Dict[str, Any]] = None
|
|
85
|
+
) -> Generator[EventSource, None, None]:
|
|
86
|
+
with connect_sse(
|
|
87
|
+
self._client, "GET", path, params=params or {}
|
|
88
|
+
) as event_source:
|
|
89
|
+
yield event_source
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
self._client.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AsyncTransport:
|
|
96
|
+
"""Asynchronous HTTP transport backed by httpx.AsyncClient."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, auth: AuthConfig, timeout: float = 30.0) -> None:
|
|
99
|
+
self._auth = auth
|
|
100
|
+
self._client = httpx.AsyncClient(
|
|
101
|
+
base_url=auth.base_url,
|
|
102
|
+
headers=auth.headers(),
|
|
103
|
+
timeout=timeout,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def request(
|
|
107
|
+
self,
|
|
108
|
+
method: str,
|
|
109
|
+
path: str,
|
|
110
|
+
*,
|
|
111
|
+
json: Any = None,
|
|
112
|
+
params: Optional[Dict[str, Any]] = None,
|
|
113
|
+
) -> Any:
|
|
114
|
+
response = await self._client.request(method, path, json=json, params=params)
|
|
115
|
+
_raise_for_status(response)
|
|
116
|
+
if not response.content:
|
|
117
|
+
return None
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
async def close(self) -> None:
|
|
121
|
+
await self._client.aclose()
|
axonpush/_tracing.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import uuid
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
_current_trace: ContextVar[TraceContext | None] = ContextVar("_current_trace", default=None)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TraceContext:
|
|
13
|
+
"""Holds a trace_id and generates sequential span IDs.
|
|
14
|
+
|
|
15
|
+
Thread-safe via a lock on the span counter.
|
|
16
|
+
Task-safe via contextvars (each asyncio Task inherits its own copy).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
trace_id: str = field(default_factory=lambda: f"tr_{uuid.uuid4().hex[:16]}")
|
|
20
|
+
_span_counter: int = field(default=0, repr=False)
|
|
21
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
22
|
+
|
|
23
|
+
def next_span_id(self) -> str:
|
|
24
|
+
with self._lock:
|
|
25
|
+
self._span_counter += 1
|
|
26
|
+
return f"sp_{self.trace_id[3:]}_{self._span_counter:04d}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_or_create_trace(trace_id: str | None = None) -> TraceContext:
|
|
30
|
+
"""Get the current trace from context, or create a new one.
|
|
31
|
+
|
|
32
|
+
If *trace_id* is provided, always creates a fresh context with that ID.
|
|
33
|
+
If *trace_id* is None and a context already exists, returns it.
|
|
34
|
+
Otherwise creates a new context with an auto-generated ID.
|
|
35
|
+
"""
|
|
36
|
+
if trace_id is not None:
|
|
37
|
+
ctx = TraceContext(trace_id=trace_id)
|
|
38
|
+
_current_trace.set(ctx)
|
|
39
|
+
return ctx
|
|
40
|
+
|
|
41
|
+
existing = _current_trace.get()
|
|
42
|
+
if existing is not None:
|
|
43
|
+
return existing
|
|
44
|
+
|
|
45
|
+
ctx = TraceContext()
|
|
46
|
+
_current_trace.set(ctx)
|
|
47
|
+
return ctx
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def current_trace() -> TraceContext | None:
|
|
51
|
+
"""Return the current trace context, or None if not set."""
|
|
52
|
+
return _current_trace.get()
|
axonpush/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
axonpush/client.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from axonpush._auth import AuthConfig
|
|
4
|
+
from axonpush._http import AsyncTransport, SyncTransport
|
|
5
|
+
from axonpush.realtime.websocket import AsyncWebSocketClient, WebSocketClient
|
|
6
|
+
from axonpush.resources.apps import AppsResource, AsyncAppsResource
|
|
7
|
+
from axonpush.resources.channels import AsyncChannelsResource, ChannelsResource
|
|
8
|
+
from axonpush.resources.events import AsyncEventsResource, EventsResource
|
|
9
|
+
from axonpush.resources.traces import AsyncTracesResource, TracesResource
|
|
10
|
+
from axonpush.resources.webhooks import AsyncWebhooksResource, WebhooksResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AxonPush:
|
|
14
|
+
"""Synchronous AxonPush client. Thread-safe.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
with AxonPush(api_key="ak_...", tenant_id="1", base_url="https://...") as client:
|
|
19
|
+
event = client.events.publish(
|
|
20
|
+
"web_search", {"query": "AI agents"}, channel_id=1,
|
|
21
|
+
agent_id="researcher", event_type="agent.tool_call.start",
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
api_key: str,
|
|
28
|
+
tenant_id: str,
|
|
29
|
+
*,
|
|
30
|
+
base_url: str,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._auth = AuthConfig(api_key, tenant_id, base_url)
|
|
34
|
+
self._transport = SyncTransport(self._auth, timeout)
|
|
35
|
+
|
|
36
|
+
self.events = EventsResource(self._transport)
|
|
37
|
+
self.channels = ChannelsResource(self._transport)
|
|
38
|
+
self.apps = AppsResource(self._transport)
|
|
39
|
+
self.webhooks = WebhooksResource(self._transport)
|
|
40
|
+
self.traces = TracesResource(self._transport)
|
|
41
|
+
|
|
42
|
+
def connect_websocket(self) -> WebSocketClient:
|
|
43
|
+
"""Create and connect a Socket.IO WebSocket client."""
|
|
44
|
+
ws = WebSocketClient(self._auth)
|
|
45
|
+
ws.connect()
|
|
46
|
+
return ws
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
"""Close the underlying HTTP transport."""
|
|
50
|
+
self._transport.close()
|
|
51
|
+
|
|
52
|
+
def __enter__(self) -> AxonPush:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, *args: object) -> None:
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AsyncAxonPush:
|
|
60
|
+
"""Asynchronous AxonPush client. Task-safe.
|
|
61
|
+
|
|
62
|
+
Usage::
|
|
63
|
+
|
|
64
|
+
async with AsyncAxonPush(api_key="ak_...", tenant_id="1", base_url="https://...") as client:
|
|
65
|
+
event = await client.events.publish(
|
|
66
|
+
"web_search", {"query": "AI agents"}, channel_id=1,
|
|
67
|
+
agent_id="researcher", event_type="agent.tool_call.start",
|
|
68
|
+
)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
api_key: str,
|
|
74
|
+
tenant_id: str,
|
|
75
|
+
*,
|
|
76
|
+
base_url: str,
|
|
77
|
+
timeout: float = 30.0,
|
|
78
|
+
) -> None:
|
|
79
|
+
self._auth = AuthConfig(api_key, tenant_id, base_url)
|
|
80
|
+
self._transport = AsyncTransport(self._auth, timeout)
|
|
81
|
+
|
|
82
|
+
self.events = AsyncEventsResource(self._transport)
|
|
83
|
+
self.channels = AsyncChannelsResource(self._transport)
|
|
84
|
+
self.apps = AsyncAppsResource(self._transport)
|
|
85
|
+
self.webhooks = AsyncWebhooksResource(self._transport)
|
|
86
|
+
self.traces = AsyncTracesResource(self._transport)
|
|
87
|
+
|
|
88
|
+
async def connect_websocket(self) -> AsyncWebSocketClient:
|
|
89
|
+
"""Create and connect an async Socket.IO WebSocket client."""
|
|
90
|
+
ws = AsyncWebSocketClient(self._auth)
|
|
91
|
+
await ws.connect()
|
|
92
|
+
return ws
|
|
93
|
+
|
|
94
|
+
async def close(self) -> None:
|
|
95
|
+
"""Close the underlying HTTP transport."""
|
|
96
|
+
await self._transport.close()
|
|
97
|
+
|
|
98
|
+
async def __aenter__(self) -> AsyncAxonPush:
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
async def __aexit__(self, *args: object) -> None:
|
|
102
|
+
await self.close()
|
axonpush/exceptions.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AxonPushError(Exception):
|
|
5
|
+
"""Base exception for all AxonPush SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationError(AxonPushError):
|
|
13
|
+
"""Raised when the API key or JWT token is invalid or missing (HTTP 401)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ForbiddenError(AxonPushError):
|
|
17
|
+
"""Raised when the authenticated user lacks permission (HTTP 403)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotFoundError(AxonPushError):
|
|
21
|
+
"""Raised when the requested resource does not exist (HTTP 404)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(AxonPushError):
|
|
25
|
+
"""Raised when the request body fails validation (HTTP 400)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(AxonPushError):
|
|
29
|
+
"""Raised when the rate limit is exceeded (HTTP 429)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, retry_after: float | None = None) -> None:
|
|
32
|
+
self.retry_after = retry_after
|
|
33
|
+
super().__init__(message, status_code=429)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ServerError(AxonPushError):
|
|
37
|
+
"""Raised when the server returns a 5xx error."""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Integrations are lazily imported to avoid requiring optional dependencies.
|
|
2
|
+
# Import them directly:
|
|
3
|
+
#
|
|
4
|
+
# from axonpush.integrations.langchain import AxonPushCallbackHandler
|
|
5
|
+
# from axonpush.integrations.openai_agents import AxonPushRunHooks
|
|
6
|
+
# from axonpush.integrations.anthropic import AxonPushAnthropicTracer
|
|
7
|
+
# from axonpush.integrations.crewai import AxonPushCrewCallbacks
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Anthropic/Claude integration for AxonPush.
|
|
2
|
+
|
|
3
|
+
Requires: ``pip install axonpush[anthropic]``
|
|
4
|
+
|
|
5
|
+
Unlike LangChain/CrewAI, the Anthropic SDK has no callback hooks. This
|
|
6
|
+
integration wraps ``messages.create()`` calls to automatically emit events
|
|
7
|
+
for tool_use blocks, text responses, and conversation turns.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from axonpush import AxonPush
|
|
12
|
+
from axonpush.integrations.anthropic import AxonPushAnthropicTracer
|
|
13
|
+
|
|
14
|
+
client = AxonPush(api_key="ak_...", tenant_id="1")
|
|
15
|
+
tracer = AxonPushAnthropicTracer(client, channel_id=1)
|
|
16
|
+
|
|
17
|
+
# Sync
|
|
18
|
+
response = tracer.create_message(
|
|
19
|
+
anthropic_client,
|
|
20
|
+
model="claude-sonnet-4-20250514",
|
|
21
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
22
|
+
tools=[...],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Async
|
|
26
|
+
response = await tracer.acreate_message(async_anthropic_client, ...)
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, Dict, Optional
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import anthropic # noqa: F401 — verify the package is installed
|
|
34
|
+
except ImportError:
|
|
35
|
+
raise ImportError(
|
|
36
|
+
"Anthropic integration requires the 'anthropic' extra. "
|
|
37
|
+
"Install it with: pip install axonpush[anthropic]"
|
|
38
|
+
) from None
|
|
39
|
+
|
|
40
|
+
from axonpush._tracing import get_or_create_trace
|
|
41
|
+
from axonpush.models.events import EventType
|
|
42
|
+
|
|
43
|
+
from typing import TYPE_CHECKING
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from axonpush.client import AsyncAxonPush, AxonPush
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AxonPushAnthropicTracer:
|
|
50
|
+
"""Wraps Anthropic API calls to emit AxonPush trace events."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
client: AxonPush | AsyncAxonPush,
|
|
55
|
+
channel_id: int,
|
|
56
|
+
*,
|
|
57
|
+
agent_id: str = "claude",
|
|
58
|
+
trace_id: Optional[str] = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._client = client
|
|
61
|
+
self._channel_id = channel_id
|
|
62
|
+
self._agent_id = agent_id
|
|
63
|
+
self._trace = get_or_create_trace(trace_id)
|
|
64
|
+
|
|
65
|
+
def create_message(self, anthropic_client: Any, **kwargs: Any) -> Any:
|
|
66
|
+
"""Wrap a sync ``anthropic_client.messages.create()`` call with tracing."""
|
|
67
|
+
self._emit_sync(
|
|
68
|
+
"conversation.turn",
|
|
69
|
+
EventType.AGENT_START,
|
|
70
|
+
{
|
|
71
|
+
"model": kwargs.get("model"),
|
|
72
|
+
"message_count": len(kwargs.get("messages", [])),
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
response = anthropic_client.messages.create(**kwargs)
|
|
77
|
+
self._process_response(response)
|
|
78
|
+
return response
|
|
79
|
+
|
|
80
|
+
async def acreate_message(self, anthropic_client: Any, **kwargs: Any) -> Any:
|
|
81
|
+
"""Wrap an async ``anthropic_client.messages.create()`` call with tracing."""
|
|
82
|
+
await self._emit_async(
|
|
83
|
+
"conversation.turn",
|
|
84
|
+
EventType.AGENT_START,
|
|
85
|
+
{
|
|
86
|
+
"model": kwargs.get("model"),
|
|
87
|
+
"message_count": len(kwargs.get("messages", [])),
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = await anthropic_client.messages.create(**kwargs)
|
|
92
|
+
await self._aprocess_response(response)
|
|
93
|
+
return response
|
|
94
|
+
|
|
95
|
+
def send_tool_result(self, tool_use_id: str, result: Any) -> None:
|
|
96
|
+
"""Emit a tool_call.end event when you send a tool result back."""
|
|
97
|
+
self._emit_sync(
|
|
98
|
+
"tool.result",
|
|
99
|
+
EventType.AGENT_TOOL_CALL_END,
|
|
100
|
+
{"tool_use_id": tool_use_id, "result_preview": str(result)[:500]},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def asend_tool_result(self, tool_use_id: str, result: Any) -> None:
|
|
104
|
+
"""Async variant of send_tool_result."""
|
|
105
|
+
await self._emit_async(
|
|
106
|
+
"tool.result",
|
|
107
|
+
EventType.AGENT_TOOL_CALL_END,
|
|
108
|
+
{"tool_use_id": tool_use_id, "result_preview": str(result)[:500]},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# -- Internal --
|
|
112
|
+
|
|
113
|
+
def _process_response(self, response: Any) -> None:
|
|
114
|
+
for block in getattr(response, "content", []):
|
|
115
|
+
block_type = getattr(block, "type", None)
|
|
116
|
+
if block_type == "tool_use":
|
|
117
|
+
self._emit_sync(
|
|
118
|
+
f"tool.{block.name}.start",
|
|
119
|
+
EventType.AGENT_TOOL_CALL_START,
|
|
120
|
+
{
|
|
121
|
+
"tool_name": block.name,
|
|
122
|
+
"tool_use_id": block.id,
|
|
123
|
+
"input": _truncate(block.input),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
elif block_type == "text":
|
|
127
|
+
self._emit_sync(
|
|
128
|
+
"agent.response",
|
|
129
|
+
EventType.AGENT_MESSAGE,
|
|
130
|
+
{"text_length": len(block.text)},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def _aprocess_response(self, response: Any) -> None:
|
|
134
|
+
for block in getattr(response, "content", []):
|
|
135
|
+
block_type = getattr(block, "type", None)
|
|
136
|
+
if block_type == "tool_use":
|
|
137
|
+
await self._emit_async(
|
|
138
|
+
f"tool.{block.name}.start",
|
|
139
|
+
EventType.AGENT_TOOL_CALL_START,
|
|
140
|
+
{
|
|
141
|
+
"tool_name": block.name,
|
|
142
|
+
"tool_use_id": block.id,
|
|
143
|
+
"input": _truncate(block.input),
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
elif block_type == "text":
|
|
147
|
+
await self._emit_async(
|
|
148
|
+
"agent.response",
|
|
149
|
+
EventType.AGENT_MESSAGE,
|
|
150
|
+
{"text_length": len(block.text)},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _emit_sync(
|
|
154
|
+
self, identifier: str, event_type: EventType, payload: Dict[str, Any]
|
|
155
|
+
) -> None:
|
|
156
|
+
self._client.events.publish( # type: ignore[union-attr]
|
|
157
|
+
identifier=identifier,
|
|
158
|
+
payload=payload,
|
|
159
|
+
channel_id=self._channel_id,
|
|
160
|
+
agent_id=self._agent_id,
|
|
161
|
+
trace_id=self._trace.trace_id,
|
|
162
|
+
span_id=self._trace.next_span_id(),
|
|
163
|
+
event_type=event_type,
|
|
164
|
+
metadata={"framework": "anthropic"},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def _emit_async(
|
|
168
|
+
self, identifier: str, event_type: EventType, payload: Dict[str, Any]
|
|
169
|
+
) -> None:
|
|
170
|
+
await self._client.events.publish( # type: ignore[union-attr]
|
|
171
|
+
identifier=identifier,
|
|
172
|
+
payload=payload,
|
|
173
|
+
channel_id=self._channel_id,
|
|
174
|
+
agent_id=self._agent_id,
|
|
175
|
+
trace_id=self._trace.trace_id,
|
|
176
|
+
span_id=self._trace.next_span_id(),
|
|
177
|
+
event_type=event_type,
|
|
178
|
+
metadata={"framework": "anthropic"},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _truncate(obj: Any, max_len: int = 500) -> Any:
|
|
183
|
+
s = str(obj)
|
|
184
|
+
return s[:max_len] if len(s) > max_len else s
|