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