trace-agent-server 0.7.0__tar.gz → 0.8.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.7.0 → trace_agent_server-0.8.0}/PKG-INFO +1 -1
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/pyproject.toml +1 -1
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/schemas.py +2 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/explanation.py +26 -8
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/otlp.py +1 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/run_lifecycle.py +3 -1
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/PKG-INFO +1 -1
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/SOURCES.txt +1 -0
- trace_agent_server-0.8.0/tests/test_flags.py +52 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_framework_observations.py +32 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/README.md +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/setup.cfg +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/__init__.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/api/__init__.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/api/otlp.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/api/runs.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/cli.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/config.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/db.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/main.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/models.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/__init__.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/analytics.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/audit_guardrails.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/compare.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/dashboard.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/events.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/graph.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/observability.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/otlp_decode.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/provider.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/run_summary.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/stored_redaction.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/dependency_links.txt +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/entry_points.txt +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/requires.txt +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/top_level.txt +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_audit_guardrails_api.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_concurrent_writers.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_dashboard.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_events.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_graph.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_inactivity_sweep.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_otlp_decode.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_otlp_ingest.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_providers.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_run_lifecycle.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_runs_api.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_sdk_wrappers.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_sse.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_stored_redaction.py +0 -0
- {trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/tests/test_turns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trace-agent-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: TraceAgent FastAPI backend server.
|
|
5
5
|
Author-email: Enrique Javier Villar Cea <villarcea.enriquejavier@gmail.com>, Javier Rodriguez Castellano <javirodriguezcastellano@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -60,6 +60,8 @@ class ToolResultInput(BaseModel):
|
|
|
60
60
|
parent_step_id: str | None = None
|
|
61
61
|
status: Literal["succeeded", "failed"]
|
|
62
62
|
args: dict[str, Any] = Field(default_factory=dict)
|
|
63
|
+
# Hash of the arguments as the tool received them (the ones in args may have long texts cut).
|
|
64
|
+
args_hash: str | None = None
|
|
63
65
|
output_summary: str | None = None
|
|
64
66
|
# What the tool returned, whole (masked by the sender). Stored only with full payloads on.
|
|
65
67
|
output: Any | None = None
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/explanation.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -9,6 +10,17 @@ from sqlalchemy.orm import Session
|
|
|
9
10
|
from trace_agent_server.models import Decision, Event, Run
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def command_title(command: str, limit: int = 80) -> str:
|
|
14
|
+
"""A short name for a command, not its whole text: the first comment of a script says what it does; without
|
|
15
|
+
one, its first line that is not an import."""
|
|
16
|
+
code = re.sub(r"^\s*(?:python3?|py)\s+-c\s+", "", command)
|
|
17
|
+
lines = [line.strip() for line in code.splitlines() if line.strip()]
|
|
18
|
+
comment = next((line.lstrip("#").strip() for line in lines if line.startswith("#")), None)
|
|
19
|
+
first_code = next((line for line in lines if not line.startswith("#") and not re.match(r"(?:import|from)\s", line)), None)
|
|
20
|
+
text = " ".join((comment or first_code or (lines[0] if lines else command)).split())
|
|
21
|
+
return text if len(text) <= limit else f"{text[: limit - 1]}…"
|
|
22
|
+
|
|
23
|
+
|
|
12
24
|
def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
13
25
|
events = list(session.scalars(select(Event).where(Event.run_id == run.id).order_by(Event.seq)))
|
|
14
26
|
session.execute(delete(Decision).where(Decision.run_id == run.id))
|
|
@@ -17,6 +29,7 @@ def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
|
17
29
|
flags: list[dict[str, Any]] = []
|
|
18
30
|
seen_flags: set[tuple[str, str]] = set()
|
|
19
31
|
loop_tracker: dict[tuple[str, str], list[Event]] = defaultdict(list)
|
|
32
|
+
loop_flags: dict[tuple[str, str], dict[str, Any]] = {}
|
|
20
33
|
failure_streaks: dict[str, int] = defaultdict(int)
|
|
21
34
|
last_failure_by_tool: dict[str, Event] = {}
|
|
22
35
|
last_failure_by_command: dict[str, Event] = {}
|
|
@@ -78,13 +91,17 @@ def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
|
78
91
|
# "Started X." is identical for every call, so tell calls apart by their arguments.
|
|
79
92
|
key = (tool_name, (event.payload_hash or event.summary) if derived_request else event.summary)
|
|
80
93
|
loop_tracker[key].append(event)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{"tool_name": tool_name}
|
|
87
|
-
|
|
94
|
+
repeats = len(loop_tracker[key])
|
|
95
|
+
if repeats >= 3:
|
|
96
|
+
# One flag per repeated call, kept up to date, not a new flag for every repetition.
|
|
97
|
+
flag = loop_flags.get(key)
|
|
98
|
+
if flag is None:
|
|
99
|
+
flag = {"type": "loop", "label": "", "event_ids": [], "metadata": {"tool_name": tool_name}}
|
|
100
|
+
loop_flags[key] = flag
|
|
101
|
+
flags.append(flag)
|
|
102
|
+
flag["label"] = f"{tool_name} was called {repeats} times with the same arguments."
|
|
103
|
+
flag["event_ids"] = [item.id for item in loop_tracker[key]]
|
|
104
|
+
flag["metadata"] = {"tool_name": tool_name, "count": repeats}
|
|
88
105
|
if tool_name in last_failure_by_tool:
|
|
89
106
|
failed_event = last_failure_by_tool[tool_name]
|
|
90
107
|
retry_count += 1
|
|
@@ -141,9 +158,10 @@ def derive_explanation(session: Session, run: Run) -> dict[str, Any]:
|
|
|
141
158
|
command_name = str(event.event_metadata.get("command") or "unknown command")
|
|
142
159
|
last_failure_by_command[command_name] = event
|
|
143
160
|
failure_streaks[event.step_id or command_name] += 1
|
|
161
|
+
exit_code = event.event_metadata.get("exit_code")
|
|
144
162
|
add_flag(
|
|
145
163
|
"command_failure",
|
|
146
|
-
f"{
|
|
164
|
+
f"Command failed{f' (exit code {exit_code})' if exit_code is not None else ''}: {command_title(command_name)}",
|
|
147
165
|
[event.id],
|
|
148
166
|
{"command": command_name, "step_id": event.step_id},
|
|
149
167
|
)
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/otlp.py
RENAMED
|
@@ -372,6 +372,7 @@ def _tool_steps(session: Session, run: Run, span: SpanRecord, capture_full: bool
|
|
|
372
372
|
parent_step_id=span.parent_span_id,
|
|
373
373
|
status="failed" if failed else "succeeded",
|
|
374
374
|
args=redact_payload(arguments if isinstance(arguments, dict) else {"input": arguments} if arguments is not None else {}),
|
|
375
|
+
args_hash=stable_hash(arguments),
|
|
375
376
|
output_summary=summarize_value(_readable(result_text), sort_keys=False) if result_text is not None else None,
|
|
376
377
|
output=_parse_json(result_text) if result_text is not None and not failed else None,
|
|
377
378
|
output_hash=stable_hash(result_text if not failed else {"error": message}),
|
|
@@ -67,7 +67,9 @@ def record_tool_result(
|
|
|
67
67
|
step_id=result.step_id,
|
|
68
68
|
parent_step_id=result.parent_step_id,
|
|
69
69
|
tool_name=result.name,
|
|
70
|
-
|
|
70
|
+
# The sender hashes the whole arguments: the ones it sends have long texts cut, so two calls that differ
|
|
71
|
+
# only after the cut would look the same (and be flagged as a loop).
|
|
72
|
+
payload_hash=result.args_hash or stable_hash(result.args),
|
|
71
73
|
payload_full={"args": result.args} if capture_full else None,
|
|
72
74
|
metadata={"call_id": result.call_id, "args_summary": summarize_value(result.args)},
|
|
73
75
|
timestamp=started_at,
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trace-agent-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: TraceAgent FastAPI backend server.
|
|
5
5
|
Author-email: Enrique Javier Villar Cea <villarcea.enriquejavier@gmail.com>, Javier Rodriguez Castellano <javirodriguezcastellano@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
|
|
6
|
+
from trace_agent_server.config import Settings
|
|
7
|
+
from trace_agent_server.main import create_app
|
|
8
|
+
from trace_agent_server.services.explanation import command_title
|
|
9
|
+
|
|
10
|
+
SCRIPT = "python -c \n# OEE de las líneas del 14 al 18 de septiembre\nimport json\nkpis = kpis('L1')\nprint(kpis)"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def client(tmp_path) -> TestClient:
|
|
15
|
+
return TestClient(create_app(Settings(database_url=f"sqlite:///{tmp_path / 'flags.db'}", cors_origins=["http://localhost:5173"])))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_with_command(client: TestClient, **observation) -> dict:
|
|
19
|
+
run_id = client.post("/api/runs", json={"agent_name": "a", "goal": "g"}).json()["run_id"]
|
|
20
|
+
body = {"kind": "command", "step_id": "c1", **observation}
|
|
21
|
+
assert client.post(f"/api/runs/{run_id}/observations", json={"observations": [body]}).status_code == 200
|
|
22
|
+
assert client.post(f"/api/runs/{run_id}/finish", json={"final_output": "done"}).status_code == 200
|
|
23
|
+
return client.get(f"/api/runs/{run_id}/explanation").json()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestCommandTitle:
|
|
27
|
+
def test_uses_the_first_comment_of_a_script(self) -> None:
|
|
28
|
+
assert command_title(SCRIPT) == "OEE de las líneas del 14 al 18 de septiembre"
|
|
29
|
+
|
|
30
|
+
def test_without_a_comment_uses_the_first_line_that_is_not_an_import(self) -> None:
|
|
31
|
+
assert command_title("python -c \nimport json\nfrom os import path\nvalor = 1\nprint(valor)") == "valor = 1"
|
|
32
|
+
|
|
33
|
+
def test_a_plain_command_stays_and_a_long_one_is_cut(self) -> None:
|
|
34
|
+
assert command_title("pytest -q packages") == "pytest -q packages"
|
|
35
|
+
cut = command_title("python3 -c " + "x = 1; " * 60)
|
|
36
|
+
assert len(cut) == 80
|
|
37
|
+
assert cut.endswith("…")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestCommandFailureFlag:
|
|
41
|
+
def test_says_what_failed_and_with_what_code_instead_of_dumping_the_script(self, client) -> None:
|
|
42
|
+
flags = run_with_command(client, command=SCRIPT, status="failed", exit_code=1)["flags"]
|
|
43
|
+
|
|
44
|
+
failure = next(flag for flag in flags if flag["type"] == "command_failure")
|
|
45
|
+
assert failure["label"] == "Command failed (exit code 1): OEE de las líneas del 14 al 18 de septiembre"
|
|
46
|
+
assert "import json" not in failure["label"]
|
|
47
|
+
|
|
48
|
+
def test_without_an_exit_code_it_still_reads(self, client) -> None:
|
|
49
|
+
flags = run_with_command(client, command="pytest -q", status="failed")["flags"]
|
|
50
|
+
|
|
51
|
+
failure = next(flag for flag in flags if flag["type"] == "command_failure")
|
|
52
|
+
assert failure["label"] == "Command failed: pytest -q"
|
|
@@ -269,6 +269,38 @@ class TestDecisionsFromFrameworkTools:
|
|
|
269
269
|
flags = explanation(client, run_id)["flags"]
|
|
270
270
|
assert any(flag["type"] == "loop" for flag in flags)
|
|
271
271
|
|
|
272
|
+
def test_a_repeated_call_is_one_flag_with_its_count_in_plain_words(self, client) -> None:
|
|
273
|
+
run_id = start_run(client)
|
|
274
|
+
for step in ("t1", "t2", "t3", "t4", "t5", "t6"):
|
|
275
|
+
post_tool(client, run_id, step, "search", args={"q": "same"})
|
|
276
|
+
|
|
277
|
+
loops = [flag for flag in explanation(client, run_id)["flags"] if flag["type"] == "loop"]
|
|
278
|
+
|
|
279
|
+
assert len(loops) == 1
|
|
280
|
+
assert loops[0]["label"] == "search was called 6 times with the same arguments."
|
|
281
|
+
assert loops[0]["metadata"]["count"] == 6
|
|
282
|
+
assert len(loops[0]["event_ids"]) == 6
|
|
283
|
+
|
|
284
|
+
def test_different_programs_that_share_a_long_start_are_not_a_loop(self, client) -> None:
|
|
285
|
+
# The arguments are cut at 160 characters before they are sent: what tells the calls apart is their hash.
|
|
286
|
+
run_id = start_run(client)
|
|
287
|
+
common = "# OEE de las lineas del 14 al 18 de septiembre " * 6
|
|
288
|
+
for step, ending in (("t1", "kpis('L1')"), ("t2", "kpis('L2')"), ("t3", "kpis('L3')")):
|
|
289
|
+
result = {"call_id": step, "name": "ejecutar_python", "step_id": step, "status": "succeeded",
|
|
290
|
+
"args": {"codigo": common[:157] + "..."}, "args_hash": f"hash-of-the-whole-{ending}"}
|
|
291
|
+
assert client.post(f"/api/runs/{run_id}/tool-results", json={"results": [result]}).status_code == 200
|
|
292
|
+
|
|
293
|
+
assert not any(flag["type"] == "loop" for flag in explanation(client, run_id)["flags"])
|
|
294
|
+
|
|
295
|
+
def test_the_same_whole_arguments_are_a_loop_even_when_what_was_sent_was_cut(self, client) -> None:
|
|
296
|
+
run_id = start_run(client)
|
|
297
|
+
for step in ("t1", "t2", "t3"):
|
|
298
|
+
result = {"call_id": step, "name": "ejecutar_python", "step_id": step, "status": "succeeded",
|
|
299
|
+
"args": {"codigo": "x" * 157 + "..."}, "args_hash": "the-same-program"}
|
|
300
|
+
client.post(f"/api/runs/{run_id}/tool-results", json={"results": [result]})
|
|
301
|
+
|
|
302
|
+
assert any(flag["type"] == "loop" for flag in explanation(client, run_id)["flags"])
|
|
303
|
+
|
|
272
304
|
def test_the_same_tool_with_different_arguments_is_not_a_loop(self, client) -> None:
|
|
273
305
|
run_id = start_run(client)
|
|
274
306
|
for step, query in (("t1", "a"), ("t2", "b"), ("t3", "c")):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/__init__.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/analytics.py
RENAMED
|
File without changes
|
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/compare.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/dashboard.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/events.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/graph.py
RENAMED
|
File without changes
|
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/otlp_decode.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/provider.py
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server/services/run_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/requires.txt
RENAMED
|
File without changes
|
{trace_agent_server-0.7.0 → trace_agent_server-0.8.0}/src/trace_agent_server.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|