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.
- infralo-0.1.0.dev1/.gitignore +176 -0
- infralo-0.1.0.dev1/.pre-commit-config.yaml +45 -0
- infralo-0.1.0.dev1/PKG-INFO +480 -0
- infralo-0.1.0.dev1/README.md +461 -0
- infralo-0.1.0.dev1/examples/README.md +29 -0
- infralo-0.1.0.dev1/examples/agno/README.md +34 -0
- infralo-0.1.0.dev1/examples/agno/agno_complex_nesting.py +139 -0
- infralo-0.1.0.dev1/examples/agno/agno_integration.py +104 -0
- infralo-0.1.0.dev1/examples/agno/pyproject.toml +15 -0
- infralo-0.1.0.dev1/examples/agno/uv.lock +757 -0
- infralo-0.1.0.dev1/examples/core/README.md +63 -0
- infralo-0.1.0.dev1/examples/core/complex_nesting.py +226 -0
- infralo-0.1.0.dev1/examples/core/complex_nesting_decorator.py +223 -0
- infralo-0.1.0.dev1/examples/core/mcp_integration.py +241 -0
- infralo-0.1.0.dev1/examples/core/parallel_execution.py +185 -0
- infralo-0.1.0.dev1/examples/core/pattern_a_context_manager.py +178 -0
- infralo-0.1.0.dev1/examples/core/pattern_b_decorator.py +178 -0
- infralo-0.1.0.dev1/examples/core/pattern_c_manual.py +136 -0
- infralo-0.1.0.dev1/examples/core/pyproject.toml +16 -0
- infralo-0.1.0.dev1/examples/core/tool_first_execution.py +111 -0
- infralo-0.1.0.dev1/examples/core/uv.lock +1098 -0
- infralo-0.1.0.dev1/examples/crewai/README.md +38 -0
- infralo-0.1.0.dev1/examples/crewai/crewai_complex_nesting.py +193 -0
- infralo-0.1.0.dev1/examples/crewai/crewai_integration.py +139 -0
- infralo-0.1.0.dev1/examples/crewai/pyproject.toml +15 -0
- infralo-0.1.0.dev1/examples/crewai/uv.lock +4781 -0
- infralo-0.1.0.dev1/examples/google_adk/README.md +33 -0
- infralo-0.1.0.dev1/examples/google_adk/google_adk_complex_nesting.py +172 -0
- infralo-0.1.0.dev1/examples/google_adk/google_adk_integration.py +121 -0
- infralo-0.1.0.dev1/examples/google_adk/pyproject.toml +16 -0
- infralo-0.1.0.dev1/examples/google_adk/uv.lock +2746 -0
- infralo-0.1.0.dev1/examples/langgraph/README.md +34 -0
- infralo-0.1.0.dev1/examples/langgraph/langgraph_complex_nesting.py +141 -0
- infralo-0.1.0.dev1/examples/langgraph/langgraph_integration.py +112 -0
- infralo-0.1.0.dev1/examples/langgraph/pyproject.toml +15 -0
- infralo-0.1.0.dev1/examples/langgraph/uv.lock +1533 -0
- infralo-0.1.0.dev1/pyproject.toml +58 -0
- infralo-0.1.0.dev1/src/infralo/__init__.py +9 -0
- infralo-0.1.0.dev1/src/infralo/_models.py +84 -0
- infralo-0.1.0.dev1/src/infralo/client.py +251 -0
- infralo-0.1.0.dev1/src/infralo/exporter.py +154 -0
- infralo-0.1.0.dev1/src/infralo/integrations/__init__.py +23 -0
- infralo-0.1.0.dev1/src/infralo/integrations/agno.py +381 -0
- infralo-0.1.0.dev1/src/infralo/integrations/crewai.py +337 -0
- infralo-0.1.0.dev1/src/infralo/integrations/google_adk.py +238 -0
- infralo-0.1.0.dev1/src/infralo/integrations/langgraph.py +492 -0
- infralo-0.1.0.dev1/src/infralo/span.py +226 -0
- infralo-0.1.0.dev1/src/infralo/trace.py +457 -0
- infralo-0.1.0.dev1/tests/conftest.py +15 -0
- infralo-0.1.0.dev1/tests/integrations/__init__.py +1 -0
- infralo-0.1.0.dev1/tests/integrations/test_agno.py +186 -0
- infralo-0.1.0.dev1/tests/integrations/test_crewai.py +195 -0
- infralo-0.1.0.dev1/tests/integrations/test_google_adk.py +183 -0
- infralo-0.1.0.dev1/tests/integrations/test_langgraph.py +160 -0
- infralo-0.1.0.dev1/tests/test_sdk.py +225 -0
- 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`).
|