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,317 @@
|
|
|
1
|
+
"""Langfuse-compatible API shim.
|
|
2
|
+
|
|
3
|
+
Drop-in replacement: change ``from langfuse import Langfuse`` to
|
|
4
|
+
``from agentos.compat.langfuse import Langfuse``. All existing code works.
|
|
5
|
+
|
|
6
|
+
Maps Langfuse primitives to AgentOS events:
|
|
7
|
+
- trace() → TraceContext
|
|
8
|
+
- generation() → agent.llm_call
|
|
9
|
+
- span() → child span
|
|
10
|
+
- score() → agent.eval
|
|
11
|
+
- event() → agent.business_event
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from agentos.client import AgentOS
|
|
20
|
+
from agentos.tracing import (
|
|
21
|
+
generate_span_id,
|
|
22
|
+
generate_trace_id,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StatefulGenerationClient:
|
|
27
|
+
"""Represents a Langfuse generation (LLM call)."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: AgentOS,
|
|
32
|
+
trace_ctx: _TraceShim,
|
|
33
|
+
*,
|
|
34
|
+
name: str | None = None,
|
|
35
|
+
model: str | None = None,
|
|
36
|
+
model_parameters: dict[str, Any] | None = None,
|
|
37
|
+
input: Any | None = None,
|
|
38
|
+
output: Any | None = None,
|
|
39
|
+
usage: dict[str, Any] | None = None,
|
|
40
|
+
metadata: dict[str, Any] | None = None,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._client = client
|
|
44
|
+
self._trace = trace_ctx
|
|
45
|
+
self._span_id = generate_span_id()
|
|
46
|
+
self._name = name
|
|
47
|
+
self._model = model or "unknown"
|
|
48
|
+
self._model_parameters = model_parameters or {}
|
|
49
|
+
self._input = input
|
|
50
|
+
self._output = output
|
|
51
|
+
self._usage = usage or {}
|
|
52
|
+
self._metadata = metadata or {}
|
|
53
|
+
self._ended = False
|
|
54
|
+
|
|
55
|
+
def end(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
output: Any | None = None,
|
|
59
|
+
usage: dict[str, Any] | None = None,
|
|
60
|
+
metadata: dict[str, Any] | None = None,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""End the generation and send the event."""
|
|
64
|
+
if output is not None:
|
|
65
|
+
self._output = output
|
|
66
|
+
if usage:
|
|
67
|
+
self._usage.update(usage)
|
|
68
|
+
if metadata:
|
|
69
|
+
self._metadata.update(metadata)
|
|
70
|
+
self._flush_event()
|
|
71
|
+
self._ended = True
|
|
72
|
+
|
|
73
|
+
def _flush_event(self) -> None:
|
|
74
|
+
"""Send the llm_call event."""
|
|
75
|
+
props: dict[str, Any] = {
|
|
76
|
+
"model": self._model,
|
|
77
|
+
"system": "unknown", # Langfuse doesn't require provider
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if self._usage:
|
|
81
|
+
if "input" in self._usage:
|
|
82
|
+
props["input_tokens"] = self._usage["input"]
|
|
83
|
+
elif "promptTokens" in self._usage:
|
|
84
|
+
props["input_tokens"] = self._usage["promptTokens"]
|
|
85
|
+
if "output" in self._usage:
|
|
86
|
+
props["output_tokens"] = self._usage["output"]
|
|
87
|
+
elif "completionTokens" in self._usage:
|
|
88
|
+
props["output_tokens"] = self._usage["completionTokens"]
|
|
89
|
+
if "total" in self._usage:
|
|
90
|
+
props["total_tokens"] = self._usage["total"]
|
|
91
|
+
|
|
92
|
+
if self._model_parameters.get("temperature") is not None:
|
|
93
|
+
props["temperature"] = self._model_parameters["temperature"]
|
|
94
|
+
if self._model_parameters.get("max_tokens") is not None:
|
|
95
|
+
props["max_tokens"] = self._model_parameters["max_tokens"]
|
|
96
|
+
|
|
97
|
+
if self._input is not None:
|
|
98
|
+
if isinstance(self._input, list):
|
|
99
|
+
props["input"] = self._input
|
|
100
|
+
else:
|
|
101
|
+
props["input"] = [{"role": "user", "content": str(self._input)}]
|
|
102
|
+
|
|
103
|
+
if self._output is not None:
|
|
104
|
+
if isinstance(self._output, list):
|
|
105
|
+
props["output"] = self._output
|
|
106
|
+
else:
|
|
107
|
+
props["output"] = [{"role": "assistant", "content": str(self._output)}]
|
|
108
|
+
|
|
109
|
+
self._client.llm_call(
|
|
110
|
+
self._trace._agent_id,
|
|
111
|
+
trace_id=self._trace._trace_id,
|
|
112
|
+
span_id=self._span_id,
|
|
113
|
+
parent_span_id=self._trace._current_span_id,
|
|
114
|
+
user_id=self._trace._user_id,
|
|
115
|
+
session_id=self._trace._session_id,
|
|
116
|
+
**props,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def __del__(self) -> None:
|
|
120
|
+
if not self._ended:
|
|
121
|
+
with contextlib.suppress(Exception):
|
|
122
|
+
self._flush_event()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StatefulSpanClient:
|
|
126
|
+
"""Represents a Langfuse span."""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
client: AgentOS,
|
|
131
|
+
trace_ctx: _TraceShim,
|
|
132
|
+
*,
|
|
133
|
+
name: str | None = None,
|
|
134
|
+
input: Any | None = None,
|
|
135
|
+
output: Any | None = None,
|
|
136
|
+
metadata: dict[str, Any] | None = None,
|
|
137
|
+
**kwargs: Any,
|
|
138
|
+
) -> None:
|
|
139
|
+
self._client = client
|
|
140
|
+
self._trace = trace_ctx
|
|
141
|
+
self._span_id = generate_span_id()
|
|
142
|
+
self._parent_span_id = trace_ctx._current_span_id
|
|
143
|
+
self._name = name
|
|
144
|
+
self._input = input
|
|
145
|
+
self._output = output
|
|
146
|
+
self._metadata = metadata or {}
|
|
147
|
+
|
|
148
|
+
def end(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
output: Any | None = None,
|
|
152
|
+
metadata: dict[str, Any] | None = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""End the span."""
|
|
156
|
+
if output is not None:
|
|
157
|
+
self._output = output
|
|
158
|
+
if metadata:
|
|
159
|
+
self._metadata.update(metadata)
|
|
160
|
+
|
|
161
|
+
def generation(self, **kwargs: Any) -> StatefulGenerationClient:
|
|
162
|
+
"""Create a nested generation within this span."""
|
|
163
|
+
return StatefulGenerationClient(
|
|
164
|
+
self._client,
|
|
165
|
+
self._trace,
|
|
166
|
+
**kwargs,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def span(self, **kwargs: Any) -> StatefulSpanClient:
|
|
170
|
+
"""Create a nested span."""
|
|
171
|
+
return StatefulSpanClient(self._client, self._trace, **kwargs)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _TraceShim:
|
|
175
|
+
"""Internal shim representing a Langfuse trace."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
client: AgentOS,
|
|
180
|
+
*,
|
|
181
|
+
id: str | None = None,
|
|
182
|
+
name: str | None = None,
|
|
183
|
+
user_id: str | None = None,
|
|
184
|
+
session_id: str | None = None,
|
|
185
|
+
metadata: dict[str, Any] | None = None,
|
|
186
|
+
tags: list[str] | None = None,
|
|
187
|
+
input: Any | None = None,
|
|
188
|
+
output: Any | None = None,
|
|
189
|
+
**kwargs: Any,
|
|
190
|
+
) -> None:
|
|
191
|
+
self._client = client
|
|
192
|
+
self._trace_id = id or generate_trace_id()
|
|
193
|
+
self._agent_id = name or "langfuse-trace"
|
|
194
|
+
self._user_id = user_id
|
|
195
|
+
self._session_id = session_id
|
|
196
|
+
self._metadata = metadata or {}
|
|
197
|
+
self._current_span_id = generate_span_id() # root span
|
|
198
|
+
|
|
199
|
+
def generation(self, **kwargs: Any) -> StatefulGenerationClient:
|
|
200
|
+
"""Create a generation (LLM call) on this trace."""
|
|
201
|
+
return StatefulGenerationClient(self._client, self, **kwargs)
|
|
202
|
+
|
|
203
|
+
def span(self, **kwargs: Any) -> StatefulSpanClient:
|
|
204
|
+
"""Create a span on this trace."""
|
|
205
|
+
return StatefulSpanClient(self._client, self, **kwargs)
|
|
206
|
+
|
|
207
|
+
def score(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
name: str,
|
|
211
|
+
value: float,
|
|
212
|
+
data_type: str | None = None,
|
|
213
|
+
comment: str | None = None,
|
|
214
|
+
**kwargs: Any,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Attach a score to this trace."""
|
|
217
|
+
self._client.eval(
|
|
218
|
+
self._agent_id,
|
|
219
|
+
eval_name=name,
|
|
220
|
+
score=value,
|
|
221
|
+
trace_id=self._trace_id,
|
|
222
|
+
span_id=self._current_span_id,
|
|
223
|
+
user_id=self._user_id,
|
|
224
|
+
session_id=self._session_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def event(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
name: str,
|
|
231
|
+
input: Any | None = None,
|
|
232
|
+
output: Any | None = None,
|
|
233
|
+
metadata: dict[str, Any] | None = None,
|
|
234
|
+
**kwargs: Any,
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Create a lightweight event on this trace."""
|
|
237
|
+
self._client.business_event(
|
|
238
|
+
self._agent_id,
|
|
239
|
+
event_name=name,
|
|
240
|
+
trace_id=self._trace_id,
|
|
241
|
+
span_id=self._current_span_id,
|
|
242
|
+
user_id=self._user_id,
|
|
243
|
+
session_id=self._session_id,
|
|
244
|
+
metadata=metadata,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Langfuse:
|
|
249
|
+
"""Langfuse-compatible client. Drop-in replacement.
|
|
250
|
+
|
|
251
|
+
Usage::
|
|
252
|
+
|
|
253
|
+
from agentos.compat.langfuse import Langfuse
|
|
254
|
+
|
|
255
|
+
langfuse = Langfuse(public_key="aos_...")
|
|
256
|
+
trace = langfuse.trace(name="my-trace")
|
|
257
|
+
gen = trace.generation(model="gpt-4o", usage={"input": 100, "output": 50})
|
|
258
|
+
trace.score(name="quality", value=0.95)
|
|
259
|
+
langfuse.flush()
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
public_key: str | None = None,
|
|
265
|
+
secret_key: str | None = None, # accepted but ignored
|
|
266
|
+
host: str | None = None,
|
|
267
|
+
*,
|
|
268
|
+
release: str | None = None,
|
|
269
|
+
debug: bool = False,
|
|
270
|
+
threads: int = 1,
|
|
271
|
+
flush_at: int = 15,
|
|
272
|
+
flush_interval: float = 0.5,
|
|
273
|
+
enabled: bool = True,
|
|
274
|
+
**kwargs: Any,
|
|
275
|
+
) -> None:
|
|
276
|
+
api_key = public_key or ""
|
|
277
|
+
base_url = host or "https://api.agentos.dev"
|
|
278
|
+
|
|
279
|
+
self._client = AgentOS(
|
|
280
|
+
api_key=api_key,
|
|
281
|
+
base_url=base_url,
|
|
282
|
+
batch_size=flush_at,
|
|
283
|
+
flush_interval=flush_interval,
|
|
284
|
+
enabled=enabled,
|
|
285
|
+
debug=debug,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def trace(self, **kwargs: Any) -> _TraceShim:
|
|
289
|
+
"""Create a new trace."""
|
|
290
|
+
return _TraceShim(self._client, **kwargs)
|
|
291
|
+
|
|
292
|
+
def score(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
trace_id: str | None = None,
|
|
296
|
+
observation_id: str | None = None,
|
|
297
|
+
name: str,
|
|
298
|
+
value: float,
|
|
299
|
+
data_type: str | None = None,
|
|
300
|
+
comment: str | None = None,
|
|
301
|
+
**kwargs: Any,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Create a score (standalone, not attached to a trace object)."""
|
|
304
|
+
self._client.eval(
|
|
305
|
+
"langfuse-score",
|
|
306
|
+
eval_name=name,
|
|
307
|
+
score=value,
|
|
308
|
+
trace_id=trace_id or generate_trace_id(),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def flush(self) -> None:
|
|
312
|
+
"""Flush all pending events."""
|
|
313
|
+
self._client.flush()
|
|
314
|
+
|
|
315
|
+
def shutdown(self) -> None:
|
|
316
|
+
"""Flush and close."""
|
|
317
|
+
self._client.shutdown()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""LangSmith-compatible API shim.
|
|
2
|
+
|
|
3
|
+
Provides ``@traceable`` decorator and ``Client`` with feedback API.
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from agentos.compat.langsmith import traceable, Client, wrap_openai
|
|
8
|
+
|
|
9
|
+
@traceable(name="my-chain", run_type="chain")
|
|
10
|
+
def my_chain(input: str) -> str: ...
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any, TypeVar
|
|
17
|
+
|
|
18
|
+
from agentos.decorators import observe
|
|
19
|
+
from agentos.integrations.openai import wrap_openai as _native_wrap_openai
|
|
20
|
+
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def traceable(
|
|
25
|
+
*,
|
|
26
|
+
name: str | None = None,
|
|
27
|
+
run_type: str | None = None,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
) -> Callable[[F], F]:
|
|
30
|
+
"""LangSmith-compatible ``@traceable`` decorator.
|
|
31
|
+
|
|
32
|
+
Maps ``run_type`` to AgentOS observation type:
|
|
33
|
+
- ``"llm"`` → ``@observe(as_type="generation")``
|
|
34
|
+
- ``"chain"`` / ``"tool"`` / None → ``@observe()``
|
|
35
|
+
"""
|
|
36
|
+
as_type = "generation" if run_type == "llm" else None
|
|
37
|
+
return observe(name=name, as_type=as_type)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def wrap_openai(openai_client: Any, **kwargs: Any) -> Any:
|
|
41
|
+
"""LangSmith-compatible ``wrap_openai``."""
|
|
42
|
+
return _native_wrap_openai(openai_client, **kwargs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Client:
|
|
46
|
+
"""LangSmith-compatible Client with feedback API.
|
|
47
|
+
|
|
48
|
+
Usage::
|
|
49
|
+
|
|
50
|
+
client = Client(api_key="aos_...")
|
|
51
|
+
client.create_feedback(run_id="...", key="quality", score=0.9)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, api_key: str | None = None, **kwargs: Any) -> None:
|
|
55
|
+
from agentos.client import AgentOS, get_client
|
|
56
|
+
|
|
57
|
+
if api_key:
|
|
58
|
+
self._client = AgentOS(api_key=api_key, flush_interval=0, **kwargs)
|
|
59
|
+
else:
|
|
60
|
+
self._client = get_client()
|
|
61
|
+
|
|
62
|
+
def create_feedback(
|
|
63
|
+
self,
|
|
64
|
+
run_id: str,
|
|
65
|
+
key: str,
|
|
66
|
+
*,
|
|
67
|
+
score: float | None = None,
|
|
68
|
+
value: Any | None = None,
|
|
69
|
+
comment: str | None = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Create feedback (maps to agent.eval event)."""
|
|
73
|
+
if self._client is None:
|
|
74
|
+
return
|
|
75
|
+
self._client.eval(
|
|
76
|
+
"langsmith-feedback",
|
|
77
|
+
eval_name=key,
|
|
78
|
+
score=score if score is not None else (1.0 if value else 0.0),
|
|
79
|
+
)
|
agentos/compat/otel.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""OpenTelemetry / Arize Phoenix compatibility — OTLP SpanExporter.
|
|
2
|
+
|
|
3
|
+
For Phoenix users: swap the OTLP endpoint to AgentOS.
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from agentos.compat.otel import register
|
|
8
|
+
|
|
9
|
+
tracer_provider = register(
|
|
10
|
+
project_name="my-project",
|
|
11
|
+
endpoint="https://ingest.agentos.dev/v1/otlp",
|
|
12
|
+
api_key="aos_...",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from openinference.instrumentation.openai import OpenAIInstrumentor
|
|
16
|
+
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("agentos.compat.otel")
|
|
26
|
+
|
|
27
|
+
# OpenInference span kind → AgentOS event type
|
|
28
|
+
_SPAN_KIND_MAP = {
|
|
29
|
+
"LLM": "agent.llm_call",
|
|
30
|
+
"TOOL": "agent.tool_call",
|
|
31
|
+
"CHAIN": None, # parent context only
|
|
32
|
+
"AGENT": None,
|
|
33
|
+
"RETRIEVER": "agent.retrieval_query",
|
|
34
|
+
"EMBEDDING": "agent.tool_call",
|
|
35
|
+
"RERANKER": "agent.tool_call",
|
|
36
|
+
"GUARDRAIL": "agent.security_alert",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# OpenInference attribute → AgentOS property
|
|
40
|
+
_ATTR_MAP = {
|
|
41
|
+
"llm.model_name": "gen_ai.request.model",
|
|
42
|
+
"llm.token_count.prompt": "gen_ai.usage.input_tokens",
|
|
43
|
+
"llm.token_count.completion": "gen_ai.usage.output_tokens",
|
|
44
|
+
"llm.token_count.total": "gen_ai.usage.total_tokens",
|
|
45
|
+
"input.value": "input",
|
|
46
|
+
"output.value": "output",
|
|
47
|
+
"llm.invocation_parameters": None, # handled specially
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register(
|
|
52
|
+
*,
|
|
53
|
+
project_name: str = "default",
|
|
54
|
+
endpoint: str = "https://ingest.agentos.dev/v1/otlp",
|
|
55
|
+
api_key: str | None = None,
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""Register an OTEL TracerProvider that exports to AgentOS.
|
|
58
|
+
|
|
59
|
+
Requires ``opentelemetry-sdk`` to be installed.
|
|
60
|
+
Returns a ``TracerProvider`` compatible with OpenInference instrumentors.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
64
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
65
|
+
except ImportError as err:
|
|
66
|
+
raise ImportError(
|
|
67
|
+
"OpenTelemetry SDK required. Install with: pip install agentos-python[otel]"
|
|
68
|
+
) from err
|
|
69
|
+
|
|
70
|
+
exporter = AgentOSSpanExporter(
|
|
71
|
+
endpoint=endpoint,
|
|
72
|
+
api_key=api_key or "",
|
|
73
|
+
project_name=project_name,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
provider = TracerProvider()
|
|
77
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
78
|
+
|
|
79
|
+
return provider
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AgentOSSpanExporter:
|
|
83
|
+
"""Custom OTEL SpanExporter that converts spans to AgentOS events."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
endpoint: str,
|
|
88
|
+
api_key: str,
|
|
89
|
+
project_name: str = "default",
|
|
90
|
+
) -> None:
|
|
91
|
+
from agentos.client import AgentOS
|
|
92
|
+
|
|
93
|
+
self._client = AgentOS(
|
|
94
|
+
api_key=api_key,
|
|
95
|
+
base_url=endpoint.replace("/v1/otlp", ""),
|
|
96
|
+
flush_interval=1.0,
|
|
97
|
+
)
|
|
98
|
+
self._project_name = project_name
|
|
99
|
+
|
|
100
|
+
def export(self, spans: Sequence[Any]) -> Any:
|
|
101
|
+
"""Convert OTEL spans to AgentOS events and enqueue."""
|
|
102
|
+
try:
|
|
103
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
104
|
+
except ImportError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
for otel_span in spans:
|
|
108
|
+
try:
|
|
109
|
+
self._export_span(otel_span)
|
|
110
|
+
except Exception:
|
|
111
|
+
logger.exception("Failed to export span")
|
|
112
|
+
|
|
113
|
+
return SpanExportResult.SUCCESS
|
|
114
|
+
|
|
115
|
+
def _export_span(self, otel_span: Any) -> None:
|
|
116
|
+
"""Convert a single OTEL span to an AgentOS event."""
|
|
117
|
+
attrs = dict(otel_span.attributes or {})
|
|
118
|
+
|
|
119
|
+
# Determine event type from OpenInference span kind
|
|
120
|
+
span_kind = attrs.get("openinference.span.kind", "CHAIN")
|
|
121
|
+
event_type = _SPAN_KIND_MAP.get(str(span_kind).upper())
|
|
122
|
+
|
|
123
|
+
if event_type is None:
|
|
124
|
+
return # Skip CHAIN/AGENT spans (they're just context)
|
|
125
|
+
|
|
126
|
+
# Build properties
|
|
127
|
+
props: dict[str, Any] = {}
|
|
128
|
+
for otel_key, aos_key in _ATTR_MAP.items():
|
|
129
|
+
if otel_key in attrs and aos_key:
|
|
130
|
+
props[aos_key] = attrs[otel_key]
|
|
131
|
+
|
|
132
|
+
# Map to specific event type
|
|
133
|
+
if event_type == "agent.llm_call":
|
|
134
|
+
props.setdefault("gen_ai.system", "unknown")
|
|
135
|
+
props.setdefault("gen_ai.request.model", "unknown")
|
|
136
|
+
self._client.llm_call(
|
|
137
|
+
self._project_name,
|
|
138
|
+
model=props.pop("gen_ai.request.model"),
|
|
139
|
+
system=props.pop("gen_ai.system"),
|
|
140
|
+
trace_id=format(otel_span.context.trace_id, "032x"),
|
|
141
|
+
span_id=format(otel_span.context.span_id, "016x"),
|
|
142
|
+
**{k: v for k, v in props.items() if not k.startswith("gen_ai.")},
|
|
143
|
+
**{k: v for k, v in props.items() if k.startswith("gen_ai.")},
|
|
144
|
+
)
|
|
145
|
+
elif event_type == "agent.tool_call":
|
|
146
|
+
tool_name = attrs.get("tool.name", otel_span.name or "unknown")
|
|
147
|
+
self._client.tool_call(
|
|
148
|
+
self._project_name,
|
|
149
|
+
tool_name=str(tool_name),
|
|
150
|
+
trace_id=format(otel_span.context.trace_id, "032x"),
|
|
151
|
+
span_id=format(otel_span.context.span_id, "016x"),
|
|
152
|
+
)
|
|
153
|
+
elif event_type == "agent.retrieval_query":
|
|
154
|
+
source = attrs.get("retrieval.source", "unknown")
|
|
155
|
+
self._client.retrieval_query(
|
|
156
|
+
self._project_name,
|
|
157
|
+
source=str(source),
|
|
158
|
+
trace_id=format(otel_span.context.trace_id, "032x"),
|
|
159
|
+
span_id=format(otel_span.context.span_id, "016x"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def shutdown(self) -> None:
|
|
163
|
+
self._client.shutdown()
|
|
164
|
+
|
|
165
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
166
|
+
self._client.flush()
|
|
167
|
+
return True
|
agentos/compat/weave.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""W&B Weave-compatible API shim.
|
|
2
|
+
|
|
3
|
+
Provides ``init``, ``@op()``, and ``Evaluation``.
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from agentos.compat import weave
|
|
8
|
+
weave.init("my-project", api_key="aos_...")
|
|
9
|
+
|
|
10
|
+
@weave.op()
|
|
11
|
+
def call_llm(prompt: str) -> str: ...
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any, TypeVar
|
|
18
|
+
|
|
19
|
+
from agentos.client import AgentOS, get_client
|
|
20
|
+
from agentos.decorators import observe
|
|
21
|
+
|
|
22
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init(project: str, *, api_key: str | None = None, **kwargs: Any) -> None:
|
|
26
|
+
"""Initialize Weave-compatible tracing."""
|
|
27
|
+
import agentos.client
|
|
28
|
+
|
|
29
|
+
client = AgentOS(api_key=api_key or "", **kwargs)
|
|
30
|
+
agentos.client._global_client = client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def op(**kwargs: Any) -> Callable[[F], F]:
|
|
34
|
+
"""Weave-compatible ``@op()`` decorator. Equivalent to ``@observe()``."""
|
|
35
|
+
return observe(**kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Evaluation:
|
|
39
|
+
"""Weave-compatible evaluation runner.
|
|
40
|
+
|
|
41
|
+
Usage::
|
|
42
|
+
|
|
43
|
+
evaluation = Evaluation(
|
|
44
|
+
dataset=[{"input": "2+2?", "expected": "4"}],
|
|
45
|
+
scorers=[accuracy_scorer],
|
|
46
|
+
)
|
|
47
|
+
await evaluation.evaluate(my_pipeline)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
dataset: list[dict[str, Any]],
|
|
54
|
+
scorers: list[Callable[..., Any]],
|
|
55
|
+
) -> None:
|
|
56
|
+
self._dataset = dataset
|
|
57
|
+
self._scorers = scorers
|
|
58
|
+
|
|
59
|
+
async def evaluate(self, fn: Callable[..., Any]) -> list[dict[str, Any]]:
|
|
60
|
+
"""Run evaluation and send score events."""
|
|
61
|
+
import inspect
|
|
62
|
+
|
|
63
|
+
client = get_client()
|
|
64
|
+
results = []
|
|
65
|
+
|
|
66
|
+
for item in self._dataset:
|
|
67
|
+
input_val = item.get("input")
|
|
68
|
+
expected = item.get("expected")
|
|
69
|
+
|
|
70
|
+
if inspect.iscoroutinefunction(fn):
|
|
71
|
+
output = await fn(input_val)
|
|
72
|
+
else:
|
|
73
|
+
output = fn(input_val)
|
|
74
|
+
|
|
75
|
+
for scorer in self._scorers:
|
|
76
|
+
if inspect.iscoroutinefunction(scorer):
|
|
77
|
+
score = await scorer(output, expected)
|
|
78
|
+
else:
|
|
79
|
+
score = scorer(output, expected)
|
|
80
|
+
|
|
81
|
+
if client:
|
|
82
|
+
client.eval(
|
|
83
|
+
"weave-eval",
|
|
84
|
+
eval_name=scorer.__name__,
|
|
85
|
+
score=float(score),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
results.append({"input": input_val, "output": output, "score": score})
|
|
89
|
+
|
|
90
|
+
if client:
|
|
91
|
+
client.flush()
|
|
92
|
+
|
|
93
|
+
return results
|
agentos/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Configuration for the Agent OS SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass
|
|
9
|
+
class AgentOSConfig:
|
|
10
|
+
"""SDK configuration.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
api_key: Agent OS API key (format: ``aos_...``).
|
|
14
|
+
base_url: Ingestion API base URL.
|
|
15
|
+
batch_size: Max events per batch before auto-flush.
|
|
16
|
+
flush_interval: Seconds between periodic flushes.
|
|
17
|
+
max_retries: Max retry attempts for failed HTTP requests.
|
|
18
|
+
capture_content: If False, strips message content from LLM and tool events.
|
|
19
|
+
enabled: Kill switch — when False, all capture calls are no-ops.
|
|
20
|
+
environment: Environment tag (e.g. production, staging, development).
|
|
21
|
+
debug: If True, log SDK internals to stderr.
|
|
22
|
+
max_queue_size: Max events in the queue before dropping oldest.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
api_key: str = ""
|
|
26
|
+
base_url: str = "https://api.agentos.dev"
|
|
27
|
+
batch_size: int = 50
|
|
28
|
+
flush_interval: float = 5.0
|
|
29
|
+
max_retries: int = 3
|
|
30
|
+
capture_content: bool = True
|
|
31
|
+
enabled: bool = True
|
|
32
|
+
environment: str | None = None
|
|
33
|
+
debug: bool = False
|
|
34
|
+
max_queue_size: int = 10_000
|