trace-agent-server 0.0.2__tar.gz → 0.1.0__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.
- trace_agent_server-0.1.0/PKG-INFO +118 -0
- trace_agent_server-0.1.0/README.md +78 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/pyproject.toml +6 -2
- trace_agent_server-0.1.0/src/trace_agent_server/api/otlp.py +50 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/api/runs.py +17 -125
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/config.py +2 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/main.py +2 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/schemas.py +1 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/compare.py +3 -2
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/events.py +3 -1
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/explanation.py +9 -2
- trace_agent_server-0.1.0/src/trace_agent_server/services/otlp.py +444 -0
- trace_agent_server-0.1.0/src/trace_agent_server/services/otlp_decode.py +213 -0
- trace_agent_server-0.1.0/src/trace_agent_server/services/run_lifecycle.py +215 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/run_summary.py +28 -4
- trace_agent_server-0.1.0/src/trace_agent_server.egg-info/PKG-INFO +118 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server.egg-info/SOURCES.txt +8 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server.egg-info/requires.txt +5 -1
- trace_agent_server-0.1.0/tests/test_framework_observations.py +287 -0
- trace_agent_server-0.1.0/tests/test_otlp_decode.py +205 -0
- trace_agent_server-0.1.0/tests/test_otlp_ingest.py +497 -0
- trace_agent_server-0.1.0/tests/test_run_lifecycle.py +117 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_sse.py +4 -6
- trace_agent_server-0.0.2/PKG-INFO +0 -76
- trace_agent_server-0.0.2/README.md +0 -39
- trace_agent_server-0.0.2/src/trace_agent_server.egg-info/PKG-INFO +0 -76
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/setup.cfg +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/__init__.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/api/__init__.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/cli.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/db.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/models.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/__init__.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/analytics.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/audit_guardrails.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/dashboard.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/graph.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/observability.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/provider.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server.egg-info/dependency_links.txt +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server.egg-info/entry_points.txt +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server.egg-info/top_level.txt +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_audit_guardrails_api.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_dashboard.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_events.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_providers.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_runs_api.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_sdk_wrappers.py +0 -0
- {trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/tests/test_turns.py +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trace-agent-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TraceAgent FastAPI backend server.
|
|
5
|
+
Author-email: Enrique Javier Villar Cea <villarcea.enriquejavier@gmail.com>, Javier Rodriguez Castellano <javirodriguezcastellano@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/LixusSoftware/TraceAgent
|
|
8
|
+
Project-URL: Repository, https://github.com/LixusSoftware/TraceAgent
|
|
9
|
+
Project-URL: Issues, https://github.com/LixusSoftware/TraceAgent/issues
|
|
10
|
+
Keywords: agent,observability,tracing,llm,tools
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: trace-agent-sdk
|
|
22
|
+
Requires-Dist: fastapi<1.0.0,>=0.115.0
|
|
23
|
+
Requires-Dist: httpx<1.0.0,>=0.27.0
|
|
24
|
+
Requires-Dist: openai<4.0.0,>=1.52.0
|
|
25
|
+
Requires-Dist: opentelemetry-proto<2.0.0,>=1.28.0
|
|
26
|
+
Requires-Dist: pydantic<3.0.0,>=2.8.0
|
|
27
|
+
Requires-Dist: pydantic-settings<3.0.0,>=2.3.0
|
|
28
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.32
|
|
29
|
+
Requires-Dist: uvicorn[standard]<1.0.0,>=0.30.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest<9.0.0,>=8.3.2; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov<7.0.0,>=6.0.0; extra == "dev"
|
|
33
|
+
Provides-Extra: providers
|
|
34
|
+
Requires-Dist: anthropic>=0.42.0; extra == "providers"
|
|
35
|
+
Requires-Dist: google-generativeai>=0.8.0; extra == "providers"
|
|
36
|
+
Provides-Extra: langchain
|
|
37
|
+
Requires-Dist: trace-agent-langchain; extra == "langchain"
|
|
38
|
+
Provides-Extra: pydantic-ai
|
|
39
|
+
Requires-Dist: trace-agent-pydantic-ai; extra == "pydantic-ai"
|
|
40
|
+
|
|
41
|
+
# TraceAgent Server
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+
[](https://traceagent.vercel.app)
|
|
49
|
+
|
|
50
|
+
FastAPI backend for TraceAgent. Stores runs, generates timelines, execution graphs, and explanations.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install trace-agent-server[providers]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Run
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# With the included CLI
|
|
62
|
+
trace-agent-server --host 0.0.0.0 --port 8000
|
|
63
|
+
|
|
64
|
+
# Or directly with uvicorn
|
|
65
|
+
uvicorn trace_agent_server.main:app --reload
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tracing agents through OpenTelemetry
|
|
69
|
+
|
|
70
|
+
The server is also an OTLP/HTTP receiver: an agent that already emits OpenTelemetry spans following the
|
|
71
|
+
[GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) (Pydantic AI does) is
|
|
72
|
+
traced by pointing its exporter at `POST /v1/traces`, with nothing from TraceAgent installed in the agent.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
76
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
77
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
78
|
+
from pydantic_ai import Agent
|
|
79
|
+
from pydantic_ai.capabilities import Instrumentation
|
|
80
|
+
from pydantic_ai.models.instrumented import InstrumentationSettings
|
|
81
|
+
|
|
82
|
+
provider = TracerProvider()
|
|
83
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:8000/v1/traces")))
|
|
84
|
+
|
|
85
|
+
agent = Agent("openai:gpt-4o", capabilities=[Instrumentation(settings=InstrumentationSettings(tracer_provider=provider))])
|
|
86
|
+
agent.run_sync("hello")
|
|
87
|
+
provider.shutdown() # a short script must flush before exiting
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- One run is created per trace (the trace id is the run id), named after `gen_ai.agent.name`. Add a `trace_agent.run_id`
|
|
91
|
+
resource attribute to attach the spans to a run you created with the SDK instead.
|
|
92
|
+
- `invoke_agent` spans become chain steps, `chat` spans model calls and `execute_tool` spans tools. Event times and
|
|
93
|
+
durations come from the spans; the run gets its provider, model, token totals and tool counters. Other spans are ignored.
|
|
94
|
+
- The run ends when its last agent span ends, also when the agent runs under a web server span or calls other agents.
|
|
95
|
+
- An agent whose output goes through an output function (`output_type=my_function`) also records that function as a tool
|
|
96
|
+
(`final_result` by default), because Pydantic AI emits an `execute_tool` span for it. It counts in the run's tool total
|
|
97
|
+
and shows up as a tool choice. The framework capability only records the agent's regular tools, so the two routes differ
|
|
98
|
+
by that one tool for such agents. Structured output (`output_type=SomeModel`) is not affected.
|
|
99
|
+
- Prompts and outputs are summarized. Full payloads are stored only if `TRACE_AGENT_CAPTURE_FULL_PAYLOADS` is on.
|
|
100
|
+
- Protobuf and JSON bodies are accepted, gzip included. Use either this or the framework integrations for an agent, not
|
|
101
|
+
both, or its run is recorded twice.
|
|
102
|
+
|
|
103
|
+
## Environment variables
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Description |
|
|
106
|
+
|----------|---------|-------------|
|
|
107
|
+
| `TRACE_AGENT_DATABASE_URL` | `sqlite:///./trace_agent.db` | Database URL (SQLite or PostgreSQL) |
|
|
108
|
+
| `TRACE_AGENT_OTLP_ENABLED` | `true` | Accept OpenTelemetry traces on `POST /v1/traces` |
|
|
109
|
+
| `TRACE_AGENT_OTLP_MAX_BODY_BYTES` | `10000000` | Largest accepted OTLP request, after decompression |
|
|
110
|
+
| `TRACE_AGENT_CAPTURE_FULL_PAYLOADS` | `true` | Keep full payloads, not only summaries |
|
|
111
|
+
| `TRACE_AGENT_OPENAI_API_KEY` | - | OpenAI API key |
|
|
112
|
+
| `TRACE_AGENT_OPENAI_BASE_URL` | - | OpenAI-compatible base URL (e.g. LM Studio) |
|
|
113
|
+
| `TRACE_AGENT_AUDIT_ENABLE_GUARDRAILS` | `false` | Enable prompt injection and PII checks |
|
|
114
|
+
| `TRACE_AGENT_AUDIT_METRICS_ENABLED` | `true` | Expose Prometheus `/metrics` endpoint |
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# TraceAgent Server
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](https://traceagent.vercel.app)
|
|
9
|
+
|
|
10
|
+
FastAPI backend for TraceAgent. Stores runs, generates timelines, execution graphs, and explanations.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install trace-agent-server[providers]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# With the included CLI
|
|
22
|
+
trace-agent-server --host 0.0.0.0 --port 8000
|
|
23
|
+
|
|
24
|
+
# Or directly with uvicorn
|
|
25
|
+
uvicorn trace_agent_server.main:app --reload
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Tracing agents through OpenTelemetry
|
|
29
|
+
|
|
30
|
+
The server is also an OTLP/HTTP receiver: an agent that already emits OpenTelemetry spans following the
|
|
31
|
+
[GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) (Pydantic AI does) is
|
|
32
|
+
traced by pointing its exporter at `POST /v1/traces`, with nothing from TraceAgent installed in the agent.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
36
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
37
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
38
|
+
from pydantic_ai import Agent
|
|
39
|
+
from pydantic_ai.capabilities import Instrumentation
|
|
40
|
+
from pydantic_ai.models.instrumented import InstrumentationSettings
|
|
41
|
+
|
|
42
|
+
provider = TracerProvider()
|
|
43
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:8000/v1/traces")))
|
|
44
|
+
|
|
45
|
+
agent = Agent("openai:gpt-4o", capabilities=[Instrumentation(settings=InstrumentationSettings(tracer_provider=provider))])
|
|
46
|
+
agent.run_sync("hello")
|
|
47
|
+
provider.shutdown() # a short script must flush before exiting
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- One run is created per trace (the trace id is the run id), named after `gen_ai.agent.name`. Add a `trace_agent.run_id`
|
|
51
|
+
resource attribute to attach the spans to a run you created with the SDK instead.
|
|
52
|
+
- `invoke_agent` spans become chain steps, `chat` spans model calls and `execute_tool` spans tools. Event times and
|
|
53
|
+
durations come from the spans; the run gets its provider, model, token totals and tool counters. Other spans are ignored.
|
|
54
|
+
- The run ends when its last agent span ends, also when the agent runs under a web server span or calls other agents.
|
|
55
|
+
- An agent whose output goes through an output function (`output_type=my_function`) also records that function as a tool
|
|
56
|
+
(`final_result` by default), because Pydantic AI emits an `execute_tool` span for it. It counts in the run's tool total
|
|
57
|
+
and shows up as a tool choice. The framework capability only records the agent's regular tools, so the two routes differ
|
|
58
|
+
by that one tool for such agents. Structured output (`output_type=SomeModel`) is not affected.
|
|
59
|
+
- Prompts and outputs are summarized. Full payloads are stored only if `TRACE_AGENT_CAPTURE_FULL_PAYLOADS` is on.
|
|
60
|
+
- Protobuf and JSON bodies are accepted, gzip included. Use either this or the framework integrations for an agent, not
|
|
61
|
+
both, or its run is recorded twice.
|
|
62
|
+
|
|
63
|
+
## Environment variables
|
|
64
|
+
|
|
65
|
+
| Variable | Default | Description |
|
|
66
|
+
|----------|---------|-------------|
|
|
67
|
+
| `TRACE_AGENT_DATABASE_URL` | `sqlite:///./trace_agent.db` | Database URL (SQLite or PostgreSQL) |
|
|
68
|
+
| `TRACE_AGENT_OTLP_ENABLED` | `true` | Accept OpenTelemetry traces on `POST /v1/traces` |
|
|
69
|
+
| `TRACE_AGENT_OTLP_MAX_BODY_BYTES` | `10000000` | Largest accepted OTLP request, after decompression |
|
|
70
|
+
| `TRACE_AGENT_CAPTURE_FULL_PAYLOADS` | `true` | Keep full payloads, not only summaries |
|
|
71
|
+
| `TRACE_AGENT_OPENAI_API_KEY` | - | OpenAI API key |
|
|
72
|
+
| `TRACE_AGENT_OPENAI_BASE_URL` | - | OpenAI-compatible base URL (e.g. LM Studio) |
|
|
73
|
+
| `TRACE_AGENT_AUDIT_ENABLE_GUARDRAILS` | `false` | Enable prompt injection and PII checks |
|
|
74
|
+
| `TRACE_AGENT_AUDIT_METRICS_ENABLED` | `true` | Expose Prometheus `/metrics` endpoint |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trace-agent-server"
|
|
7
|
-
version = "0.0
|
|
7
|
+
version = "0.1.0"
|
|
8
8
|
description = "TraceAgent FastAPI backend server."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -29,7 +29,8 @@ dependencies = [
|
|
|
29
29
|
"trace-agent-sdk",
|
|
30
30
|
"fastapi>=0.115.0,<1.0.0",
|
|
31
31
|
"httpx>=0.27.0,<1.0.0",
|
|
32
|
-
"openai>=1.52.0,<
|
|
32
|
+
"openai>=1.52.0,<4.0.0",
|
|
33
|
+
"opentelemetry-proto>=1.28.0,<2.0.0",
|
|
33
34
|
"pydantic>=2.8.0,<3.0.0",
|
|
34
35
|
"pydantic-settings>=2.3.0,<3.0.0",
|
|
35
36
|
"sqlalchemy>=2.0.32,<3.0.0",
|
|
@@ -53,6 +54,9 @@ providers = [
|
|
|
53
54
|
langchain = [
|
|
54
55
|
"trace-agent-langchain",
|
|
55
56
|
]
|
|
57
|
+
pydantic-ai = [
|
|
58
|
+
"trace-agent-pydantic-ai",
|
|
59
|
+
]
|
|
56
60
|
|
|
57
61
|
[project.scripts]
|
|
58
62
|
trace-agent-server = "trace_agent_server.cli:main"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""OTLP/HTTP trace receiver: point an OpenTelemetry exporter at ``<server>/v1/traces``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
8
|
+
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceResponse
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from trace_agent_server.db import get_session
|
|
12
|
+
from trace_agent_server.services.otlp import ingest_spans
|
|
13
|
+
from trace_agent_server.services.otlp_decode import BodyTooLarge, OtlpDecodeError, decode_traces, decompress
|
|
14
|
+
|
|
15
|
+
# Not under /api: OTLP exporters append /v1/traces to the endpoint they are given.
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.post("/v1/traces")
|
|
20
|
+
async def receive_traces(request: Request, session: Session = Depends(get_session)) -> Response:
|
|
21
|
+
settings = request.app.state.settings
|
|
22
|
+
if not settings.otlp_enabled:
|
|
23
|
+
raise HTTPException(status_code=404, detail="The OTLP receiver is disabled.")
|
|
24
|
+
|
|
25
|
+
limit = settings.otlp_max_body_bytes
|
|
26
|
+
declared = request.headers.get("content-length")
|
|
27
|
+
if declared and declared.isdigit() and int(declared) > limit:
|
|
28
|
+
raise HTTPException(status_code=413, detail=f"Request body is over {limit} bytes.")
|
|
29
|
+
|
|
30
|
+
content_type = request.headers.get("content-type")
|
|
31
|
+
try:
|
|
32
|
+
body = decompress(await request.body(), request.headers.get("content-encoding"), limit)
|
|
33
|
+
spans = decode_traces(body, content_type)
|
|
34
|
+
except BodyTooLarge as exc:
|
|
35
|
+
raise HTTPException(status_code=413, detail=str(exc)) from exc
|
|
36
|
+
except OtlpDecodeError as exc:
|
|
37
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
38
|
+
|
|
39
|
+
result = ingest_spans(session, spans, capture_full=settings.capture_full_payloads)
|
|
40
|
+
session.commit()
|
|
41
|
+
|
|
42
|
+
is_json = (content_type or "").split(";")[0].strip().lower() == "application/json"
|
|
43
|
+
if is_json:
|
|
44
|
+
partial = {"partialSuccess": {"rejectedSpans": result.rejected}} if result.rejected else {}
|
|
45
|
+
return Response(content=json.dumps(partial), media_type="application/json")
|
|
46
|
+
response = ExportTraceServiceResponse()
|
|
47
|
+
if result.rejected:
|
|
48
|
+
response.partial_success.rejected_spans = result.rejected
|
|
49
|
+
response.partial_success.error_message = "Some spans could not be understood."
|
|
50
|
+
return Response(content=response.SerializeToString(), media_type="application/x-protobuf")
|
|
@@ -42,6 +42,12 @@ from trace_agent_server.services.events import create_artifact, record_event
|
|
|
42
42
|
from trace_agent_server.services.explanation import derive_explanation
|
|
43
43
|
from trace_agent_server.services.graph import build_decision_graph, build_execution_graph
|
|
44
44
|
from trace_agent_server.services.provider import ProviderTurnRequest
|
|
45
|
+
from trace_agent_server.services.run_lifecycle import (
|
|
46
|
+
apply_llm_usage,
|
|
47
|
+
complete_run,
|
|
48
|
+
fail_run_with,
|
|
49
|
+
record_tool_result,
|
|
50
|
+
)
|
|
45
51
|
from trace_agent_server.services.run_summary import (
|
|
46
52
|
build_command_chain,
|
|
47
53
|
build_execution_metadata,
|
|
@@ -458,80 +464,7 @@ def store_tool_results(
|
|
|
458
464
|
capture_full = getattr(request.app.state.settings, "capture_full_payloads", True)
|
|
459
465
|
|
|
460
466
|
for result in payload.results:
|
|
461
|
-
|
|
462
|
-
session,
|
|
463
|
-
run,
|
|
464
|
-
actor="sdk",
|
|
465
|
-
event_type="tool.started",
|
|
466
|
-
summary=f"Started {result.name}.",
|
|
467
|
-
status="running",
|
|
468
|
-
step_id=result.step_id,
|
|
469
|
-
tool_name=result.name,
|
|
470
|
-
payload_hash=stable_hash(result.args),
|
|
471
|
-
payload_full={"args": result.args} if capture_full else None,
|
|
472
|
-
metadata={"call_id": result.call_id, "args_summary": summarize_value(result.args)},
|
|
473
|
-
)
|
|
474
|
-
if result.status == "succeeded":
|
|
475
|
-
run.tool_count += 1
|
|
476
|
-
completed_event = record_event(
|
|
477
|
-
session,
|
|
478
|
-
run,
|
|
479
|
-
actor="sdk",
|
|
480
|
-
event_type="tool.succeeded",
|
|
481
|
-
summary=result.output_summary or f"{result.name} completed.",
|
|
482
|
-
status="succeeded",
|
|
483
|
-
step_id=result.step_id,
|
|
484
|
-
tool_name=result.name,
|
|
485
|
-
duration_ms=result.duration_ms,
|
|
486
|
-
payload_hash=result.output_hash,
|
|
487
|
-
payload_full={"output_summary": result.output_summary} if capture_full else None,
|
|
488
|
-
metadata={"call_id": result.call_id, "artifact_count": len(result.artifacts)},
|
|
489
|
-
)
|
|
490
|
-
else:
|
|
491
|
-
run.tool_count += 1
|
|
492
|
-
run.error_count += 1
|
|
493
|
-
completed_event = record_event(
|
|
494
|
-
session,
|
|
495
|
-
run,
|
|
496
|
-
actor="sdk",
|
|
497
|
-
event_type="tool.failed",
|
|
498
|
-
summary=result.error_summary or f"{result.name} failed.",
|
|
499
|
-
status="failed",
|
|
500
|
-
step_id=result.step_id,
|
|
501
|
-
tool_name=result.name,
|
|
502
|
-
duration_ms=result.duration_ms,
|
|
503
|
-
error_code=result.error_code,
|
|
504
|
-
payload_hash=result.output_hash,
|
|
505
|
-
payload_full={"error_summary": result.error_summary} if capture_full else None,
|
|
506
|
-
metadata={"call_id": result.call_id, "artifact_count": len(result.artifacts)},
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
for artifact_input in result.artifacts:
|
|
510
|
-
artifact = create_artifact(
|
|
511
|
-
session,
|
|
512
|
-
run,
|
|
513
|
-
source_event_id=completed_event.id,
|
|
514
|
-
kind=artifact_input.kind,
|
|
515
|
-
label=artifact_input.label,
|
|
516
|
-
summary=artifact_input.summary,
|
|
517
|
-
content_hash=stable_hash({"kind": artifact_input.kind, "label": artifact_input.label, "summary": artifact_input.summary}),
|
|
518
|
-
metadata=artifact_input.metadata,
|
|
519
|
-
)
|
|
520
|
-
run.artifact_count += 1
|
|
521
|
-
record_event(
|
|
522
|
-
session,
|
|
523
|
-
run,
|
|
524
|
-
actor="sdk",
|
|
525
|
-
event_type="artifact.created",
|
|
526
|
-
summary=artifact.summary,
|
|
527
|
-
status="created",
|
|
528
|
-
step_id=result.step_id,
|
|
529
|
-
tool_name=result.name,
|
|
530
|
-
artifact_refs=[artifact.id],
|
|
531
|
-
metadata={"artifact_label": artifact.label, "call_id": result.call_id},
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
started_event.status = "completed"
|
|
467
|
+
record_tool_result(session, run, result, capture_full=capture_full)
|
|
535
468
|
|
|
536
469
|
session.commit()
|
|
537
470
|
return {"status": "ok", "recorded": len(payload.results)}
|
|
@@ -755,6 +688,14 @@ def store_observations(
|
|
|
755
688
|
event_type = "llm.started" if observation.status == "running" else "llm.completed"
|
|
756
689
|
if observation.status == "failed":
|
|
757
690
|
event_type = "llm.failed"
|
|
691
|
+
if observation.status == "succeeded":
|
|
692
|
+
apply_llm_usage(
|
|
693
|
+
run,
|
|
694
|
+
provider=observation.provider,
|
|
695
|
+
model=observation.model,
|
|
696
|
+
prompt_tokens=observation.metadata.get("prompt_tokens"),
|
|
697
|
+
completion_tokens=observation.metadata.get("completion_tokens"),
|
|
698
|
+
)
|
|
758
699
|
record_event(
|
|
759
700
|
session,
|
|
760
701
|
run,
|
|
@@ -861,42 +802,7 @@ def finish_run(
|
|
|
861
802
|
session: Session = Depends(get_session),
|
|
862
803
|
) -> dict[str, Any]:
|
|
863
804
|
run = _get_run_or_404(session, run_id)
|
|
864
|
-
|
|
865
|
-
run.final_output_summary = final_summary
|
|
866
|
-
run.status = "completed"
|
|
867
|
-
run.ended_at = utcnow()
|
|
868
|
-
completed_event = record_event(
|
|
869
|
-
session,
|
|
870
|
-
run,
|
|
871
|
-
actor="proxy",
|
|
872
|
-
event_type="run.completed",
|
|
873
|
-
summary=final_summary,
|
|
874
|
-
status="completed",
|
|
875
|
-
payload_hash=stable_hash(payload.final_output),
|
|
876
|
-
)
|
|
877
|
-
for artifact_input in payload.artifacts:
|
|
878
|
-
artifact = create_artifact(
|
|
879
|
-
session,
|
|
880
|
-
run,
|
|
881
|
-
source_event_id=completed_event.id,
|
|
882
|
-
kind=artifact_input.kind,
|
|
883
|
-
label=artifact_input.label,
|
|
884
|
-
summary=artifact_input.summary,
|
|
885
|
-
content_hash=stable_hash({"kind": artifact_input.kind, "label": artifact_input.label, "summary": artifact_input.summary}),
|
|
886
|
-
metadata=artifact_input.metadata,
|
|
887
|
-
)
|
|
888
|
-
run.artifact_count += 1
|
|
889
|
-
record_event(
|
|
890
|
-
session,
|
|
891
|
-
run,
|
|
892
|
-
actor="proxy",
|
|
893
|
-
event_type="artifact.created",
|
|
894
|
-
summary=artifact.summary,
|
|
895
|
-
status="created",
|
|
896
|
-
artifact_refs=[artifact.id],
|
|
897
|
-
metadata={"artifact_label": artifact.label},
|
|
898
|
-
)
|
|
899
|
-
derivation = derive_explanation(session, run)
|
|
805
|
+
derivation = complete_run(session, run, payload.final_output, payload.artifacts)
|
|
900
806
|
session.commit()
|
|
901
807
|
return {"status": "completed", "run_id": run.id, "decisions": len(derivation["decisions"])}
|
|
902
808
|
|
|
@@ -908,21 +814,7 @@ def fail_run(
|
|
|
908
814
|
session: Session = Depends(get_session),
|
|
909
815
|
) -> dict[str, Any]:
|
|
910
816
|
run = _get_run_or_404(session, run_id)
|
|
911
|
-
run.
|
|
912
|
-
run.ended_at = utcnow()
|
|
913
|
-
run.error_count += 1
|
|
914
|
-
run.final_output_summary = payload.error_summary
|
|
915
|
-
record_event(
|
|
916
|
-
session,
|
|
917
|
-
run,
|
|
918
|
-
actor="proxy",
|
|
919
|
-
event_type="run.failed",
|
|
920
|
-
summary=payload.error_summary,
|
|
921
|
-
status="failed",
|
|
922
|
-
error_code=payload.error_code,
|
|
923
|
-
payload_hash=stable_hash(payload.error_summary),
|
|
924
|
-
)
|
|
925
|
-
derive_explanation(session, run)
|
|
817
|
+
fail_run_with(session, run, payload.error_code, payload.error_summary)
|
|
926
818
|
session.commit()
|
|
927
819
|
return {"status": "failed", "run_id": run.id}
|
|
928
820
|
|
|
@@ -43,6 +43,8 @@ class Settings(BaseSettings):
|
|
|
43
43
|
audit_otel_exporter_endpoint: str | None = None
|
|
44
44
|
audit_otel_exporter_protocol: str = "grpc"
|
|
45
45
|
capture_full_payloads: bool = True
|
|
46
|
+
otlp_enabled: bool = True
|
|
47
|
+
otlp_max_body_bytes: int = 10_000_000
|
|
46
48
|
|
|
47
49
|
model_config = SettingsConfigDict(
|
|
48
50
|
env_prefix="TRACE_AGENT_",
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from fastapi import FastAPI
|
|
4
4
|
from fastapi.middleware.cors import CORSMiddleware
|
|
5
5
|
|
|
6
|
+
from trace_agent_server.api.otlp import router as otlp_router
|
|
6
7
|
from trace_agent_server.api.runs import router as runs_router
|
|
7
8
|
from trace_agent_server.config import Settings, get_settings
|
|
8
9
|
from trace_agent_server.db import Base, create_session_factory
|
|
@@ -67,6 +68,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
67
68
|
return {"status": "ok"}
|
|
68
69
|
|
|
69
70
|
app.include_router(runs_router)
|
|
71
|
+
app.include_router(otlp_router)
|
|
70
72
|
return app
|
|
71
73
|
|
|
72
74
|
|
{trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/compare.py
RENAMED
|
@@ -9,6 +9,7 @@ from trace_agent_server.services.run_summary import (
|
|
|
9
9
|
build_patch_activity,
|
|
10
10
|
build_timeline_groups,
|
|
11
11
|
build_tool_chain,
|
|
12
|
+
event_total_tokens,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -537,8 +538,8 @@ def _align_timeline(left_items: list[dict[str, Any]], right_items: list[dict[str
|
|
|
537
538
|
|
|
538
539
|
|
|
539
540
|
def _overview_diff(left_run: Run, right_run: Run, left_events: list[Event], right_events: list[Event]) -> dict[str, Any]:
|
|
540
|
-
left_token_total = sum(
|
|
541
|
-
right_token_total = sum(
|
|
541
|
+
left_token_total = sum(event_total_tokens(event) for event in left_events)
|
|
542
|
+
right_token_total = sum(event_total_tokens(event) for event in right_events)
|
|
542
543
|
|
|
543
544
|
duration_ms_delta = None
|
|
544
545
|
if left_run.ended_at is not None and right_run.ended_at is not None:
|
{trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/events.py
RENAMED
|
@@ -63,14 +63,16 @@ def record_event(
|
|
|
63
63
|
payload_full: dict[str, Any] | None = None,
|
|
64
64
|
metadata: dict[str, Any] | None = None,
|
|
65
65
|
turn_id: str | None = None,
|
|
66
|
+
timestamp: datetime | None = None,
|
|
66
67
|
) -> Event:
|
|
68
|
+
"""Record an event; ``timestamp`` overrides "now" for sources that report when it happened."""
|
|
67
69
|
run.last_event_seq += 1
|
|
68
70
|
run.updated_at = utcnow()
|
|
69
71
|
event = Event(
|
|
70
72
|
run_id=run.id,
|
|
71
73
|
turn_id=turn_id,
|
|
72
74
|
seq=run.last_event_seq,
|
|
73
|
-
timestamp=utcnow(),
|
|
75
|
+
timestamp=timestamp or utcnow(),
|
|
74
76
|
step_id=step_id,
|
|
75
77
|
parent_step_id=parent_step_id,
|
|
76
78
|
actor=actor,
|
{trace_agent_server-0.0.2 → trace_agent_server-0.1.0}/src/trace_agent_server/services/explanation.py
RENAMED
|
@@ -57,8 +57,14 @@ def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
|
57
57
|
)
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
# Framework integrations (LangChain, Pydantic AI) report tools only through
|
|
61
|
+
# /tool-results and never emit tool.requested: there, the start of a tool is
|
|
62
|
+
# the moment the agent chose it.
|
|
63
|
+
requested_steps = {event.step_id for event in events if event.type == "tool.requested" and event.step_id}
|
|
64
|
+
|
|
60
65
|
for event in events:
|
|
61
|
-
|
|
66
|
+
derived_request = event.type == "tool.started" and event.step_id not in requested_steps
|
|
67
|
+
if event.type == "tool.requested" or derived_request:
|
|
62
68
|
tool_name = event.tool_name or "unknown tool"
|
|
63
69
|
add_decision(
|
|
64
70
|
seq=event.seq,
|
|
@@ -69,7 +75,8 @@ def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
|
69
75
|
evidence_event_ids=[event.id],
|
|
70
76
|
outcome="requested",
|
|
71
77
|
)
|
|
72
|
-
|
|
78
|
+
# "Started X." is identical for every call, so tell calls apart by their arguments.
|
|
79
|
+
key = (tool_name, (event.payload_hash or event.summary) if derived_request else event.summary)
|
|
73
80
|
loop_tracker[key].append(event)
|
|
74
81
|
if len(loop_tracker[key]) >= 3:
|
|
75
82
|
add_flag(
|