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,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
|