agentos-python 0.1.0__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.
- agentos/__init__.py +58 -0
- agentos/client.py +273 -0
- agentos/compat/__init__.py +4 -0
- agentos/compat/braintrust.py +97 -0
- agentos/compat/helicone.py +72 -0
- agentos/compat/langfuse.py +317 -0
- agentos/compat/langsmith.py +79 -0
- agentos/compat/otel.py +167 -0
- agentos/compat/weave.py +93 -0
- agentos/config.py +34 -0
- agentos/decorators.py +372 -0
- agentos/events.py +449 -0
- agentos/integrations/__init__.py +15 -0
- agentos/integrations/anthropic.py +130 -0
- agentos/integrations/crewai.py +221 -0
- agentos/integrations/langchain.py +307 -0
- agentos/integrations/litellm.py +123 -0
- agentos/integrations/llamaindex.py +174 -0
- agentos/integrations/openai.py +149 -0
- agentos/integrations/pydantic_ai.py +236 -0
- agentos/py.typed +0 -0
- agentos/tracing.py +168 -0
- agentos/transport.py +285 -0
- agentos/types.py +101 -0
- agentos_python-0.1.0.dist-info/METADATA +155 -0
- agentos_python-0.1.0.dist-info/RECORD +28 -0
- agentos_python-0.1.0.dist-info/WHEEL +4 -0
- agentos_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""LlamaIndex integration — CallbackHandler for auto-capturing LLM and retriever events.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from agentos.integrations.llamaindex import AgentOSCallbackHandler
|
|
6
|
+
from llama_index.core import Settings
|
|
7
|
+
|
|
8
|
+
handler = AgentOSCallbackHandler(api_key="aos_...", agent_id="my-agent")
|
|
9
|
+
Settings.callback_manager.add_handler(handler)
|
|
10
|
+
|
|
11
|
+
# All LlamaIndex LLM calls and retrievals auto-captured
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("agentos.integrations.llamaindex")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentOSCallbackHandler:
|
|
24
|
+
"""LlamaIndex-compatible callback handler.
|
|
25
|
+
|
|
26
|
+
Implements the LlamaIndex BaseCallbackHandler interface via duck typing
|
|
27
|
+
(no import required). Captures LLM calls, retriever queries, and tool use.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
agent_id: str = "llamaindex-agent",
|
|
34
|
+
base_url: str = "https://api.agentos.dev",
|
|
35
|
+
capture_content: bool = True,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
from agentos.client import AgentOS, get_client
|
|
39
|
+
|
|
40
|
+
if api_key:
|
|
41
|
+
self._client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0, **kwargs)
|
|
42
|
+
else:
|
|
43
|
+
self._client = get_client()
|
|
44
|
+
|
|
45
|
+
self._agent_id = agent_id
|
|
46
|
+
self._capture_content = capture_content
|
|
47
|
+
self._starts: dict[str, float] = {}
|
|
48
|
+
self._event_data: dict[str, dict[str, Any]] = {}
|
|
49
|
+
|
|
50
|
+
# Required by LlamaIndex BaseCallbackHandler
|
|
51
|
+
def on_event_start(
|
|
52
|
+
self,
|
|
53
|
+
event_type: str,
|
|
54
|
+
payload: dict[str, Any] | None = None,
|
|
55
|
+
event_id: str = "",
|
|
56
|
+
parent_id: str = "",
|
|
57
|
+
**kwargs: Any,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Called when a LlamaIndex event starts."""
|
|
60
|
+
self._starts[event_id] = time.monotonic()
|
|
61
|
+
self._event_data[event_id] = {
|
|
62
|
+
"event_type": event_type,
|
|
63
|
+
"payload": payload or {},
|
|
64
|
+
"parent_id": parent_id,
|
|
65
|
+
}
|
|
66
|
+
return event_id
|
|
67
|
+
|
|
68
|
+
def on_event_end(
|
|
69
|
+
self,
|
|
70
|
+
event_type: str,
|
|
71
|
+
payload: dict[str, Any] | None = None,
|
|
72
|
+
event_id: str = "",
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Called when a LlamaIndex event ends."""
|
|
76
|
+
if self._client is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
start = self._starts.pop(event_id, None)
|
|
80
|
+
start_data = self._event_data.pop(event_id, {})
|
|
81
|
+
duration_ms = (time.monotonic() - start) * 1000 if start else None
|
|
82
|
+
payload = payload or {}
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if event_type == "llm":
|
|
86
|
+
self._handle_llm_end(payload, start_data, duration_ms)
|
|
87
|
+
elif event_type == "retrieve":
|
|
88
|
+
self._handle_retrieve_end(payload, start_data, duration_ms)
|
|
89
|
+
elif event_type == "function_call":
|
|
90
|
+
self._handle_tool_end(payload, start_data, duration_ms)
|
|
91
|
+
except Exception:
|
|
92
|
+
logger.exception("Failed to capture LlamaIndex event")
|
|
93
|
+
|
|
94
|
+
def _handle_llm_end(
|
|
95
|
+
self,
|
|
96
|
+
payload: dict[str, Any],
|
|
97
|
+
start_data: dict[str, Any],
|
|
98
|
+
duration_ms: float | None,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Process an LLM completion event."""
|
|
101
|
+
response = payload.get("response", "")
|
|
102
|
+
model = payload.get("model", "unknown")
|
|
103
|
+
|
|
104
|
+
data: dict[str, Any] = {
|
|
105
|
+
"model": model,
|
|
106
|
+
"system": "llamaindex",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if duration_ms is not None:
|
|
110
|
+
data["duration_ms"] = duration_ms
|
|
111
|
+
|
|
112
|
+
# Token counts from payload
|
|
113
|
+
token_counts = payload.get("token_counts", {})
|
|
114
|
+
if token_counts:
|
|
115
|
+
data["input_tokens"] = token_counts.get("prompt_tokens")
|
|
116
|
+
data["output_tokens"] = token_counts.get("completion_tokens")
|
|
117
|
+
|
|
118
|
+
if self._capture_content and response:
|
|
119
|
+
data["output"] = [{"role": "assistant", "content": str(response)}]
|
|
120
|
+
|
|
121
|
+
self._client.llm_call(self._agent_id, **data) # type: ignore[union-attr]
|
|
122
|
+
|
|
123
|
+
def _handle_retrieve_end(
|
|
124
|
+
self,
|
|
125
|
+
payload: dict[str, Any],
|
|
126
|
+
start_data: dict[str, Any],
|
|
127
|
+
duration_ms: float | None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Process a retrieval event."""
|
|
130
|
+
nodes = payload.get("nodes", [])
|
|
131
|
+
|
|
132
|
+
self._client.retrieval_query( # type: ignore[union-attr]
|
|
133
|
+
self._agent_id,
|
|
134
|
+
source="llamaindex-retriever",
|
|
135
|
+
results_count=len(nodes),
|
|
136
|
+
duration_ms=duration_ms,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _handle_tool_end(
|
|
140
|
+
self,
|
|
141
|
+
payload: dict[str, Any],
|
|
142
|
+
start_data: dict[str, Any],
|
|
143
|
+
duration_ms: float | None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Process a tool/function call event."""
|
|
146
|
+
tool_name = payload.get("function_call", {}).get("name", "unknown")
|
|
147
|
+
|
|
148
|
+
data: dict[str, Any] = {
|
|
149
|
+
"tool_name": tool_name,
|
|
150
|
+
"status": "success",
|
|
151
|
+
}
|
|
152
|
+
if duration_ms is not None:
|
|
153
|
+
data["duration_ms"] = duration_ms
|
|
154
|
+
|
|
155
|
+
self._client.tool_call(self._agent_id, **data) # type: ignore[union-attr]
|
|
156
|
+
|
|
157
|
+
# Required interface methods
|
|
158
|
+
def start_trace(self, trace_id: str | None = None) -> None:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
def end_trace(
|
|
162
|
+
self,
|
|
163
|
+
trace_id: str | None = None,
|
|
164
|
+
trace_map: dict[str, list[str]] | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def flush(self) -> None:
|
|
169
|
+
if self._client:
|
|
170
|
+
self._client.flush()
|
|
171
|
+
|
|
172
|
+
def shutdown(self) -> None:
|
|
173
|
+
if self._client:
|
|
174
|
+
self._client.shutdown()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""OpenAI SDK integration — wraps client to auto-capture LLM calls.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from agentos.integrations.openai import wrap_openai
|
|
6
|
+
import openai
|
|
7
|
+
|
|
8
|
+
client = openai.OpenAI()
|
|
9
|
+
client = wrap_openai(client, api_key="aos_...", agent_id="my-agent")
|
|
10
|
+
|
|
11
|
+
# All chat.completions.create() calls auto-traced
|
|
12
|
+
response = client.chat.completions.create(model="gpt-4o", messages=[...])
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("agentos.integrations.openai")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def wrap_openai(
|
|
25
|
+
openai_client: Any,
|
|
26
|
+
*,
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
agent_id: str = "default",
|
|
29
|
+
base_url: str = "https://api.agentos.dev",
|
|
30
|
+
capture_content: bool = True,
|
|
31
|
+
**kwargs: Any,
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""Wrap an OpenAI client to auto-capture LLM calls.
|
|
34
|
+
|
|
35
|
+
Returns the same client with patched ``chat.completions.create`` method.
|
|
36
|
+
"""
|
|
37
|
+
from agentos.client import AgentOS, get_client
|
|
38
|
+
|
|
39
|
+
aos_client = None
|
|
40
|
+
if api_key:
|
|
41
|
+
aos_client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0, **kwargs)
|
|
42
|
+
else:
|
|
43
|
+
aos_client = get_client()
|
|
44
|
+
|
|
45
|
+
if aos_client is None:
|
|
46
|
+
logger.warning("No AgentOS client configured — wrap_openai is a no-op")
|
|
47
|
+
return openai_client
|
|
48
|
+
|
|
49
|
+
original_create = openai_client.chat.completions.create
|
|
50
|
+
|
|
51
|
+
def _extract_event_data(
|
|
52
|
+
response: Any,
|
|
53
|
+
model_arg: str,
|
|
54
|
+
messages: Any,
|
|
55
|
+
start: float,
|
|
56
|
+
call_kwargs: dict[str, Any],
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Extract event data from an OpenAI response."""
|
|
59
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
60
|
+
|
|
61
|
+
data: dict[str, Any] = {
|
|
62
|
+
"model": getattr(response, "model", model_arg),
|
|
63
|
+
"system": "openai",
|
|
64
|
+
"duration_ms": duration_ms,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Usage
|
|
68
|
+
usage = getattr(response, "usage", None)
|
|
69
|
+
if usage:
|
|
70
|
+
data["input_tokens"] = getattr(usage, "prompt_tokens", None)
|
|
71
|
+
data["output_tokens"] = getattr(usage, "completion_tokens", None)
|
|
72
|
+
data["total_tokens"] = getattr(usage, "total_tokens", None)
|
|
73
|
+
|
|
74
|
+
# Finish reason
|
|
75
|
+
choices = getattr(response, "choices", [])
|
|
76
|
+
if choices:
|
|
77
|
+
fr = getattr(choices[0], "finish_reason", None)
|
|
78
|
+
if fr:
|
|
79
|
+
data["finish_reason"] = fr
|
|
80
|
+
|
|
81
|
+
# Response ID
|
|
82
|
+
resp_id = getattr(response, "id", None)
|
|
83
|
+
if resp_id:
|
|
84
|
+
data["response_id"] = resp_id
|
|
85
|
+
|
|
86
|
+
# Content (if capture enabled)
|
|
87
|
+
if capture_content:
|
|
88
|
+
if messages:
|
|
89
|
+
data["input"] = [
|
|
90
|
+
{"role": m.get("role", ""), "content": m.get("content")}
|
|
91
|
+
for m in messages
|
|
92
|
+
if isinstance(m, dict)
|
|
93
|
+
]
|
|
94
|
+
if choices:
|
|
95
|
+
msg = getattr(choices[0], "message", None)
|
|
96
|
+
if msg:
|
|
97
|
+
data["output"] = [
|
|
98
|
+
{
|
|
99
|
+
"role": getattr(msg, "role", "assistant"),
|
|
100
|
+
"content": getattr(msg, "content", None),
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Temperature, max_tokens from call args
|
|
105
|
+
if "temperature" in call_kwargs:
|
|
106
|
+
data["temperature"] = call_kwargs["temperature"]
|
|
107
|
+
if "max_tokens" in call_kwargs:
|
|
108
|
+
data["max_tokens"] = call_kwargs["max_tokens"]
|
|
109
|
+
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
def patched_create(*args: Any, **call_kwargs: Any) -> Any:
|
|
113
|
+
model_arg = call_kwargs.get("model", "unknown")
|
|
114
|
+
messages = call_kwargs.get("messages")
|
|
115
|
+
stream = call_kwargs.get("stream", False)
|
|
116
|
+
|
|
117
|
+
start = time.monotonic()
|
|
118
|
+
try:
|
|
119
|
+
response = original_create(*args, **call_kwargs)
|
|
120
|
+
|
|
121
|
+
if stream:
|
|
122
|
+
# For streaming, return the stream as-is
|
|
123
|
+
# (full streaming capture would require wrapping the iterator)
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
data = _extract_event_data(response, model_arg, messages, start, call_kwargs)
|
|
127
|
+
try:
|
|
128
|
+
aos_client.llm_call(agent_id, **data)
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("Failed to capture LLM call")
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
136
|
+
try:
|
|
137
|
+
aos_client.llm_call(
|
|
138
|
+
agent_id,
|
|
139
|
+
model=model_arg,
|
|
140
|
+
system="openai",
|
|
141
|
+
duration_ms=duration_ms,
|
|
142
|
+
error={"type": type(exc).__name__, "message": str(exc)},
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.exception("Failed to capture LLM error")
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
openai_client.chat.completions.create = patched_create
|
|
149
|
+
return openai_client
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Pydantic AI integration — instruments Agent.run() to capture LLM calls and tool use.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from agentos.integrations.pydantic_ai import instrument_pydantic_ai
|
|
6
|
+
|
|
7
|
+
instrument_pydantic_ai(api_key="aos_...", agent_id="my-agent")
|
|
8
|
+
|
|
9
|
+
# All pydantic_ai Agent.run() calls auto-captured
|
|
10
|
+
from pydantic_ai import Agent
|
|
11
|
+
agent = Agent("openai:gpt-4o")
|
|
12
|
+
result = agent.run_sync("What is 2+2?")
|
|
13
|
+
|
|
14
|
+
Or wrap a single agent::
|
|
15
|
+
|
|
16
|
+
from agentos.integrations.pydantic_ai import wrap_agent
|
|
17
|
+
|
|
18
|
+
agent = Agent("openai:gpt-4o")
|
|
19
|
+
agent = wrap_agent(agent, api_key="aos_...", agent_id="math-bot")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import functools
|
|
25
|
+
import logging
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("agentos.integrations.pydantic_ai")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def wrap_agent(
|
|
33
|
+
agent: Any,
|
|
34
|
+
*,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
agent_id: str = "pydantic-ai",
|
|
37
|
+
base_url: str = "https://api.agentos.dev",
|
|
38
|
+
capture_content: bool = True,
|
|
39
|
+
) -> Any:
|
|
40
|
+
"""Wrap a Pydantic AI Agent to auto-capture runs.
|
|
41
|
+
|
|
42
|
+
Patches ``run()``, ``run_sync()``, and ``run_stream()`` on the agent instance.
|
|
43
|
+
"""
|
|
44
|
+
from agentos.client import AgentOS, get_client
|
|
45
|
+
from agentos.tracing import trace
|
|
46
|
+
|
|
47
|
+
client = get_client()
|
|
48
|
+
if client is None and api_key:
|
|
49
|
+
client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0)
|
|
50
|
+
|
|
51
|
+
if client is None:
|
|
52
|
+
logger.warning("No AgentOS client — wrap_agent is a no-op")
|
|
53
|
+
return agent
|
|
54
|
+
|
|
55
|
+
# Extract model info from agent
|
|
56
|
+
model_name = "unknown"
|
|
57
|
+
model_obj = getattr(agent, "model", None)
|
|
58
|
+
if model_obj is not None:
|
|
59
|
+
model_name = str(model_obj)
|
|
60
|
+
|
|
61
|
+
# Determine system from model string (e.g., "openai:gpt-4o" → "openai")
|
|
62
|
+
system = "unknown"
|
|
63
|
+
if ":" in model_name:
|
|
64
|
+
system = model_name.split(":")[0]
|
|
65
|
+
|
|
66
|
+
# Patch run_sync
|
|
67
|
+
if hasattr(agent, "run_sync"):
|
|
68
|
+
original_run_sync = agent.run_sync
|
|
69
|
+
|
|
70
|
+
@functools.wraps(original_run_sync)
|
|
71
|
+
def patched_run_sync(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
start = time.monotonic()
|
|
73
|
+
with trace(agent_id=agent_id) as ctx:
|
|
74
|
+
try:
|
|
75
|
+
result = original_run_sync(*args, **kwargs)
|
|
76
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
77
|
+
|
|
78
|
+
data_: dict[str, Any] = {
|
|
79
|
+
"model": model_name,
|
|
80
|
+
"system": system,
|
|
81
|
+
"duration_ms": duration_ms,
|
|
82
|
+
"trace_id": ctx.trace_id,
|
|
83
|
+
"span_id": ctx.span_id,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Extract usage from result if available
|
|
87
|
+
usage = getattr(result, "usage", None) or getattr(result, "_usage", None)
|
|
88
|
+
if usage:
|
|
89
|
+
data_["input_tokens"] = getattr(usage, "request_tokens", None) or getattr(
|
|
90
|
+
usage, "input_tokens", None
|
|
91
|
+
)
|
|
92
|
+
data_["output_tokens"] = getattr(usage, "response_tokens", None) or getattr(
|
|
93
|
+
usage, "output_tokens", None
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if capture_content:
|
|
97
|
+
prompt = args[0] if args else kwargs.get("user_prompt")
|
|
98
|
+
if prompt:
|
|
99
|
+
data_["input"] = [{"role": "user", "content": str(prompt)}]
|
|
100
|
+
result_data = getattr(result, "data", None)
|
|
101
|
+
if result_data is not None:
|
|
102
|
+
data_["output"] = [{"role": "assistant", "content": str(result_data)}]
|
|
103
|
+
|
|
104
|
+
client.llm_call(agent_id, **data_)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
109
|
+
client.llm_call(
|
|
110
|
+
agent_id,
|
|
111
|
+
model=model_name,
|
|
112
|
+
system=system,
|
|
113
|
+
duration_ms=duration_ms,
|
|
114
|
+
error={"type": type(exc).__name__, "message": str(exc)},
|
|
115
|
+
trace_id=ctx.trace_id,
|
|
116
|
+
span_id=ctx.span_id,
|
|
117
|
+
)
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
agent.run_sync = patched_run_sync
|
|
121
|
+
|
|
122
|
+
# Patch async run
|
|
123
|
+
if hasattr(agent, "run"):
|
|
124
|
+
original_run = agent.run
|
|
125
|
+
|
|
126
|
+
@functools.wraps(original_run)
|
|
127
|
+
async def patched_run(*args: Any, **kwargs: Any) -> Any:
|
|
128
|
+
start = time.monotonic()
|
|
129
|
+
with trace(agent_id=agent_id) as ctx:
|
|
130
|
+
try:
|
|
131
|
+
result = await original_run(*args, **kwargs)
|
|
132
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
133
|
+
|
|
134
|
+
data_: dict[str, Any] = {
|
|
135
|
+
"model": model_name,
|
|
136
|
+
"system": system,
|
|
137
|
+
"duration_ms": duration_ms,
|
|
138
|
+
"trace_id": ctx.trace_id,
|
|
139
|
+
"span_id": ctx.span_id,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if capture_content:
|
|
143
|
+
prompt = args[0] if args else kwargs.get("user_prompt")
|
|
144
|
+
if prompt:
|
|
145
|
+
data_["input"] = [{"role": "user", "content": str(prompt)}]
|
|
146
|
+
result_data = getattr(result, "data", None)
|
|
147
|
+
if result_data is not None:
|
|
148
|
+
data_["output"] = [{"role": "assistant", "content": str(result_data)}]
|
|
149
|
+
|
|
150
|
+
client.llm_call(agent_id, **data_)
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
155
|
+
client.llm_call(
|
|
156
|
+
agent_id,
|
|
157
|
+
model=model_name,
|
|
158
|
+
system=system,
|
|
159
|
+
duration_ms=duration_ms,
|
|
160
|
+
error={"type": type(exc).__name__, "message": str(exc)},
|
|
161
|
+
trace_id=ctx.trace_id,
|
|
162
|
+
span_id=ctx.span_id,
|
|
163
|
+
)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
agent.run = patched_run
|
|
167
|
+
|
|
168
|
+
return agent
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def instrument_pydantic_ai(
|
|
172
|
+
*,
|
|
173
|
+
api_key: str | None = None,
|
|
174
|
+
agent_id: str = "pydantic-ai",
|
|
175
|
+
base_url: str = "https://api.agentos.dev",
|
|
176
|
+
capture_content: bool = True,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Monkey-patch Pydantic AI's Agent class to auto-capture all runs.
|
|
179
|
+
|
|
180
|
+
Every ``Agent.run()`` and ``Agent.run_sync()`` call will be instrumented.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
from pydantic_ai import Agent
|
|
184
|
+
except ImportError:
|
|
185
|
+
logger.error("pydantic-ai not installed — cannot instrument")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
from agentos.client import AgentOS, get_client
|
|
189
|
+
|
|
190
|
+
client = get_client()
|
|
191
|
+
if client is None and api_key:
|
|
192
|
+
client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0)
|
|
193
|
+
|
|
194
|
+
if client is None:
|
|
195
|
+
logger.warning("No AgentOS client — instrument_pydantic_ai is a no-op")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
original_run_sync = Agent.run_sync
|
|
199
|
+
|
|
200
|
+
@functools.wraps(original_run_sync)
|
|
201
|
+
def patched_run_sync(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
202
|
+
from agentos.tracing import trace
|
|
203
|
+
|
|
204
|
+
model_name = str(getattr(self, "model", "unknown"))
|
|
205
|
+
system = model_name.split(":")[0] if ":" in model_name else "unknown"
|
|
206
|
+
name = getattr(self, "name", None) or agent_id
|
|
207
|
+
|
|
208
|
+
start = time.monotonic()
|
|
209
|
+
with trace(agent_id=name) as ctx:
|
|
210
|
+
try:
|
|
211
|
+
result = original_run_sync(self, *args, **kwargs)
|
|
212
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
213
|
+
client.llm_call(
|
|
214
|
+
name,
|
|
215
|
+
model=model_name,
|
|
216
|
+
system=system,
|
|
217
|
+
duration_ms=duration_ms,
|
|
218
|
+
trace_id=ctx.trace_id,
|
|
219
|
+
span_id=ctx.span_id,
|
|
220
|
+
)
|
|
221
|
+
return result
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
224
|
+
client.llm_call(
|
|
225
|
+
name,
|
|
226
|
+
model=model_name,
|
|
227
|
+
system=system,
|
|
228
|
+
duration_ms=duration_ms,
|
|
229
|
+
error={"type": type(exc).__name__, "message": str(exc)},
|
|
230
|
+
trace_id=ctx.trace_id,
|
|
231
|
+
span_id=ctx.span_id,
|
|
232
|
+
)
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
Agent.run_sync = patched_run_sync # type: ignore[assignment]
|
|
236
|
+
logger.info("Pydantic AI instrumented — Agent.run_sync patched")
|
agentos/py.typed
ADDED
|
File without changes
|