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.
@@ -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