infralo 0.1.0.dev1__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.
- infralo/__init__.py +9 -0
- infralo/_models.py +84 -0
- infralo/client.py +251 -0
- infralo/exporter.py +154 -0
- infralo/integrations/__init__.py +23 -0
- infralo/integrations/agno.py +381 -0
- infralo/integrations/crewai.py +337 -0
- infralo/integrations/google_adk.py +238 -0
- infralo/integrations/langgraph.py +492 -0
- infralo/span.py +226 -0
- infralo/trace.py +457 -0
- infralo-0.1.0.dev1.dist-info/METADATA +480 -0
- infralo-0.1.0.dev1.dist-info/RECORD +14 -0
- infralo-0.1.0.dev1.dist-info/WHEEL +4 -0
infralo/__init__.py
ADDED
infralo/_models.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infralo — Internal span data model.
|
|
3
|
+
|
|
4
|
+
ToolSpanData mirrors the ToolCallSpanCreate API schema and is serialised
|
|
5
|
+
to JSON before submission to the /api/v1/traces/spans/tool-call endpoint.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_PREVIEW_MAX_CHARS = 500
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _to_preview(value: Any) -> tuple[str, bool]:
|
|
18
|
+
"""Return (preview_str, is_truncated) for any value."""
|
|
19
|
+
text = value if isinstance(value, str) else str(value)
|
|
20
|
+
if len(text) > _PREVIEW_MAX_CHARS:
|
|
21
|
+
return text[:_PREVIEW_MAX_CHARS], True
|
|
22
|
+
return text, False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _isoformat(dt: datetime) -> str:
|
|
26
|
+
if dt.tzinfo is None:
|
|
27
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
28
|
+
return dt.isoformat()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ToolSpanData:
|
|
33
|
+
"""
|
|
34
|
+
Internal representation of one tool call span.
|
|
35
|
+
Serialises to the ToolCallSpanCreate API body shape.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
trace_id: str
|
|
39
|
+
span_id: str
|
|
40
|
+
parent_span_id: str
|
|
41
|
+
sequence_number: int
|
|
42
|
+
tool_name: str
|
|
43
|
+
tool_call_id: str
|
|
44
|
+
start_time: datetime
|
|
45
|
+
end_time: datetime
|
|
46
|
+
|
|
47
|
+
session_id: str = ""
|
|
48
|
+
status: str = "success" # "success" | "error" | "timeout"
|
|
49
|
+
error_code: str | None = None
|
|
50
|
+
error_message: str | None = None
|
|
51
|
+
|
|
52
|
+
input_preview: str = ""
|
|
53
|
+
is_input_truncated: bool = False
|
|
54
|
+
output_preview: str = ""
|
|
55
|
+
is_output_truncated: bool = False
|
|
56
|
+
|
|
57
|
+
input_payload: dict | None = None
|
|
58
|
+
output_payload: Any = None
|
|
59
|
+
|
|
60
|
+
metadata: dict = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
"""Serialise to the ToolCallSpanCreate JSON shape."""
|
|
64
|
+
return {
|
|
65
|
+
"trace_id": self.trace_id,
|
|
66
|
+
"span_id": self.span_id,
|
|
67
|
+
"parent_span_id": self.parent_span_id,
|
|
68
|
+
"sequence_number": self.sequence_number,
|
|
69
|
+
"tool_name": self.tool_name,
|
|
70
|
+
"tool_call_id": self.tool_call_id,
|
|
71
|
+
"start_time": _isoformat(self.start_time),
|
|
72
|
+
"end_time": _isoformat(self.end_time),
|
|
73
|
+
"session_id": self.session_id,
|
|
74
|
+
"status": self.status,
|
|
75
|
+
"error_code": self.error_code,
|
|
76
|
+
"error_message": self.error_message,
|
|
77
|
+
"input_preview": self.input_preview,
|
|
78
|
+
"is_input_truncated": self.is_input_truncated,
|
|
79
|
+
"output_preview": self.output_preview,
|
|
80
|
+
"is_output_truncated": self.is_output_truncated,
|
|
81
|
+
"input_payload": self.input_payload,
|
|
82
|
+
"output_payload": self.output_payload,
|
|
83
|
+
"metadata": self.metadata,
|
|
84
|
+
}
|
infralo/client.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infralo — Infralo client: main entry point.
|
|
3
|
+
|
|
4
|
+
from infralo import Infralo
|
|
5
|
+
|
|
6
|
+
infralo = Infralo(api_key="vk_your_key")
|
|
7
|
+
|
|
8
|
+
with infralo.start_trace(session_id="user-123") as trace:
|
|
9
|
+
...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import atexit
|
|
16
|
+
import functools
|
|
17
|
+
import inspect
|
|
18
|
+
import uuid
|
|
19
|
+
from typing import Any, Callable, TypeVar
|
|
20
|
+
|
|
21
|
+
from infralo.exporter import SpanExporter
|
|
22
|
+
from infralo.trace import Trace, _active_trace
|
|
23
|
+
|
|
24
|
+
F = TypeVar("F", bound=Callable)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Infralo:
|
|
28
|
+
"""
|
|
29
|
+
Main Infralo SDK client.
|
|
30
|
+
|
|
31
|
+
Create one instance per application (typically at module level) and reuse it.
|
|
32
|
+
The background exporter starts immediately and flushes remaining spans on exit.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
api_key: Your Infralo Virtual API Key (starts with vk_).
|
|
36
|
+
endpoint: Base URL of your Infralo gateway. Default: https://api.infralo.com
|
|
37
|
+
flush_interval: Seconds between background flush cycles. Default: 5.0
|
|
38
|
+
max_batch_size: Max spans per HTTP request. Default: 50
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
api_key: str,
|
|
44
|
+
endpoint: str = "https://api.infralo.com",
|
|
45
|
+
flush_interval: float = 5.0,
|
|
46
|
+
max_batch_size: int = 50,
|
|
47
|
+
max_queue_size: int = 10000,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._exporter = SpanExporter(
|
|
50
|
+
api_key=api_key,
|
|
51
|
+
endpoint=endpoint,
|
|
52
|
+
flush_interval=flush_interval,
|
|
53
|
+
max_batch_size=max_batch_size,
|
|
54
|
+
max_queue_size=max_queue_size,
|
|
55
|
+
)
|
|
56
|
+
# Guaranteed final flush when the process exits (scripts, workers, servers)
|
|
57
|
+
atexit.register(self._exporter.flush)
|
|
58
|
+
|
|
59
|
+
# ── Trace factory ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def start_trace(
|
|
62
|
+
self,
|
|
63
|
+
trace_id: str | None = None,
|
|
64
|
+
session_id: str = "",
|
|
65
|
+
) -> Trace:
|
|
66
|
+
"""
|
|
67
|
+
Create a new Trace for one agentic chain run.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
trace_id: Shared across the entire chain. Auto-generated UUID if omitted.
|
|
71
|
+
Pass the same trace_id on every LLM gateway call via the
|
|
72
|
+
x-infralo-trace-id header.
|
|
73
|
+
session_id: Optional user/session identifier for grouping traces
|
|
74
|
+
(e.g., a user_id, conversation_id).
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
with infralo.start_trace(session_id="user-123") as trace:
|
|
78
|
+
response = client.chat.completions.create(
|
|
79
|
+
model="my-deployment",
|
|
80
|
+
messages=[...],
|
|
81
|
+
extra_headers={"x-infralo-trace-id": trace.trace_id},
|
|
82
|
+
)
|
|
83
|
+
trace.update_from_response(response)
|
|
84
|
+
...
|
|
85
|
+
"""
|
|
86
|
+
return Trace(
|
|
87
|
+
trace_id=trace_id or str(uuid.uuid4()),
|
|
88
|
+
session_id=session_id,
|
|
89
|
+
exporter=self._exporter,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# ── @tool decorator ──────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def tool(
|
|
95
|
+
self,
|
|
96
|
+
fn: F | None = None,
|
|
97
|
+
*,
|
|
98
|
+
name: str | None = None,
|
|
99
|
+
capture_input: bool = True,
|
|
100
|
+
capture_output: bool = True,
|
|
101
|
+
) -> F | Callable[[F], F]:
|
|
102
|
+
"""
|
|
103
|
+
Decorator that automatically wraps a sync or async function as a tool span.
|
|
104
|
+
|
|
105
|
+
Reads the active Trace from contextvars — must be called inside a
|
|
106
|
+
`with infralo.start_trace(...):` block. Works transparently with both
|
|
107
|
+
sync and async functions.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
name: Override the span name (defaults to the function's __name__).
|
|
111
|
+
capture_input: Auto-capture all function kwargs as input_payload
|
|
112
|
+
and input_preview. Default True.
|
|
113
|
+
Set False to avoid storing sensitive arguments (e.g., passwords).
|
|
114
|
+
capture_output: Auto-capture the return value as output_payload
|
|
115
|
+
and output_preview. Default True.
|
|
116
|
+
Set False to avoid storing sensitive outputs.
|
|
117
|
+
|
|
118
|
+
Metadata:
|
|
119
|
+
Pass _infralo_metadata={...} as a keyword argument to the decorated
|
|
120
|
+
function to attach custom metadata to the span:
|
|
121
|
+
result = my_tool(query="test", _infralo_metadata={"version": "2"})
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
@infralo.tool
|
|
125
|
+
def search_web(query: str) -> list[dict]:
|
|
126
|
+
return search_api(query)
|
|
127
|
+
|
|
128
|
+
@infralo.tool(name="custom_name", capture_input=False)
|
|
129
|
+
async def search_web(query: str) -> list[dict]:
|
|
130
|
+
return await async_search_api(query)
|
|
131
|
+
|
|
132
|
+
Parallel usage (inside `async with trace.parallel():`):
|
|
133
|
+
async with trace.parallel():
|
|
134
|
+
results = await asyncio.gather(
|
|
135
|
+
search_web("q1"), # @infralo.tool — shares seq number
|
|
136
|
+
fetch_doc("url"), # @infralo.tool — shares seq number
|
|
137
|
+
)
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def decorator(func: F) -> F:
|
|
141
|
+
tool_name = name or func.__name__
|
|
142
|
+
|
|
143
|
+
def _build_input(args: tuple, kwargs: dict) -> dict | None:
|
|
144
|
+
if not capture_input:
|
|
145
|
+
return None
|
|
146
|
+
try:
|
|
147
|
+
sig = inspect.signature(func)
|
|
148
|
+
bound = sig.bind(*args, **kwargs)
|
|
149
|
+
bound.apply_defaults()
|
|
150
|
+
# Exclude private infralo kwargs
|
|
151
|
+
return {
|
|
152
|
+
k: v for k, v in bound.arguments.items() if not k.startswith("_infralo_")
|
|
153
|
+
}
|
|
154
|
+
except Exception:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _pop_infralo_kwargs(kwargs: dict) -> dict:
|
|
158
|
+
"""Extract and remove _infralo_* kwargs before calling the real function."""
|
|
159
|
+
return {k: kwargs.pop(k) for k in list(kwargs) if k.startswith("_infralo_")}
|
|
160
|
+
|
|
161
|
+
@functools.wraps(func)
|
|
162
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
163
|
+
trace = _active_trace.get()
|
|
164
|
+
if trace is None:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"@infralo.tool '{tool_name}' was called outside of an active trace. "
|
|
167
|
+
"Wrap your code with `with infralo.start_trace(): ...`."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
infralo_kwargs = _pop_infralo_kwargs(kwargs)
|
|
171
|
+
metadata: dict = infralo_kwargs.get("_infralo_metadata", {})
|
|
172
|
+
tool_call_id: str = infralo_kwargs.get("_infralo_tool_call_id", str(uuid.uuid4()))
|
|
173
|
+
seq_override: int | None = infralo_kwargs.get("_infralo_sequence_number")
|
|
174
|
+
parent_override: str | None = infralo_kwargs.get("_infralo_parent_span_id")
|
|
175
|
+
|
|
176
|
+
input_data = _build_input(args, dict(kwargs))
|
|
177
|
+
|
|
178
|
+
with trace.tool(
|
|
179
|
+
name=tool_name,
|
|
180
|
+
tool_call_id=tool_call_id,
|
|
181
|
+
sequence_number=seq_override,
|
|
182
|
+
parent_span_id=parent_override,
|
|
183
|
+
) as span:
|
|
184
|
+
if input_data is not None:
|
|
185
|
+
span.set_input(input_data, capture_payload=capture_input)
|
|
186
|
+
if metadata:
|
|
187
|
+
span.set_metadata(**metadata)
|
|
188
|
+
result = func(*args, **kwargs)
|
|
189
|
+
if capture_output:
|
|
190
|
+
span.set_output(result, capture_payload=capture_output)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
@functools.wraps(func)
|
|
194
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
195
|
+
trace = _active_trace.get()
|
|
196
|
+
if trace is None:
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
f"@infralo.tool '{tool_name}' was called outside of an active trace. "
|
|
199
|
+
"Wrap your code with `with infralo.start_trace(): ...`."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
infralo_kwargs = _pop_infralo_kwargs(kwargs)
|
|
203
|
+
metadata: dict = infralo_kwargs.get("_infralo_metadata", {})
|
|
204
|
+
tool_call_id: str = infralo_kwargs.get("_infralo_tool_call_id", str(uuid.uuid4()))
|
|
205
|
+
seq_override: int | None = infralo_kwargs.get("_infralo_sequence_number")
|
|
206
|
+
parent_override: str | None = infralo_kwargs.get("_infralo_parent_span_id")
|
|
207
|
+
|
|
208
|
+
input_data = _build_input(args, dict(kwargs))
|
|
209
|
+
|
|
210
|
+
with trace.tool(
|
|
211
|
+
name=tool_name,
|
|
212
|
+
tool_call_id=tool_call_id,
|
|
213
|
+
sequence_number=seq_override,
|
|
214
|
+
parent_span_id=parent_override,
|
|
215
|
+
) as span:
|
|
216
|
+
if input_data is not None:
|
|
217
|
+
span.set_input(input_data, capture_payload=capture_input)
|
|
218
|
+
if metadata:
|
|
219
|
+
span.set_metadata(**metadata)
|
|
220
|
+
result = await func(*args, **kwargs)
|
|
221
|
+
if capture_output:
|
|
222
|
+
span.set_output(result, capture_payload=capture_output)
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
# Transparently support both sync and async functions
|
|
226
|
+
if asyncio.iscoroutinefunction(func):
|
|
227
|
+
return async_wrapper # type: ignore[return-value]
|
|
228
|
+
return sync_wrapper # type: ignore[return-value]
|
|
229
|
+
|
|
230
|
+
# Support both @infralo.tool and @infralo.tool(name="...", ...)
|
|
231
|
+
if fn is not None:
|
|
232
|
+
return decorator(fn)
|
|
233
|
+
return decorator
|
|
234
|
+
|
|
235
|
+
# ── Utilities ────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def flush(self, timeout: float = 10.0) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Manually flush all pending spans. Useful in tests or before shutdown.
|
|
240
|
+
|
|
241
|
+
Calling this is NOT required in production — spans are flushed automatically
|
|
242
|
+
by the background thread and on process exit.
|
|
243
|
+
"""
|
|
244
|
+
self._exporter.flush(timeout)
|
|
245
|
+
|
|
246
|
+
def shutdown(self) -> None:
|
|
247
|
+
"""
|
|
248
|
+
Shutdown the background exporter gracefully. Called automatically on exit.
|
|
249
|
+
Safe to call manually if you need to ensure spans are sent before teardown.
|
|
250
|
+
"""
|
|
251
|
+
self._exporter.shutdown()
|
infralo/exporter.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infralo — Background HTTP span exporter.
|
|
3
|
+
|
|
4
|
+
Spans are pushed to a thread-safe queue and flushed to the Infralo API
|
|
5
|
+
by a single daemon thread. The caller never waits for HTTP — enqueue()
|
|
6
|
+
returns immediately.
|
|
7
|
+
|
|
8
|
+
Flush schedule:
|
|
9
|
+
- Every `flush_interval` seconds (default 5 s).
|
|
10
|
+
- Early if the queue reaches `max_batch_size` (default 50).
|
|
11
|
+
- Final flush on process exit via atexit (called by Infralo.__init__).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import queue
|
|
18
|
+
import threading
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from infralo._models import ToolSpanData
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("infralo.exporter")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SpanExporter:
|
|
30
|
+
"""
|
|
31
|
+
Background HTTP exporter for tool call spans.
|
|
32
|
+
|
|
33
|
+
Not intended for direct use — created and managed by the Infralo client.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api_key: str,
|
|
39
|
+
endpoint: str,
|
|
40
|
+
flush_interval: float = 5.0,
|
|
41
|
+
max_batch_size: int = 50,
|
|
42
|
+
max_queue_size: int = 10000,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.api_key = api_key
|
|
45
|
+
clean_endpoint = endpoint.rstrip("/")
|
|
46
|
+
for suffix in ("/api/v1", "/v1"):
|
|
47
|
+
if clean_endpoint.endswith(suffix):
|
|
48
|
+
clean_endpoint = clean_endpoint[: -len(suffix)]
|
|
49
|
+
self.endpoint = clean_endpoint.rstrip("/")
|
|
50
|
+
self.flush_interval = flush_interval
|
|
51
|
+
self.max_batch_size = max_batch_size
|
|
52
|
+
self.max_queue_size = max_queue_size
|
|
53
|
+
|
|
54
|
+
self._queue: queue.Queue[ToolSpanData] = queue.Queue(maxsize=max_queue_size)
|
|
55
|
+
self._flush_event = threading.Event()
|
|
56
|
+
self._stop_event = threading.Event()
|
|
57
|
+
self._flush_lock = threading.Lock()
|
|
58
|
+
|
|
59
|
+
# Retry backoff state
|
|
60
|
+
self._current_interval = flush_interval
|
|
61
|
+
self._max_backoff = 60.0
|
|
62
|
+
|
|
63
|
+
self._thread = threading.Thread(
|
|
64
|
+
target=self._flush_loop,
|
|
65
|
+
name="infralo-exporter",
|
|
66
|
+
daemon=True, # dies with the process — atexit handles final flush
|
|
67
|
+
)
|
|
68
|
+
self._thread.start()
|
|
69
|
+
|
|
70
|
+
# ── Public API ──────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def enqueue(self, span: "ToolSpanData") -> None:
|
|
73
|
+
"""
|
|
74
|
+
Push a span onto the export queue. Always returns immediately.
|
|
75
|
+
Never raises — failures are logged and the span is re-queued.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
self._queue.put_nowait(span)
|
|
79
|
+
except queue.Full:
|
|
80
|
+
logger.warning("infralo: export queue full — dropping span %s", span.span_id)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if self._queue.qsize() >= self.max_batch_size:
|
|
84
|
+
self._flush_event.set() # wake the background thread early
|
|
85
|
+
|
|
86
|
+
def flush(self, timeout: float = 10.0) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Synchronously drain all pending spans from the queue.
|
|
89
|
+
Called automatically on process exit (atexit). Safe to call manually in tests.
|
|
90
|
+
"""
|
|
91
|
+
self._do_flush()
|
|
92
|
+
|
|
93
|
+
def shutdown(self) -> None:
|
|
94
|
+
"""Stop the background thread and do a final flush. Idempotent."""
|
|
95
|
+
self._stop_event.set()
|
|
96
|
+
self._flush_event.set()
|
|
97
|
+
self._thread.join(timeout=5.0)
|
|
98
|
+
self._do_flush()
|
|
99
|
+
|
|
100
|
+
# ── Background thread ───────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def _flush_loop(self) -> None:
|
|
103
|
+
"""Daemon thread: flush on interval or early-wake event with backoff on failure."""
|
|
104
|
+
while not self._stop_event.is_set():
|
|
105
|
+
self._flush_event.wait(timeout=self._current_interval)
|
|
106
|
+
self._flush_event.clear()
|
|
107
|
+
self._do_flush()
|
|
108
|
+
# Final drain after stop signal
|
|
109
|
+
self._do_flush()
|
|
110
|
+
|
|
111
|
+
def _do_flush(self) -> None:
|
|
112
|
+
"""Drain up to max_batch_size spans and POST them to the API."""
|
|
113
|
+
with self._flush_lock:
|
|
114
|
+
batch: list[ToolSpanData] = []
|
|
115
|
+
try:
|
|
116
|
+
while len(batch) < self.max_batch_size:
|
|
117
|
+
batch.append(self._queue.get_nowait())
|
|
118
|
+
except queue.Empty:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
if not batch:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
|
|
126
|
+
resp = httpx.post(
|
|
127
|
+
f"{self.endpoint}/api/v1/traces/spans/tool-call",
|
|
128
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
129
|
+
json=[s.to_dict() for s in batch],
|
|
130
|
+
timeout=10.0,
|
|
131
|
+
)
|
|
132
|
+
resp.raise_for_status()
|
|
133
|
+
logger.debug("infralo: flushed %d spans", len(batch))
|
|
134
|
+
|
|
135
|
+
# Reset backoff on success
|
|
136
|
+
self._current_interval = self.flush_interval
|
|
137
|
+
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.warning(
|
|
140
|
+
"infralo: failed to flush %d span(s): %s — re-enqueueing for retry",
|
|
141
|
+
len(batch),
|
|
142
|
+
exc,
|
|
143
|
+
)
|
|
144
|
+
# Apply exponential backoff
|
|
145
|
+
self._current_interval = min(self._current_interval * 2.0, self._max_backoff)
|
|
146
|
+
|
|
147
|
+
# Best-effort re-enqueue for next flush cycle
|
|
148
|
+
for span in batch:
|
|
149
|
+
try:
|
|
150
|
+
self._queue.put_nowait(span)
|
|
151
|
+
except queue.Full:
|
|
152
|
+
logger.warning(
|
|
153
|
+
"infralo: queue full during retry — dropping span %s", span.span_id
|
|
154
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infralo — Agent framework integrations.
|
|
3
|
+
|
|
4
|
+
Each integration is a separate optional module. Install only the extras you
|
|
5
|
+
need; the core ``infralo`` package intentionally has no framework dependencies.
|
|
6
|
+
|
|
7
|
+
+-----------------+-------------------------------------+---------------------------+
|
|
8
|
+
| Framework | Install extra | Import path |
|
|
9
|
+
+-----------------+-------------------------------------+---------------------------+
|
|
10
|
+
| Agno | pip install infralo[agno] | infralo.integrations.agno |
|
|
11
|
+
| LangGraph/Chain | pip install infralo[langgraph] | infralo.integrations.langgraph |
|
|
12
|
+
| CrewAI | pip install infralo[crewai] | infralo.integrations.crewai |
|
|
13
|
+
| Google ADK | pip install infralo[google-adk] | infralo.integrations.google_adk |
|
|
14
|
+
+-----------------+-------------------------------------+---------------------------+
|
|
15
|
+
|
|
16
|
+
All integrations follow the same three-step pattern:
|
|
17
|
+
|
|
18
|
+
1. Create the Infralo client once at module level.
|
|
19
|
+
2. Create / register the framework-specific adapter.
|
|
20
|
+
3. Wrap every agentic run with ``infralo.start_trace()``.
|
|
21
|
+
|
|
22
|
+
See the individual module docstrings and ``examples/`` for complete usage.
|
|
23
|
+
"""
|