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,221 @@
1
+ """CrewAI integration — decorators and hooks for CrewAI agent instrumentation.
2
+
3
+ Usage::
4
+
5
+ from agentos.integrations.crewai import instrument_crewai
6
+
7
+ # Patches CrewAI to auto-capture agent runs, task executions, and LLM calls
8
+ instrument_crewai(api_key="aos_...", agent_id="my-crew")
9
+
10
+ Or use decorators directly::
11
+
12
+ from agentos.integrations.crewai import trace_crew, trace_task
13
+
14
+ @trace_crew("research-crew")
15
+ def run_crew():
16
+ crew = Crew(agents=[...], tasks=[...])
17
+ return crew.kickoff()
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import functools
23
+ import logging
24
+ import time
25
+ from collections.abc import Callable
26
+ from typing import Any, TypeVar
27
+
28
+ logger = logging.getLogger("agentos.integrations.crewai")
29
+
30
+ F = TypeVar("F", bound=Callable[..., Any])
31
+
32
+
33
+ def trace_crew(
34
+ crew_name: str,
35
+ *,
36
+ api_key: str | None = None,
37
+ base_url: str = "https://api.agentos.dev",
38
+ ) -> Callable[[F], F]:
39
+ """Decorator that wraps a CrewAI crew execution in a trace.
40
+
41
+ Captures the full crew run as a trace with business_event on completion.
42
+
43
+ Usage::
44
+
45
+ @trace_crew("research-crew")
46
+ def run_my_crew():
47
+ crew = Crew(agents=[researcher, writer], tasks=[...])
48
+ return crew.kickoff()
49
+ """
50
+
51
+ def decorator(fn: F) -> F:
52
+ @functools.wraps(fn)
53
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
54
+ from agentos.client import AgentOS, get_client
55
+ from agentos.tracing import trace
56
+
57
+ client = get_client()
58
+ if client is None and api_key:
59
+ client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0)
60
+
61
+ start = time.monotonic()
62
+
63
+ with trace(agent_id=crew_name) as ctx:
64
+ try:
65
+ result = fn(*args, **kwargs)
66
+ duration_ms = (time.monotonic() - start) * 1000
67
+
68
+ if client:
69
+ client.business_event(
70
+ crew_name,
71
+ event_name="crew.completed",
72
+ trace_id=ctx.trace_id,
73
+ span_id=ctx.span_id,
74
+ metadata={"duration_ms": duration_ms},
75
+ )
76
+
77
+ return result
78
+
79
+ except Exception as exc:
80
+ duration_ms = (time.monotonic() - start) * 1000
81
+
82
+ if client:
83
+ client.business_event(
84
+ crew_name,
85
+ event_name="crew.failed",
86
+ trace_id=ctx.trace_id,
87
+ span_id=ctx.span_id,
88
+ metadata={
89
+ "duration_ms": duration_ms,
90
+ "error": str(exc),
91
+ },
92
+ )
93
+ raise
94
+
95
+ return wrapper # type: ignore[return-value]
96
+
97
+ return decorator
98
+
99
+
100
+ def trace_task(
101
+ task_name: str,
102
+ ) -> Callable[[F], F]:
103
+ """Decorator that wraps a CrewAI task function as a tool_call span.
104
+
105
+ Usage::
106
+
107
+ @trace_task("research")
108
+ def research_task(topic: str) -> str:
109
+ ...
110
+ """
111
+
112
+ def decorator(fn: F) -> F:
113
+ @functools.wraps(fn)
114
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
115
+ from agentos.client import get_client
116
+ from agentos.tracing import span
117
+
118
+ client = get_client()
119
+ start = time.monotonic()
120
+
121
+ with span() as ctx:
122
+ try:
123
+ result = fn(*args, **kwargs)
124
+ duration_ms = (time.monotonic() - start) * 1000
125
+
126
+ if client:
127
+ client.tool_call(
128
+ ctx.agent_id,
129
+ tool_name=task_name,
130
+ status="success",
131
+ duration_ms=duration_ms,
132
+ trace_id=ctx.trace_id,
133
+ span_id=ctx.span_id,
134
+ parent_span_id=ctx.parent_span_id,
135
+ )
136
+
137
+ return result
138
+
139
+ except Exception as exc:
140
+ duration_ms = (time.monotonic() - start) * 1000
141
+
142
+ if client:
143
+ client.tool_call(
144
+ ctx.agent_id,
145
+ tool_name=task_name,
146
+ status="error",
147
+ duration_ms=duration_ms,
148
+ error={"type": type(exc).__name__, "message": str(exc)},
149
+ trace_id=ctx.trace_id,
150
+ span_id=ctx.span_id,
151
+ parent_span_id=ctx.parent_span_id,
152
+ )
153
+ raise
154
+
155
+ return wrapper # type: ignore[return-value]
156
+
157
+ return decorator
158
+
159
+
160
+ def instrument_crewai(
161
+ *,
162
+ api_key: str | None = None,
163
+ agent_id: str = "crewai",
164
+ base_url: str = "https://api.agentos.dev",
165
+ capture_content: bool = True,
166
+ ) -> None:
167
+ """Monkey-patch CrewAI to auto-capture all agent and task activity.
168
+
169
+ Patches ``Crew.kickoff()`` and ``Task._execute_core()`` to emit events.
170
+ """
171
+ try:
172
+ from crewai import Crew
173
+ except ImportError:
174
+ logger.error("crewai not installed — cannot instrument")
175
+ return
176
+
177
+ from agentos.client import AgentOS, get_client
178
+
179
+ client = get_client()
180
+ if client is None and api_key:
181
+ client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0)
182
+
183
+ if client is None:
184
+ logger.warning("No AgentOS client — instrument_crewai is a no-op")
185
+ return
186
+
187
+ # Patch Crew.kickoff
188
+ original_kickoff = Crew.kickoff
189
+
190
+ @functools.wraps(original_kickoff)
191
+ def patched_kickoff(self: Any, *args: Any, **kwargs: Any) -> Any:
192
+ from agentos.tracing import trace
193
+
194
+ crew_name = getattr(self, "name", agent_id) or agent_id
195
+ start = time.monotonic()
196
+
197
+ with trace(agent_id=crew_name) as ctx:
198
+ try:
199
+ result = original_kickoff(self, *args, **kwargs)
200
+ duration_ms = (time.monotonic() - start) * 1000
201
+ client.business_event(
202
+ crew_name,
203
+ event_name="crew.completed",
204
+ trace_id=ctx.trace_id,
205
+ span_id=ctx.span_id,
206
+ metadata={"duration_ms": duration_ms},
207
+ )
208
+ return result
209
+ except Exception as exc:
210
+ duration_ms = (time.monotonic() - start) * 1000
211
+ client.business_event(
212
+ crew_name,
213
+ event_name="crew.failed",
214
+ trace_id=ctx.trace_id,
215
+ span_id=ctx.span_id,
216
+ metadata={"duration_ms": duration_ms, "error": str(exc)},
217
+ )
218
+ raise
219
+
220
+ Crew.kickoff = patched_kickoff # type: ignore[assignment]
221
+ logger.info("CrewAI instrumented — Crew.kickoff patched")
@@ -0,0 +1,307 @@
1
+ """LangChain integration — CallbackHandler that auto-captures LLM and tool events.
2
+
3
+ Usage::
4
+
5
+ from agentos.integrations.langchain import AgentOSCallbackHandler
6
+
7
+ handler = AgentOSCallbackHandler(api_key="aos_...", agent_id="my-agent")
8
+ chain.invoke({"input": "..."}, config={"callbacks": [handler]})
9
+
10
+ # Or set globally:
11
+ import langchain
12
+ langchain.callbacks.set_handler(handler)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import time
19
+ from collections.abc import Sequence
20
+ from typing import Any
21
+ from uuid import UUID
22
+
23
+ logger = logging.getLogger("agentos.integrations.langchain")
24
+
25
+
26
+ class AgentOSCallbackHandler:
27
+ """LangChain-compatible callback handler.
28
+
29
+ Captures LLM calls, tool invocations, chain runs, and retriever queries
30
+ as Agent OS events.
31
+
32
+ Implements the LangChain BaseCallbackHandler interface without importing it,
33
+ so this module works even if langchain isn't installed (duck typing).
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str | None = None,
39
+ agent_id: str = "langchain-agent",
40
+ base_url: str = "https://api.agentos.dev",
41
+ capture_content: bool = True,
42
+ **kwargs: Any,
43
+ ) -> None:
44
+ from agentos.client import AgentOS, get_client
45
+
46
+ if api_key:
47
+ self._client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0, **kwargs)
48
+ else:
49
+ self._client = get_client()
50
+
51
+ self._agent_id = agent_id
52
+ self._capture_content = capture_content
53
+ self._run_starts: dict[str, float] = {} # run_id → start time
54
+
55
+ # --- LLM callbacks ---
56
+
57
+ def on_llm_start(
58
+ self,
59
+ serialized: dict[str, Any],
60
+ prompts: list[str],
61
+ *,
62
+ run_id: UUID,
63
+ parent_run_id: UUID | None = None,
64
+ tags: list[str] | None = None,
65
+ metadata: dict[str, Any] | None = None,
66
+ **kwargs: Any,
67
+ ) -> None:
68
+ """Called when an LLM starts."""
69
+ self._run_starts[str(run_id)] = time.monotonic()
70
+
71
+ def on_chat_model_start(
72
+ self,
73
+ serialized: dict[str, Any],
74
+ messages: list[list[Any]],
75
+ *,
76
+ run_id: UUID,
77
+ parent_run_id: UUID | None = None,
78
+ tags: list[str] | None = None,
79
+ metadata: dict[str, Any] | None = None,
80
+ **kwargs: Any,
81
+ ) -> None:
82
+ """Called when a chat model starts."""
83
+ self._run_starts[str(run_id)] = time.monotonic()
84
+
85
+ def on_llm_end(
86
+ self,
87
+ response: Any,
88
+ *,
89
+ run_id: UUID,
90
+ parent_run_id: UUID | None = None,
91
+ **kwargs: Any,
92
+ ) -> None:
93
+ """Called when an LLM finishes. Sends agent.llm_call event."""
94
+ if self._client is None:
95
+ return
96
+
97
+ start = self._run_starts.pop(str(run_id), None)
98
+ duration_ms = (time.monotonic() - start) * 1000 if start else None
99
+
100
+ # Extract model info from response
101
+ llm_output = getattr(response, "llm_output", {}) or {}
102
+ model = llm_output.get("model_name", "unknown")
103
+ model_id = llm_output.get("model_id", "")
104
+ system = model_id.split("/")[0] if "/" in model_id else "unknown"
105
+
106
+ # Token usage
107
+ token_usage = llm_output.get("token_usage", {})
108
+ data: dict[str, Any] = {
109
+ "model": model,
110
+ "system": system,
111
+ }
112
+
113
+ if duration_ms is not None:
114
+ data["duration_ms"] = duration_ms
115
+
116
+ if token_usage:
117
+ if "prompt_tokens" in token_usage:
118
+ data["input_tokens"] = token_usage["prompt_tokens"]
119
+ if "completion_tokens" in token_usage:
120
+ data["output_tokens"] = token_usage["completion_tokens"]
121
+ if "total_tokens" in token_usage:
122
+ data["total_tokens"] = token_usage["total_tokens"]
123
+
124
+ # Content
125
+ if self._capture_content:
126
+ generations = getattr(response, "generations", [])
127
+ if generations and generations[0]:
128
+ gen = generations[0][0]
129
+ text = getattr(gen, "text", None)
130
+ if text:
131
+ data["output"] = [{"role": "assistant", "content": text}]
132
+ # Check for message-based output
133
+ message = getattr(gen, "message", None)
134
+ if message:
135
+ content = getattr(message, "content", None)
136
+ role = getattr(message, "type", "assistant")
137
+ if content:
138
+ data["output"] = [{"role": role, "content": content}]
139
+
140
+ try:
141
+ self._client.llm_call(self._agent_id, **data)
142
+ except Exception:
143
+ logger.exception("Failed to capture LangChain LLM call")
144
+
145
+ def on_llm_error(
146
+ self,
147
+ error: BaseException,
148
+ *,
149
+ run_id: UUID,
150
+ parent_run_id: UUID | None = None,
151
+ **kwargs: Any,
152
+ ) -> None:
153
+ """Called when an LLM errors."""
154
+ if self._client is None:
155
+ return
156
+
157
+ start = self._run_starts.pop(str(run_id), None)
158
+ duration_ms = (time.monotonic() - start) * 1000 if start else None
159
+
160
+ try:
161
+ self._client.llm_call(
162
+ self._agent_id,
163
+ model="unknown",
164
+ system="unknown",
165
+ duration_ms=duration_ms,
166
+ error={"type": type(error).__name__, "message": str(error)},
167
+ )
168
+ except Exception:
169
+ logger.exception("Failed to capture LangChain LLM error")
170
+
171
+ # --- Tool callbacks ---
172
+
173
+ def on_tool_start(
174
+ self,
175
+ serialized: dict[str, Any],
176
+ input_str: str,
177
+ *,
178
+ run_id: UUID,
179
+ parent_run_id: UUID | None = None,
180
+ tags: list[str] | None = None,
181
+ metadata: dict[str, Any] | None = None,
182
+ **kwargs: Any,
183
+ ) -> None:
184
+ """Called when a tool starts."""
185
+ self._run_starts[str(run_id)] = time.monotonic()
186
+
187
+ def on_tool_end(
188
+ self,
189
+ output: Any,
190
+ *,
191
+ run_id: UUID,
192
+ parent_run_id: UUID | None = None,
193
+ tags: list[str] | None = None,
194
+ **kwargs: Any,
195
+ ) -> None:
196
+ """Called when a tool finishes. Sends agent.tool_call event."""
197
+ if self._client is None:
198
+ return
199
+
200
+ start = self._run_starts.pop(str(run_id), None)
201
+ duration_ms = (time.monotonic() - start) * 1000 if start else None
202
+
203
+ tool_name = kwargs.get("name", "unknown")
204
+
205
+ data: dict[str, Any] = {
206
+ "tool_name": tool_name,
207
+ "status": "success",
208
+ }
209
+ if duration_ms is not None:
210
+ data["duration_ms"] = duration_ms
211
+ if self._capture_content and output is not None:
212
+ data["output"] = str(output)
213
+
214
+ try:
215
+ self._client.tool_call(self._agent_id, **data)
216
+ except Exception:
217
+ logger.exception("Failed to capture LangChain tool call")
218
+
219
+ def on_tool_error(
220
+ self,
221
+ error: BaseException,
222
+ *,
223
+ run_id: UUID,
224
+ parent_run_id: UUID | None = None,
225
+ **kwargs: Any,
226
+ ) -> None:
227
+ """Called when a tool errors."""
228
+ if self._client is None:
229
+ return
230
+
231
+ start = self._run_starts.pop(str(run_id), None)
232
+ duration_ms = (time.monotonic() - start) * 1000 if start else None
233
+
234
+ try:
235
+ self._client.tool_call(
236
+ self._agent_id,
237
+ tool_name=kwargs.get("name", "unknown"),
238
+ status="error",
239
+ duration_ms=duration_ms,
240
+ error={"type": type(error).__name__, "message": str(error)},
241
+ )
242
+ except Exception:
243
+ logger.exception("Failed to capture LangChain tool error")
244
+
245
+ # --- Retriever callbacks ---
246
+
247
+ def on_retriever_start(
248
+ self,
249
+ serialized: dict[str, Any],
250
+ query: str,
251
+ *,
252
+ run_id: UUID,
253
+ parent_run_id: UUID | None = None,
254
+ **kwargs: Any,
255
+ ) -> None:
256
+ """Called when a retriever starts."""
257
+ self._run_starts[str(run_id)] = time.monotonic()
258
+
259
+ def on_retriever_end(
260
+ self,
261
+ documents: Sequence[Any],
262
+ *,
263
+ run_id: UUID,
264
+ parent_run_id: UUID | None = None,
265
+ **kwargs: Any,
266
+ ) -> None:
267
+ """Called when a retriever finishes. Sends agent.retrieval_query event."""
268
+ if self._client is None:
269
+ return
270
+
271
+ start = self._run_starts.pop(str(run_id), None)
272
+ duration_ms = (time.monotonic() - start) * 1000 if start else None
273
+
274
+ try:
275
+ self._client.retrieval_query(
276
+ self._agent_id,
277
+ source="langchain-retriever",
278
+ results_count=len(documents),
279
+ duration_ms=duration_ms,
280
+ )
281
+ except Exception:
282
+ logger.exception("Failed to capture LangChain retriever call")
283
+
284
+ # --- Chain callbacks (no-ops, used for context) ---
285
+
286
+ def on_chain_start(
287
+ self, serialized: dict[str, Any], inputs: dict[str, Any], **kwargs: Any
288
+ ) -> None:
289
+ pass
290
+
291
+ def on_chain_end(self, outputs: dict[str, Any], **kwargs: Any) -> None:
292
+ pass
293
+
294
+ def on_chain_error(self, error: BaseException, **kwargs: Any) -> None:
295
+ pass
296
+
297
+ # --- Flush ---
298
+
299
+ def flush(self) -> None:
300
+ """Flush pending events."""
301
+ if self._client:
302
+ self._client.flush()
303
+
304
+ def shutdown(self) -> None:
305
+ """Flush and close."""
306
+ if self._client:
307
+ self._client.shutdown()
@@ -0,0 +1,123 @@
1
+ """LiteLLM integration — patches litellm.completion() globally.
2
+
3
+ Usage::
4
+
5
+ from agentos.integrations.litellm import patch_litellm
6
+ patch_litellm(api_key="aos_...", agent_id="my-agent")
7
+
8
+ import litellm
9
+ response = litellm.completion(model="gpt-4o", messages=[...])
10
+ # Auto-captured
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import time
17
+ from typing import Any
18
+
19
+ logger = logging.getLogger("agentos.integrations.litellm")
20
+
21
+ _original_completion: Any = None
22
+
23
+
24
+ def patch_litellm(
25
+ *,
26
+ api_key: str | None = None,
27
+ agent_id: str = "default",
28
+ base_url: str = "https://api.agentos.dev",
29
+ capture_content: bool = True,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ """Patch ``litellm.completion`` to auto-capture LLM calls."""
33
+ global _original_completion
34
+
35
+ try:
36
+ import litellm
37
+ except ImportError:
38
+ logger.error("litellm not installed — cannot patch")
39
+ return
40
+
41
+ from agentos.client import AgentOS, get_client
42
+
43
+ aos_client = None
44
+ if api_key:
45
+ aos_client = AgentOS(api_key=api_key, base_url=base_url, flush_interval=0, **kwargs)
46
+ else:
47
+ aos_client = get_client()
48
+
49
+ if aos_client is None:
50
+ logger.warning("No AgentOS client configured — patch_litellm is a no-op")
51
+ return
52
+
53
+ _original_completion = litellm.completion
54
+
55
+ def patched_completion(*args: Any, **call_kwargs: Any) -> Any:
56
+ model_arg = call_kwargs.get("model", args[0] if args else "unknown")
57
+ messages = call_kwargs.get("messages")
58
+
59
+ start = time.monotonic()
60
+ try:
61
+ response = _original_completion(*args, **call_kwargs)
62
+ duration_ms = (time.monotonic() - start) * 1000
63
+
64
+ # LiteLLM responses follow the OpenAI format
65
+ data: dict[str, Any] = {
66
+ "model": getattr(response, "model", model_arg),
67
+ "system": "litellm",
68
+ "duration_ms": duration_ms,
69
+ }
70
+
71
+ usage = getattr(response, "usage", None)
72
+ if usage:
73
+ data["input_tokens"] = getattr(usage, "prompt_tokens", None)
74
+ data["output_tokens"] = getattr(usage, "completion_tokens", None)
75
+
76
+ choices = getattr(response, "choices", [])
77
+ if choices:
78
+ fr = getattr(choices[0], "finish_reason", None)
79
+ if fr:
80
+ data["finish_reason"] = fr
81
+
82
+ if capture_content and messages:
83
+ data["input"] = [
84
+ {"role": m.get("role", ""), "content": m.get("content")}
85
+ for m in messages
86
+ if isinstance(m, dict)
87
+ ]
88
+
89
+ try:
90
+ aos_client.llm_call(agent_id, **data)
91
+ except Exception:
92
+ logger.exception("Failed to capture LiteLLM call")
93
+
94
+ return response
95
+
96
+ except Exception as exc:
97
+ duration_ms = (time.monotonic() - start) * 1000
98
+ try:
99
+ aos_client.llm_call(
100
+ agent_id,
101
+ model=str(model_arg),
102
+ system="litellm",
103
+ duration_ms=duration_ms,
104
+ error={"type": type(exc).__name__, "message": str(exc)},
105
+ )
106
+ except Exception:
107
+ logger.exception("Failed to capture LiteLLM error")
108
+ raise
109
+
110
+ litellm.completion = patched_completion
111
+
112
+
113
+ def unpatch_litellm() -> None:
114
+ """Restore original ``litellm.completion``."""
115
+ global _original_completion
116
+ if _original_completion is not None:
117
+ try:
118
+ import litellm
119
+
120
+ litellm.completion = _original_completion
121
+ _original_completion = None
122
+ except ImportError:
123
+ pass