pyagent-trace 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ """PyAgent Trace — pattern-aware OpenTelemetry tracing for multi-agent LLM systems."""
2
+
3
+ from pyagent_trace.attributes import PyAgentAttributes
4
+ from pyagent_trace.cost import CostTracker
5
+ from pyagent_trace.recorder import Recorder
6
+
7
+ # OTel-dependent imports are lazy to avoid hard dependency
8
+ def __getattr__(name: str):
9
+ if name == "traced_agent":
10
+ from pyagent_trace.decorators import traced_agent
11
+ return traced_agent
12
+ if name == "traced_pattern":
13
+ from pyagent_trace.decorators import traced_pattern
14
+ return traced_pattern
15
+ if name == "PatternSpanEmitter":
16
+ from pyagent_trace.spans import PatternSpanEmitter
17
+ return PatternSpanEmitter
18
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19
+
20
+ __all__ = [
21
+ "traced_pattern",
22
+ "traced_agent",
23
+ "PatternSpanEmitter",
24
+ "PyAgentAttributes",
25
+ "CostTracker",
26
+ "Recorder",
27
+ ]
28
+ __version__ = "0.1.0"
@@ -0,0 +1,46 @@
1
+ """Custom OTel attributes for pyagent spans.
2
+
3
+ All attributes are namespaced under `pyagent.*` to avoid collisions.
4
+ Follows OpenTelemetry GenAI semantic conventions where applicable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class PyAgentAttributes:
11
+ """Attribute key constants for pyagent OTel spans."""
12
+
13
+ # Pattern attributes
14
+ PATTERN_TYPE = "pyagent.pattern.type"
15
+ PATTERN_ROUNDS = "pyagent.pattern.rounds"
16
+ PATTERN_CONSENSUS = "pyagent.pattern.consensus"
17
+ PATTERN_ESCALATED = "pyagent.pattern.escalated"
18
+ PATTERN_ESCALATION_LEVEL = "pyagent.pattern.escalation_level"
19
+
20
+ # Router attributes
21
+ ROUTER_DIFFICULTY = "pyagent.router.difficulty"
22
+ ROUTER_DIFFICULTY_CATEGORY = "pyagent.router.difficulty_category"
23
+ ROUTER_SELECTED_MODEL = "pyagent.router.selected_model"
24
+ ROUTER_COST_ESTIMATE = "pyagent.router.cost_estimate"
25
+ ROUTER_ALTERNATIVES = "pyagent.router.alternatives"
26
+
27
+ # Compression attributes
28
+ COMPRESS_INPUT_TOKENS = "pyagent.compress.input_tokens"
29
+ COMPRESS_OUTPUT_TOKENS = "pyagent.compress.output_tokens"
30
+ COMPRESS_SAVINGS_PCT = "pyagent.compress.savings_pct"
31
+
32
+ # Agent attributes
33
+ AGENT_NAME = "pyagent.agent.name"
34
+ AGENT_ROLE = "pyagent.agent.role"
35
+ AGENT_CONTRIBUTION = "pyagent.agent.contribution"
36
+
37
+ # Cost attributes
38
+ COST_TOTAL_USD = "pyagent.cost.total_usd"
39
+ COST_INPUT_TOKENS = "pyagent.cost.input_tokens"
40
+ COST_OUTPUT_TOKENS = "pyagent.cost.output_tokens"
41
+ COST_MODEL = "pyagent.cost.model"
42
+
43
+ # Execution attributes
44
+ EXEC_DURATION_MS = "pyagent.exec.duration_ms"
45
+ EXEC_TOKEN_ESTIMATE = "pyagent.exec.token_estimate"
46
+ EXEC_LLM_CALLS = "pyagent.exec.llm_calls"
pyagent_trace/cost.py ADDED
@@ -0,0 +1,90 @@
1
+ """CostTracker: accumulate and attribute costs by pattern, agent, and model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class CostEntry:
10
+ """A single cost record."""
11
+
12
+ pattern_type: str
13
+ agent_name: str
14
+ model: str
15
+ input_tokens: int
16
+ output_tokens: int
17
+ cost_usd: float
18
+
19
+
20
+ class CostTracker:
21
+ """Track costs across an entire workflow.
22
+
23
+ Usage:
24
+ tracker = CostTracker()
25
+ tracker.record("debate", "bull_agent", "gpt-4o", 500, 200, 0.003)
26
+ tracker.record("debate", "bear_agent", "gpt-4o-mini", 500, 200, 0.0004)
27
+ print(tracker.summary())
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ self._entries: list[CostEntry] = []
32
+
33
+ def record(
34
+ self,
35
+ pattern_type: str,
36
+ agent_name: str,
37
+ model: str,
38
+ input_tokens: int,
39
+ output_tokens: int,
40
+ cost_usd: float,
41
+ ) -> None:
42
+ """Record a cost entry."""
43
+ self._entries.append(CostEntry(
44
+ pattern_type=pattern_type,
45
+ agent_name=agent_name,
46
+ model=model,
47
+ input_tokens=input_tokens,
48
+ output_tokens=output_tokens,
49
+ cost_usd=cost_usd,
50
+ ))
51
+
52
+ @property
53
+ def total_cost(self) -> float:
54
+ return sum(e.cost_usd for e in self._entries)
55
+
56
+ @property
57
+ def total_tokens(self) -> int:
58
+ return sum(e.input_tokens + e.output_tokens for e in self._entries)
59
+
60
+ def by_pattern(self) -> dict[str, float]:
61
+ """Cost breakdown by pattern type."""
62
+ result: dict[str, float] = {}
63
+ for e in self._entries:
64
+ result[e.pattern_type] = result.get(e.pattern_type, 0.0) + e.cost_usd
65
+ return result
66
+
67
+ def by_agent(self) -> dict[str, float]:
68
+ """Cost breakdown by agent name."""
69
+ result: dict[str, float] = {}
70
+ for e in self._entries:
71
+ result[e.agent_name] = result.get(e.agent_name, 0.0) + e.cost_usd
72
+ return result
73
+
74
+ def by_model(self) -> dict[str, float]:
75
+ """Cost breakdown by model."""
76
+ result: dict[str, float] = {}
77
+ for e in self._entries:
78
+ result[e.model] = result.get(e.model, 0.0) + e.cost_usd
79
+ return result
80
+
81
+ def summary(self) -> dict[str, object]:
82
+ """Full cost summary."""
83
+ return {
84
+ "total_cost_usd": self.total_cost,
85
+ "total_tokens": self.total_tokens,
86
+ "entries": len(self._entries),
87
+ "by_pattern": self.by_pattern(),
88
+ "by_agent": self.by_agent(),
89
+ "by_model": self.by_model(),
90
+ }
@@ -0,0 +1,87 @@
1
+ """Decorators for automatic tracing of patterns and agents.
2
+
3
+ @traced_pattern: wraps a Pattern subclass to emit spans on every run().
4
+ @traced_agent: wraps an Agent to emit spans on every run().
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ import time
11
+ from typing import Any
12
+
13
+ from opentelemetry import trace
14
+
15
+ from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
16
+ from pyagent_trace.attributes import PyAgentAttributes
17
+
18
+ _tracer = trace.get_tracer("pyagent", "0.1.0")
19
+
20
+
21
+ def traced_pattern(cls: type[Pattern]) -> type[Pattern]:
22
+ """Class decorator: auto-emit OTel spans for every pattern.run() call.
23
+
24
+ Usage:
25
+ @traced_pattern
26
+ class MyPattern(Pattern):
27
+ ...
28
+ """
29
+ original_run = cls.run
30
+
31
+ @functools.wraps(original_run)
32
+ async def traced_run(self: Pattern, task: str, context: Context | None = None) -> Result:
33
+ with _tracer.start_as_current_span(
34
+ f"pyagent.pattern.{self.pattern_type}",
35
+ attributes={PyAgentAttributes.PATTERN_TYPE: self.pattern_type},
36
+ ) as span:
37
+ start = time.perf_counter()
38
+ try:
39
+ result = await original_run(self, task, context)
40
+
41
+ duration_ms = (time.perf_counter() - start) * 1000
42
+ span.set_attribute(PyAgentAttributes.EXEC_DURATION_MS, duration_ms)
43
+ span.set_attribute(PyAgentAttributes.EXEC_TOKEN_ESTIMATE, result.token_estimate)
44
+ span.set_attribute(PyAgentAttributes.COST_TOTAL_USD, result.cost_estimate)
45
+
46
+ if "rounds" in result.metadata:
47
+ span.set_attribute(PyAgentAttributes.PATTERN_ROUNDS, result.metadata["rounds"])
48
+
49
+ span.set_status(trace.StatusCode.OK)
50
+ return result
51
+ except Exception as e:
52
+ span.set_status(trace.StatusCode.ERROR, str(e))
53
+ span.record_exception(e)
54
+ raise
55
+
56
+ cls.run = traced_run # type: ignore[assignment]
57
+ return cls
58
+
59
+
60
+ def traced_agent(agent: Agent) -> Agent:
61
+ """Wrap an Agent instance to emit OTel spans on every run() call.
62
+
63
+ Usage:
64
+ agent = traced_agent(Agent("my_agent", llm))
65
+ """
66
+ original_run = agent.run
67
+
68
+ @functools.wraps(original_run)
69
+ async def traced_run(messages: list[Message]) -> Message:
70
+ with _tracer.start_as_current_span(
71
+ f"pyagent.agent.{agent.name}",
72
+ attributes={PyAgentAttributes.AGENT_NAME: agent.name},
73
+ ) as span:
74
+ start = time.perf_counter()
75
+ try:
76
+ result = await original_run(messages)
77
+ duration_ms = (time.perf_counter() - start) * 1000
78
+ span.set_attribute(PyAgentAttributes.EXEC_DURATION_MS, duration_ms)
79
+ span.set_status(trace.StatusCode.OK)
80
+ return result
81
+ except Exception as e:
82
+ span.set_status(trace.StatusCode.ERROR, str(e))
83
+ span.record_exception(e)
84
+ raise
85
+
86
+ agent.run = traced_run # type: ignore[assignment]
87
+ return agent
pyagent_trace/py.typed ADDED
File without changes
@@ -0,0 +1,111 @@
1
+ """Recorder: serialize all pattern messages and LLM responses for replay/debug.
2
+
3
+ Record mode: saves all messages + LLM responses to a JSONL file.
4
+ Replay mode: re-runs pattern with recorded LLM responses (deterministic).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from dataclasses import asdict, dataclass
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from pyagent_patterns.base import Message, Role
16
+
17
+
18
+ @dataclass
19
+ class RecordEntry:
20
+ """A single recorded event."""
21
+
22
+ timestamp: float
23
+ event_type: str # "llm_call", "message", "pattern_start", "pattern_end"
24
+ agent_name: str
25
+ messages_in: list[dict[str, Any]]
26
+ response: str
27
+ metadata: dict[str, Any]
28
+
29
+
30
+ class Recorder:
31
+ """Record pattern executions for debugging and replay.
32
+
33
+ Usage:
34
+ recorder = Recorder()
35
+ recorder.start("debate")
36
+ recorder.record_llm_call("bull", messages, response)
37
+ recorder.save("debug_trace.jsonl")
38
+ """
39
+
40
+ def __init__(self) -> None:
41
+ self._entries: list[RecordEntry] = []
42
+ self._start_time: float = 0.0
43
+
44
+ def start(self, pattern_type: str) -> None:
45
+ """Mark the start of a pattern execution."""
46
+ self._start_time = time.time()
47
+ self._entries.append(RecordEntry(
48
+ timestamp=self._start_time,
49
+ event_type="pattern_start",
50
+ agent_name="",
51
+ messages_in=[],
52
+ response="",
53
+ metadata={"pattern_type": pattern_type},
54
+ ))
55
+
56
+ def record_llm_call(
57
+ self,
58
+ agent_name: str,
59
+ messages: list[Message],
60
+ response: str,
61
+ metadata: dict[str, Any] | None = None,
62
+ ) -> None:
63
+ """Record an LLM call and its response."""
64
+ self._entries.append(RecordEntry(
65
+ timestamp=time.time(),
66
+ event_type="llm_call",
67
+ agent_name=agent_name,
68
+ messages_in=[
69
+ {"role": m.role.value, "content": m.content, "name": m.name}
70
+ for m in messages
71
+ ],
72
+ response=response,
73
+ metadata=metadata or {},
74
+ ))
75
+
76
+ def end(self, result_output: str) -> None:
77
+ """Mark the end of a pattern execution."""
78
+ self._entries.append(RecordEntry(
79
+ timestamp=time.time(),
80
+ event_type="pattern_end",
81
+ agent_name="",
82
+ messages_in=[],
83
+ response=result_output,
84
+ metadata={"duration_seconds": time.time() - self._start_time},
85
+ ))
86
+
87
+ def save(self, path: str | Path) -> None:
88
+ """Save recorded entries to a JSONL file."""
89
+ p = Path(path)
90
+ p.parent.mkdir(parents=True, exist_ok=True)
91
+ with p.open("w") as f:
92
+ for entry in self._entries:
93
+ f.write(json.dumps(asdict(entry), default=str) + "\n")
94
+
95
+ @classmethod
96
+ def load(cls, path: str | Path) -> list[RecordEntry]:
97
+ """Load recorded entries from a JSONL file."""
98
+ entries: list[RecordEntry] = []
99
+ with Path(path).open() as f:
100
+ for line in f:
101
+ data = json.loads(line.strip())
102
+ entries.append(RecordEntry(**data))
103
+ return entries
104
+
105
+ @property
106
+ def entries(self) -> list[RecordEntry]:
107
+ return list(self._entries)
108
+
109
+ @property
110
+ def llm_calls(self) -> list[RecordEntry]:
111
+ return [e for e in self._entries if e.event_type == "llm_call"]
pyagent_trace/spans.py ADDED
@@ -0,0 +1,115 @@
1
+ """PatternSpanEmitter: create OTel spans for pattern executions.
2
+
3
+ Emits structured spans with pyagent.* attributes for every pattern run,
4
+ agent call, and routing/compression decision.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from opentelemetry import trace
12
+ from opentelemetry.trace import Span, StatusCode
13
+
14
+ from pyagent_trace.attributes import PyAgentAttributes
15
+
16
+ _tracer = trace.get_tracer("pyagent", "0.1.0")
17
+
18
+
19
+ class PatternSpanEmitter:
20
+ """Emit OTel spans for pattern executions.
21
+
22
+ Usage:
23
+ emitter = PatternSpanEmitter()
24
+ with emitter.pattern_span("debate", {"rounds": 3}) as span:
25
+ # ... pattern logic ...
26
+ emitter.set_result(span, result)
27
+ """
28
+
29
+ def pattern_span(
30
+ self,
31
+ pattern_type: str,
32
+ attributes: dict[str, Any] | None = None,
33
+ ) -> Span:
34
+ """Start a span for a pattern execution."""
35
+ span = _tracer.start_span(
36
+ f"pyagent.pattern.{pattern_type}",
37
+ attributes={
38
+ PyAgentAttributes.PATTERN_TYPE: pattern_type,
39
+ **(attributes or {}),
40
+ },
41
+ )
42
+ return span
43
+
44
+ def agent_span(
45
+ self,
46
+ agent_name: str,
47
+ parent_span: Span | None = None,
48
+ attributes: dict[str, Any] | None = None,
49
+ ) -> Span:
50
+ """Start a span for an individual agent call."""
51
+ ctx = trace.set_span_in_context(parent_span) if parent_span else None
52
+ span = _tracer.start_span(
53
+ f"pyagent.agent.{agent_name}",
54
+ context=ctx,
55
+ attributes={
56
+ PyAgentAttributes.AGENT_NAME: agent_name,
57
+ **(attributes or {}),
58
+ },
59
+ )
60
+ return span
61
+
62
+ @staticmethod
63
+ def set_pattern_result(
64
+ span: Span,
65
+ output_length: int,
66
+ rounds: int | None = None,
67
+ consensus: float | None = None,
68
+ escalated: bool = False,
69
+ duration_ms: float = 0.0,
70
+ token_estimate: int = 0,
71
+ cost_estimate: float = 0.0,
72
+ ) -> None:
73
+ """Set result attributes on a pattern span."""
74
+ if rounds is not None:
75
+ span.set_attribute(PyAgentAttributes.PATTERN_ROUNDS, rounds)
76
+ if consensus is not None:
77
+ span.set_attribute(PyAgentAttributes.PATTERN_CONSENSUS, consensus)
78
+ span.set_attribute(PyAgentAttributes.PATTERN_ESCALATED, escalated)
79
+ span.set_attribute(PyAgentAttributes.EXEC_DURATION_MS, duration_ms)
80
+ span.set_attribute(PyAgentAttributes.EXEC_TOKEN_ESTIMATE, token_estimate)
81
+ span.set_attribute(PyAgentAttributes.COST_TOTAL_USD, cost_estimate)
82
+ span.set_status(StatusCode.OK)
83
+
84
+ @staticmethod
85
+ def set_routing_info(
86
+ span: Span,
87
+ difficulty: int,
88
+ selected_model: str,
89
+ cost_estimate: float,
90
+ category: str = "",
91
+ ) -> None:
92
+ """Set routing attributes on a span."""
93
+ span.set_attribute(PyAgentAttributes.ROUTER_DIFFICULTY, difficulty)
94
+ span.set_attribute(PyAgentAttributes.ROUTER_SELECTED_MODEL, selected_model)
95
+ span.set_attribute(PyAgentAttributes.ROUTER_COST_ESTIMATE, cost_estimate)
96
+ if category:
97
+ span.set_attribute(PyAgentAttributes.ROUTER_DIFFICULTY_CATEGORY, category)
98
+
99
+ @staticmethod
100
+ def set_compression_info(
101
+ span: Span,
102
+ input_tokens: int,
103
+ output_tokens: int,
104
+ savings_pct: float,
105
+ ) -> None:
106
+ """Set compression attributes on a span."""
107
+ span.set_attribute(PyAgentAttributes.COMPRESS_INPUT_TOKENS, input_tokens)
108
+ span.set_attribute(PyAgentAttributes.COMPRESS_OUTPUT_TOKENS, output_tokens)
109
+ span.set_attribute(PyAgentAttributes.COMPRESS_SAVINGS_PCT, savings_pct)
110
+
111
+ @staticmethod
112
+ def set_error(span: Span, error: Exception) -> None:
113
+ """Record an error on a span."""
114
+ span.set_status(StatusCode.ERROR, str(error))
115
+ span.record_exception(error)
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyagent-trace
3
+ Version: 0.1.0
4
+ Summary: Pattern-aware OpenTelemetry tracing for multi-agent LLM systems
5
+ License: MIT
6
+ Keywords: LLM,OpenTelemetry,agents,observability,tracing
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: opentelemetry-api>=1.25
17
+ Requires-Dist: opentelemetry-sdk>=1.25
18
+ Requires-Dist: pyagent-patterns>=0.1.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: mypy>=1.10; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
22
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.5; extra == 'dev'
24
+ Provides-Extra: langfuse
25
+ Requires-Dist: langfuse>=2.0; extra == 'langfuse'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pyagent-trace
29
+
30
+ **Pattern-aware OpenTelemetry tracing** for multi-agent LLM systems. Track costs, record interactions, debug with replay.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install pyagent-trace # Core (CostTracker, Recorder work without OTel)
36
+ pip install pyagent-trace[otel] # With OpenTelemetry spans
37
+ ```
38
+
39
+ ## Components
40
+
41
+ - **CostTracker** — Accumulate costs by pattern, agent, and model (no OTel required)
42
+ - **Recorder** — Record all messages + LLM responses to JSONL for replay/debug
43
+ - **PatternSpanEmitter** — Create OTel spans for pattern executions (requires OTel)
44
+ - **traced_pattern / traced_agent** — Decorators for automatic tracing (requires OTel)
45
+ - **PyAgentAttributes** — Custom `pyagent.*` attribute constants
46
+
47
+ ## Quick Example (no OTel required)
48
+
49
+ ```python
50
+ from pyagent_trace import CostTracker, Recorder
51
+
52
+ tracker = CostTracker()
53
+ tracker.record("debate", "bull_agent", "gpt-4o", 500, 200, 0.003)
54
+ print(f"Total: ${tracker.total_cost:.4f}")
55
+ print(f"By model: {tracker.by_model()}")
56
+
57
+ recorder = Recorder()
58
+ recorder.start("debate")
59
+ recorder.record_llm_call("bull", messages, "Bull case: ...")
60
+ recorder.end("Final decision")
61
+ recorder.save("traces/debug.jsonl")
62
+ ```
@@ -0,0 +1,10 @@
1
+ pyagent_trace/__init__.py,sha256=hG7qwzYIFma6GmjlaPzTmI07KAS7ABPDg8PTF9mNrwA,930
2
+ pyagent_trace/attributes.py,sha256=Gn5G50XJ_TriB_QmXc5fkY3XX4vWLrY96W5hqf5OeaM,1669
3
+ pyagent_trace/cost.py,sha256=i8qQQXoXaOWHGp4JcMNdZ4LE2Nfp5LXB-g-BaN-Xz9Q,2589
4
+ pyagent_trace/decorators.py,sha256=JCZxmtDnpe4Ncj4-4Q5OXvCMhwqTVPKDKz1hFwuXMDc,3066
5
+ pyagent_trace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ pyagent_trace/recorder.py,sha256=fPCV-ACt_6-bq_u6RKc02gUiS8KEp_oq5YMSiBvNIlo,3389
7
+ pyagent_trace/spans.py,sha256=ioQiZ3dl2Jr4sLEQHNxZYbHafQHtLJYWH02j1i-eYVE,3924
8
+ pyagent_trace-0.1.0.dist-info/METADATA,sha256=JJB-SZwnWqcAXufztxvUggRGcUY83OaUR67TuBFmUSc,2241
9
+ pyagent_trace-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ pyagent_trace-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any