infralo 0.1.0.dev1__tar.gz

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.
Files changed (56) hide show
  1. infralo-0.1.0.dev1/.gitignore +176 -0
  2. infralo-0.1.0.dev1/.pre-commit-config.yaml +45 -0
  3. infralo-0.1.0.dev1/PKG-INFO +480 -0
  4. infralo-0.1.0.dev1/README.md +461 -0
  5. infralo-0.1.0.dev1/examples/README.md +29 -0
  6. infralo-0.1.0.dev1/examples/agno/README.md +34 -0
  7. infralo-0.1.0.dev1/examples/agno/agno_complex_nesting.py +139 -0
  8. infralo-0.1.0.dev1/examples/agno/agno_integration.py +104 -0
  9. infralo-0.1.0.dev1/examples/agno/pyproject.toml +15 -0
  10. infralo-0.1.0.dev1/examples/agno/uv.lock +757 -0
  11. infralo-0.1.0.dev1/examples/core/README.md +63 -0
  12. infralo-0.1.0.dev1/examples/core/complex_nesting.py +226 -0
  13. infralo-0.1.0.dev1/examples/core/complex_nesting_decorator.py +223 -0
  14. infralo-0.1.0.dev1/examples/core/mcp_integration.py +241 -0
  15. infralo-0.1.0.dev1/examples/core/parallel_execution.py +185 -0
  16. infralo-0.1.0.dev1/examples/core/pattern_a_context_manager.py +178 -0
  17. infralo-0.1.0.dev1/examples/core/pattern_b_decorator.py +178 -0
  18. infralo-0.1.0.dev1/examples/core/pattern_c_manual.py +136 -0
  19. infralo-0.1.0.dev1/examples/core/pyproject.toml +16 -0
  20. infralo-0.1.0.dev1/examples/core/tool_first_execution.py +111 -0
  21. infralo-0.1.0.dev1/examples/core/uv.lock +1098 -0
  22. infralo-0.1.0.dev1/examples/crewai/README.md +38 -0
  23. infralo-0.1.0.dev1/examples/crewai/crewai_complex_nesting.py +193 -0
  24. infralo-0.1.0.dev1/examples/crewai/crewai_integration.py +139 -0
  25. infralo-0.1.0.dev1/examples/crewai/pyproject.toml +15 -0
  26. infralo-0.1.0.dev1/examples/crewai/uv.lock +4781 -0
  27. infralo-0.1.0.dev1/examples/google_adk/README.md +33 -0
  28. infralo-0.1.0.dev1/examples/google_adk/google_adk_complex_nesting.py +172 -0
  29. infralo-0.1.0.dev1/examples/google_adk/google_adk_integration.py +121 -0
  30. infralo-0.1.0.dev1/examples/google_adk/pyproject.toml +16 -0
  31. infralo-0.1.0.dev1/examples/google_adk/uv.lock +2746 -0
  32. infralo-0.1.0.dev1/examples/langgraph/README.md +34 -0
  33. infralo-0.1.0.dev1/examples/langgraph/langgraph_complex_nesting.py +141 -0
  34. infralo-0.1.0.dev1/examples/langgraph/langgraph_integration.py +112 -0
  35. infralo-0.1.0.dev1/examples/langgraph/pyproject.toml +15 -0
  36. infralo-0.1.0.dev1/examples/langgraph/uv.lock +1533 -0
  37. infralo-0.1.0.dev1/pyproject.toml +58 -0
  38. infralo-0.1.0.dev1/src/infralo/__init__.py +9 -0
  39. infralo-0.1.0.dev1/src/infralo/_models.py +84 -0
  40. infralo-0.1.0.dev1/src/infralo/client.py +251 -0
  41. infralo-0.1.0.dev1/src/infralo/exporter.py +154 -0
  42. infralo-0.1.0.dev1/src/infralo/integrations/__init__.py +23 -0
  43. infralo-0.1.0.dev1/src/infralo/integrations/agno.py +381 -0
  44. infralo-0.1.0.dev1/src/infralo/integrations/crewai.py +337 -0
  45. infralo-0.1.0.dev1/src/infralo/integrations/google_adk.py +238 -0
  46. infralo-0.1.0.dev1/src/infralo/integrations/langgraph.py +492 -0
  47. infralo-0.1.0.dev1/src/infralo/span.py +226 -0
  48. infralo-0.1.0.dev1/src/infralo/trace.py +457 -0
  49. infralo-0.1.0.dev1/tests/conftest.py +15 -0
  50. infralo-0.1.0.dev1/tests/integrations/__init__.py +1 -0
  51. infralo-0.1.0.dev1/tests/integrations/test_agno.py +186 -0
  52. infralo-0.1.0.dev1/tests/integrations/test_crewai.py +195 -0
  53. infralo-0.1.0.dev1/tests/integrations/test_google_adk.py +183 -0
  54. infralo-0.1.0.dev1/tests/integrations/test_langgraph.py +160 -0
  55. infralo-0.1.0.dev1/tests/test_sdk.py +225 -0
  56. infralo-0.1.0.dev1/uv.lock +5716 -0
@@ -0,0 +1,176 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+ *.env
139
+ !example.env
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ #.idea/
171
+
172
+ # Ruff stuff:
173
+ .ruff_cache/
174
+
175
+ # PyPI configuration file
176
+ .pypirc
@@ -0,0 +1,45 @@
1
+ repos:
2
+ - repo: https://github.com/psf/black
3
+ rev: 26.3.0
4
+ hooks:
5
+ - id: black
6
+ args: [--line-length=100]
7
+ exclude: ^examples/
8
+
9
+ - repo: https://github.com/astral-sh/ruff-pre-commit
10
+ rev: v0.15.5
11
+ hooks:
12
+ - id: ruff
13
+ args: [--fix]
14
+ exclude: ^examples/
15
+
16
+ - repo: https://github.com/pre-commit/mirrors-mypy
17
+ rev: v1.18.1
18
+ hooks:
19
+ - id: mypy
20
+ args:
21
+ [
22
+ --strict-optional,
23
+ --ignore-missing-imports,
24
+ --follow-imports=silent,
25
+ --show-traceback
26
+ ]
27
+ additional_dependencies:
28
+ [
29
+ types-requests,
30
+ types-paramiko,
31
+ types-bleach
32
+ ]
33
+ exclude: ^examples/
34
+
35
+ - repo: https://github.com/pre-commit/pre-commit-hooks
36
+ rev: v6.0.0
37
+ hooks:
38
+ - id: trailing-whitespace
39
+ - id: check-merge-conflict
40
+ - id: debug-statements
41
+ - id: end-of-file-fixer
42
+ - id: check-yaml
43
+ - id: check-toml
44
+ - id: check-added-large-files
45
+ args: ['--maxkb=2000']
@@ -0,0 +1,480 @@
1
+ Metadata-Version: 2.4
2
+ Name: infralo
3
+ Version: 0.1.0.dev1
4
+ Summary: Infralo SDK
5
+ License: MIT
6
+ Keywords: agents,ai,llm,observability,tracing
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: httpx<1.0,>=0.28.0
9
+ Provides-Extra: agno
10
+ Requires-Dist: agno>=2.0; extra == 'agno'
11
+ Provides-Extra: crewai
12
+ Requires-Dist: crewai>=1.0.0; extra == 'crewai'
13
+ Provides-Extra: google-adk
14
+ Requires-Dist: google-adk>=2.0.0; extra == 'google-adk'
15
+ Provides-Extra: langgraph
16
+ Requires-Dist: langchain-core>=1.0.0; extra == 'langgraph'
17
+ Requires-Dist: langgraph>=1.0.0; extra == 'langgraph'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Infralo Python SDK
21
+
22
+ Observability SDK for agentic AI workflows. Submits tool call spans to Infralo
23
+ in the background — your user-facing code is never blocked.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install infralo
29
+ # or
30
+ uv add infralo
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Core concept
36
+
37
+ You create one `Trace` per user request. The trace lives in a `ContextVar` and
38
+ propagates automatically through `async/await` and thread contexts — no need to
39
+ pass it around manually.
40
+
41
+ Every gateway LLM call needs two Infralo headers. `trace.headers()` builds them
42
+ for you from the current context:
43
+
44
+ ```python
45
+ extra_headers=trace.headers()
46
+ # → {"x-infralo-trace-id": "...", "x-infralo-parent-span-id": "..."}
47
+ # The parent_span_id is automatically the last tool span submitted, or absent
48
+ # on the first call. You never track trace.last_tool_span_id yourself.
49
+ ```
50
+
51
+ After the LLM responds, `trace.update_from_response(response)` reads back the
52
+ gateway's `x-infralo-span-id` header so the next tool spans know their parent.
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ from infralo import Infralo
60
+ from openai import OpenAI
61
+
62
+ infralo = Infralo(api_key="vk_your_key")
63
+ client = OpenAI(base_url="https://your-infralo-gateway.com/v1")
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Pattern A — Context manager (explicit, most control)
69
+
70
+ Use when you iterate over `tool_calls` from the LLM response or need fine control
71
+ over what gets recorded.
72
+
73
+ ```python
74
+ def run_agent(user_message: str, session_id: str) -> str:
75
+ with infralo.start_trace(session_id=session_id) as trace:
76
+
77
+ # ── First LLM call ──────────────────────────────────────────────────
78
+ response = client.chat.completions.create(
79
+ model="my-deployment",
80
+ messages=[{"role": "user", "content": user_message}],
81
+ extra_headers=trace.headers(), # ← always just this, nothing else
82
+ )
83
+ trace.update_from_response(response) # read x-infralo-span-id back
84
+
85
+ # ── Execute tools and record each as a span ─────────────────────────
86
+ tool_results = []
87
+ for tool_call in response.choices[0].message.tool_calls:
88
+ with trace.tool(
89
+ name=tool_call.function.name,
90
+ tool_call_id=tool_call.id,
91
+ # parent_span_id defaults to the last one from update_from_response()
92
+ ) as span:
93
+ args = json.loads(tool_call.function.arguments)
94
+ span.set_input(args) # optional: record inputs
95
+ result = dispatch_tool(tool_call)
96
+ span.set_output(result) # optional: record outputs
97
+ span.set_metadata(version="1.0") # optional: custom metadata
98
+ tool_results.append(result)
99
+
100
+ # ── Second LLM call ─────────────────────────────────────────────────
101
+ # trace.headers() now automatically includes x-infralo-parent-span-id
102
+ # pointing to the last tool span — no manual tracking needed.
103
+ response2 = client.chat.completions.create(
104
+ model="my-deployment",
105
+ messages=[..., *tool_messages(response, tool_results)],
106
+ extra_headers=trace.headers(), # ← same call, updated automatically
107
+ )
108
+ return response2.choices[0].message.content
109
+
110
+ # Trace exits: spans already queued; background thread flushes them.
111
+ ```
112
+
113
+ ### Error handling in Pattern A
114
+
115
+ **If the tool raises an exception inside `with trace.tool(...) as span:`, the span is
116
+ automatically marked `status="error"` with the exception message. The exception still
117
+ propagates normally — the SDK never swallows it.**
118
+
119
+ ```python
120
+ with trace.tool(name="search", tool_call_id="call_1") as span:
121
+ span.set_input({"query": "..."})
122
+ result = search_api(query) # ← if this raises, span is auto-marked as error
123
+ span.set_output(result) # ← only called if search_api succeeded
124
+ # ^ __exit__ is called regardless; exc_type != None → status="error"
125
+ ```
126
+
127
+ You do not need a try/except to get error recording. You only need one if you want
128
+ to recover and continue — in which case catch the exception, then decide whether to
129
+ re-raise or return a fallback.
130
+
131
+ ---
132
+
133
+ ## Pattern B — `@infralo.tool` decorator (ergonomic)
134
+
135
+ Best for clean code where timing and I/O capture can be automatic. The decorator
136
+ reads the active trace from context — the same error-handling guarantee applies:
137
+ **if the wrapped function raises, the span is marked as error automatically**.
138
+
139
+ ```python
140
+ @infralo.tool
141
+ def search_web(query: str) -> list[dict]:
142
+ """Timing and input/output captured automatically."""
143
+ return call_search_api(query)
144
+
145
+ @infralo.tool(name="fetch_doc", capture_input=False) # omit sensitive args from payload
146
+ async def fetch_doc(url: str, auth_token: str) -> str:
147
+ async with httpx.AsyncClient() as c:
148
+ r = await c.get(url, headers={"Authorization": auth_token})
149
+ return r.text
150
+
151
+ # Must be called inside an active trace
152
+ with infralo.start_trace(session_id="s1") as trace:
153
+ response = client.chat.completions.create(...)
154
+ trace.update_from_response(response)
155
+
156
+ # Decorated tools automatically read trace context — no headers to pass
157
+ results = search_web(query="latest AI news")
158
+ doc = await fetch_doc(url="https://...", auth_token="secret")
159
+
160
+ # Attach per-call metadata via _infralo_metadata kwarg
161
+ results2 = search_web(
162
+ query="page 2",
163
+ _infralo_metadata={"source": "google", "page": 2},
164
+ )
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Pattern C — Manual span (full control)
170
+
171
+ Use when you need try/except for recovery logic, or when you can't use a context
172
+ manager (e.g., tool starts in one coroutine and finishes in another).
173
+
174
+ Unlike Pattern A, you must call `span.end()` explicitly in all code paths.
175
+
176
+ ```python
177
+ with infralo.start_trace(session_id="s1") as trace:
178
+ trace.update_from_response(llm_response)
179
+
180
+ span = trace.start_tool_span(
181
+ name="slow_db_query",
182
+ tool_call_id="call_xyz",
183
+ )
184
+ try:
185
+ result = db.query("SELECT ...")
186
+ span.end(output={"rows": len(result)}) # status="success"
187
+ except TimeoutError:
188
+ span.end(status="timeout", error="Query exceeded 30s")
189
+ except Exception as e:
190
+ span.end(status="error", error=str(e), error_code="DB_ERROR")
191
+ ```
192
+
193
+ > [!IMPORTANT]
194
+ > **Always call `span.end()` in every branch.** If you forget, the span is never
195
+ > submitted. Pattern A's context manager handles this automatically — prefer it
196
+ > when possible.
197
+
198
+ ---
199
+
200
+ ## Parallel Tool Execution
201
+
202
+ Use `async with trace.parallel():` to give all tool spans inside the block the same
203
+ `sequence_number`, correctly representing that they ran concurrently.
204
+
205
+ ```python
206
+ @infralo.tool
207
+ async def search(query: str) -> list[dict]: ...
208
+
209
+ @infralo.tool
210
+ async def fetch(url: str) -> str: ...
211
+
212
+ with infralo.start_trace() as trace:
213
+ response = client.chat.completions.create(...)
214
+ trace.update_from_response(response)
215
+
216
+ # Parallel @tool calls — share sequence_number automatically
217
+ async with trace.parallel():
218
+ results = await asyncio.gather(
219
+ search(query="q1"),
220
+ fetch(url="https://..."),
221
+ )
222
+ # After the block: sequence counter advances past this group.
223
+ # The next tool() call gets the next sequential number.
224
+ ```
225
+
226
+ For parallel execution with explicit context managers (Pattern A style):
227
+
228
+ ```python
229
+ async with trace.parallel():
230
+ async def run_search():
231
+ with trace.tool("search", "call_1") as span:
232
+ result = await search_api("q1")
233
+ span.set_output(result)
234
+ return result
235
+
236
+ async def run_fetch():
237
+ with trace.tool("fetch", "call_2") as span:
238
+ result = await fetch_api("url")
239
+ span.set_output(result)
240
+ return result
241
+
242
+ results = await asyncio.gather(run_search(), run_fetch())
243
+ ```
244
+
245
+ Both approaches work the same way: `async with trace.parallel():` pins a shared
246
+ sequence number in a `ContextVar` before the tasks are created, so all tasks
247
+ inside inherit it automatically.
248
+
249
+ ---
250
+
251
+ ## Edge Cases
252
+
253
+ ### Header Propagation Failure
254
+
255
+ If `trace.update_from_response(response)` cannot find the `x-infralo-span-id` header (e.g., if a proxy or gateway stripped it):
256
+ 1. **A warning is logged** via Python's standard `logging` library under the logger name `infralo.trace`.
257
+ 2. **The active parent span ID is cleared** (`trace._current_parent_span_id = None`) to prevent subsequent tool spans from accidentally being linked to an older LLM span from a previous step.
258
+
259
+ ### Nested Traces
260
+
261
+ Starting a trace while another trace is already active on the same thread/async task is **fully supported** and behaves like a stack:
262
+ - Entering the inner `with infralo.start_trace()` block temporarily registers the inner trace as the active one.
263
+ - Any spans created inside the inner block belong to the inner trace.
264
+ - Exiting the inner block automatically restores the outer trace as the active trace.
265
+
266
+ ---
267
+
268
+ ## Configuration
269
+
270
+ ```python
271
+ infralo = Infralo(
272
+ api_key="vk_your_key",
273
+ endpoint="https://your-infralo-gateway.com", # default: https://api.infralo.com
274
+ flush_interval=5.0, # seconds between background flushes (default: 5.0)
275
+ max_batch_size=50, # flush early if queue reaches this size (default: 50)
276
+ max_queue_size=10000, # limit in-memory queue to prevent OOM on outages (default: 10000)
277
+ )
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Framework Integration
283
+
284
+ ### Web Frameworks
285
+
286
+ #### FastAPI
287
+
288
+ ```python
289
+ from contextlib import asynccontextmanager
290
+ from fastapi import FastAPI, Request
291
+
292
+ @asynccontextmanager
293
+ async def lifespan(app: FastAPI):
294
+ yield
295
+ infralo.shutdown() # flush remaining spans on shutdown
296
+
297
+ app = FastAPI(lifespan=lifespan)
298
+
299
+ @app.middleware("http")
300
+ async def infralo_middleware(request: Request, call_next):
301
+ session_id = request.headers.get("x-session-id", "")
302
+ with infralo.start_trace(session_id=session_id) as trace:
303
+ request.state.infralo_trace = trace
304
+ return await call_next(request)
305
+ ```
306
+
307
+ #### Django (WSGI — each thread has its own isolated ContextVar)
308
+
309
+ ```python
310
+ class InfraLoMiddleware:
311
+ def __init__(self, get_response):
312
+ self.get_response = get_response
313
+
314
+ def __call__(self, request):
315
+ session_id = request.META.get("HTTP_X_SESSION_ID", "")
316
+ with infralo.start_trace(session_id=session_id) as trace:
317
+ request.infralo_trace = trace
318
+ return self.get_response(request)
319
+ ```
320
+
321
+ #### Plain Python script
322
+
323
+ ```python
324
+ with infralo.start_trace() as trace:
325
+ ...
326
+ # atexit flushes remaining spans automatically — no manual flush needed.
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Agent Framework Integration
332
+
333
+ Infralo provides first-class integrations for the most popular agentic frameworks.
334
+ Install **only** the extras you need — the core `infralo` package has zero framework dependencies.
335
+
336
+ | Framework | Install | Import |
337
+ |---|---|---|
338
+ | Agno | `pip install infralo[agno]` | `infralo.integrations.agno` |
339
+ | LangGraph / LangChain | `pip install infralo[langgraph]` | `infralo.integrations.langgraph` |
340
+ | CrewAI | `pip install infralo[crewai]` | `infralo.integrations.crewai` |
341
+ | Google ADK | `pip install infralo[google-adk]` | `infralo.integrations.google_adk` |
342
+
343
+ All integrations follow the same three-step pattern:
344
+
345
+ 1. Create the `Infralo` client once at module level.
346
+ 2. Create / register the framework-specific adapter.
347
+ 3. Wrap every agentic run with `infralo.start_trace()`.
348
+
349
+ ### LLM span linking
350
+
351
+ Tool spans are **always captured** regardless of LLM routing. For the full LLM → tool
352
+ parent link, route your model calls through the Infralo gateway. The gateway injects
353
+ `x-infralo-span-id` into the HTTP response; each integration's model callback reads
354
+ this header and calls `trace.set_parent_span_id()` automatically.
355
+
356
+ When the gateway header is unavailable, tool spans appear as roots in the trace tree
357
+ (still useful) and a `DEBUG`-level log message is emitted.
358
+
359
+ ---
360
+
361
+ ### Agno
362
+
363
+ ```python
364
+ from infralo import Infralo
365
+ from infralo.integrations.agno import make_agno_callbacks
366
+ from agno.agent import Agent
367
+ from agno.models.openai import OpenAIChat
368
+
369
+ infralo = Infralo(api_key="vk_...")
370
+ callbacks = make_agno_callbacks(infralo)
371
+
372
+ with infralo.start_trace(session_id="user-123") as trace:
373
+ agent = Agent(
374
+ model=OpenAIChat(
375
+ id="my-deployment",
376
+ base_url="http://localhost:8000/v1", # Infralo gateway
377
+ api_key="vk_...",
378
+ ),
379
+ tools=[search_web, get_weather],
380
+ **callbacks, # injects before/after_tool + after_model
381
+ )
382
+ agent.print_response("What's the weather in London?")
383
+ ```
384
+
385
+ `make_agno_callbacks()` returns three hooks as a dict:
386
+
387
+ | Key | Purpose |
388
+ |---|---|
389
+ | `before_tool_callback` | Starts a ToolSpan; records tool name and input args |
390
+ | `after_tool_callback` | Ends the span; records output and status |
391
+ | `after_model_callback` | Reads `x-infralo-span-id` from the HTTP response; sets parent span |
392
+
393
+ ---
394
+
395
+ ### LangGraph / LangChain
396
+
397
+ ```python
398
+ from infralo import Infralo
399
+ from infralo.integrations.langgraph import InfraloCallbackHandler
400
+ from langchain_openai import ChatOpenAI
401
+ from langgraph.prebuilt import create_react_agent
402
+ from langchain_core.messages import HumanMessage
403
+
404
+ infralo = Infralo(api_key="vk_...")
405
+
406
+ llm = ChatOpenAI(
407
+ model="my-deployment",
408
+ base_url="http://localhost:8000/v1",
409
+ api_key="vk_...",
410
+ )
411
+
412
+ with infralo.start_trace(session_id="user-123") as trace:
413
+ agent = create_react_agent(llm, [search_web, get_weather])
414
+ handler = InfraloCallbackHandler() # one per invocation is safest
415
+
416
+ result = await agent.ainvoke(
417
+ {"messages": [HumanMessage(content="Search for LangGraph news")]},
418
+ config={"callbacks": [handler]},
419
+ )
420
+ ```
421
+
422
+ > [!NOTE]
423
+ > LangChain's callback abstraction does not expose raw HTTP response headers, so
424
+ > `x-infralo-span-id` cannot be extracted from `on_llm_end` automatically. Tool spans
425
+ > are always captured. Full LLM↔tool linkage is available when you inject the span ID
426
+ > into `llm_output` via a custom `ChatOpenAI` subclass or middleware.
427
+
428
+ ---
429
+
430
+ ### CrewAI
431
+
432
+ ```python
433
+ from infralo import Infralo
434
+ from infralo.integrations.crewai import InfraloCrewAIListener
435
+ from crewai import Agent, Task, Crew
436
+
437
+ infralo = Infralo(api_key="vk_...")
438
+
439
+ # Register once at module level — self-registers on the CrewAI event bus
440
+ InfraloCrewAIListener()
441
+
442
+ with infralo.start_trace(session_id="user-123") as trace:
443
+ crew = Crew(agents=[...], tasks=[...])
444
+ crew.kickoff()
445
+ ```
446
+
447
+ The listener handles `ToolUsageStartedEvent`, `ToolUsageFinishedEvent`, and
448
+ `ToolUsageErrorEvent` — no changes to your crew, agents, or task definitions.
449
+
450
+ ---
451
+
452
+ ### Google ADK
453
+
454
+ ```python
455
+ from infralo import Infralo
456
+ from infralo.integrations.google_adk import make_google_adk_callbacks
457
+ from google.adk.agents import Agent
458
+ from google.adk.models.lite_llm import LiteLlm
459
+ from google.adk.runners import Runner
460
+
461
+ infralo = Infralo(api_key="vk_...")
462
+ callbacks = make_google_adk_callbacks(infralo)
463
+
464
+ with infralo.start_trace(session_id="user-123") as trace:
465
+ agent = Agent(
466
+ name="my_agent",
467
+ model=LiteLlm(
468
+ model="openai/my-deployment",
469
+ api_base="http://localhost:8000/v1",
470
+ ),
471
+ tools=[search_web, get_weather],
472
+ **callbacks, # injects before/after_tool + after_model
473
+ )
474
+ runner = Runner(agent=agent, ...)
475
+ async for event in runner.run_async(...):
476
+ ...
477
+ ```
478
+
479
+ `make_google_adk_callbacks()` returns the same three hook keys as the Agno integration
480
+ (`before_tool_callback`, `after_tool_callback`, `after_model_callback`).