agentdynamics 0.4.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.
- agentdynamics/__init__.py +12 -0
- agentdynamics/__main__.py +322 -0
- agentdynamics/analysis.py +755 -0
- agentdynamics/autotrace.py +697 -0
- agentdynamics/bootstrap/sitecustomize.py +27 -0
- agentdynamics/collectors/__init__.py +0 -0
- agentdynamics/collectors/aegis_audit.py +72 -0
- agentdynamics/collectors/claude_code.py +257 -0
- agentdynamics/collectors/generic.py +103 -0
- agentdynamics/collectors/inbox.py +95 -0
- agentdynamics/collectors/langfuse.py +98 -0
- agentdynamics/collectors/langsmith.py +258 -0
- agentdynamics/collectors/otlp.py +349 -0
- agentdynamics/collectors/spans.py +202 -0
- agentdynamics/config.py +139 -0
- agentdynamics/engine.py +376 -0
- agentdynamics/flows.py +142 -0
- agentdynamics/govern.py +231 -0
- agentdynamics/integrations/__init__.py +1 -0
- agentdynamics/integrations/aegis.py +352 -0
- agentdynamics/phases.py +82 -0
- agentdynamics/pricing.py +69 -0
- agentdynamics/privacy.py +41 -0
- agentdynamics/sdk.py +105 -0
- agentdynamics/server.py +949 -0
- agentdynamics/slo.py +84 -0
- agentdynamics/store.py +228 -0
- agentdynamics/web/app.js +1069 -0
- agentdynamics/web/charts.js +185 -0
- agentdynamics/web/index.html +40 -0
- agentdynamics/web/style.css +244 -0
- agentdynamics-0.4.0.dist-info/METADATA +195 -0
- agentdynamics-0.4.0.dist-info/RECORD +37 -0
- agentdynamics-0.4.0.dist-info/WHEEL +5 -0
- agentdynamics-0.4.0.dist-info/entry_points.txt +2 -0
- agentdynamics-0.4.0.dist-info/licenses/LICENSE +202 -0
- agentdynamics-0.4.0.dist-info/top_level.txt +1 -0
agentdynamics/pricing.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Token pricing (USD per 1M tokens). Anthropic first-party list prices.
|
|
2
|
+
|
|
3
|
+
Cache writes are billed at 1.25x input (5-minute TTL) or 2x input (1-hour TTL);
|
|
4
|
+
cache reads at 0.1x input. Override any entry via `pricing.json` in the data dir.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# model-id prefix -> (input, output)
|
|
10
|
+
BASE = {
|
|
11
|
+
"claude-fable-5-1": (10.0, 50.0),
|
|
12
|
+
"claude-mythos-5-1": (10.0, 50.0),
|
|
13
|
+
"claude-fable-5": (10.0, 50.0),
|
|
14
|
+
"claude-opus-5": (5.0, 25.0),
|
|
15
|
+
"claude-opus-4-8": (5.0, 25.0),
|
|
16
|
+
"claude-opus-4-7": (5.0, 25.0),
|
|
17
|
+
"claude-opus-4-6": (5.0, 25.0),
|
|
18
|
+
"claude-opus-4-5": (5.0, 25.0),
|
|
19
|
+
"claude-opus-4": (15.0, 75.0),
|
|
20
|
+
"claude-sonnet-5": (2.0, 10.0),
|
|
21
|
+
"claude-sonnet-4": (3.0, 15.0),
|
|
22
|
+
"claude-haiku-4-5": (1.0, 5.0),
|
|
23
|
+
"claude-3-5-haiku": (0.8, 4.0),
|
|
24
|
+
}
|
|
25
|
+
# Explicit cache-read overrides where the rate is not 0.1x input.
|
|
26
|
+
CACHE_READ_OVERRIDE = {"claude-fable-5-1": 0.25}
|
|
27
|
+
|
|
28
|
+
_overrides = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_overrides(data_dir):
|
|
32
|
+
path = os.path.join(data_dir, "pricing.json")
|
|
33
|
+
if os.path.exists(path):
|
|
34
|
+
with open(path, encoding="utf-8") as f:
|
|
35
|
+
_overrides.update(json.load(f))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def rates(model):
|
|
39
|
+
"""Return dict of per-1M-token rates for a model id (longest prefix match)."""
|
|
40
|
+
model = (model or "").lower()
|
|
41
|
+
if model in _overrides:
|
|
42
|
+
return _overrides[model]
|
|
43
|
+
best = None
|
|
44
|
+
for prefix in BASE:
|
|
45
|
+
if model.startswith(prefix) and (best is None or len(prefix) > len(best)):
|
|
46
|
+
best = prefix
|
|
47
|
+
if best is None:
|
|
48
|
+
return None
|
|
49
|
+
inp, out = BASE[best]
|
|
50
|
+
return {
|
|
51
|
+
"input": inp,
|
|
52
|
+
"output": out,
|
|
53
|
+
"cache_write_5m": inp * 1.25,
|
|
54
|
+
"cache_write_1h": inp * 2.0,
|
|
55
|
+
"cache_read": CACHE_READ_OVERRIDE.get(best, inp * 0.1),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cost(model, input_tokens=0, output_tokens=0, cache_read=0, cache_write_5m=0, cache_write_1h=0):
|
|
60
|
+
r = rates(model)
|
|
61
|
+
if not r:
|
|
62
|
+
return 0.0
|
|
63
|
+
return (
|
|
64
|
+
input_tokens * r["input"]
|
|
65
|
+
+ output_tokens * r["output"]
|
|
66
|
+
+ cache_read * r["cache_read"]
|
|
67
|
+
+ cache_write_5m * r["cache_write_5m"]
|
|
68
|
+
+ cache_write_1h * r["cache_write_1h"]
|
|
69
|
+
) / 1_000_000
|
agentdynamics/privacy.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""PII / secret redaction applied to every stored text field before it reaches the database."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
PATTERNS = {
|
|
5
|
+
"email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
|
|
6
|
+
"api_key": r"\b(sk-[A-Za-z0-9_-]{16,}|sk-ant-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,}|xox[abpr]-[A-Za-z0-9-]{10,}|lsv2_[A-Za-z0-9_]{20,}|AIza[0-9A-Za-z_-]{30,})",
|
|
7
|
+
"bearer": r"[Bb]earer\s+[A-Za-z0-9._~+/=-]{16,}",
|
|
8
|
+
"aws_key": r"\bAKIA[0-9A-Z]{16}\b",
|
|
9
|
+
"credit_card": r"\b(?:\d[ -]?){13,16}\b",
|
|
10
|
+
}
|
|
11
|
+
TEXT_FIELDS = ("text", "input_preview", "error", "target")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Redactor:
|
|
15
|
+
def __init__(self, privacy_cfg):
|
|
16
|
+
self.store_content = privacy_cfg.get("store_content", True)
|
|
17
|
+
pats = [PATTERNS[n] for n in privacy_cfg.get("redact", []) if n in PATTERNS]
|
|
18
|
+
pats += privacy_cfg.get("extra_patterns", [])
|
|
19
|
+
self.rx = re.compile("|".join(f"(?:{p})" for p in pats)) if pats else None
|
|
20
|
+
|
|
21
|
+
def text(self, s):
|
|
22
|
+
if not s or not isinstance(s, str):
|
|
23
|
+
return s
|
|
24
|
+
if not self.store_content:
|
|
25
|
+
return f"[content not stored · {len(s)} chars]"
|
|
26
|
+
return self.rx.sub("[REDACTED]", s) if self.rx else s
|
|
27
|
+
|
|
28
|
+
def run(self, run):
|
|
29
|
+
"""Redact in place. Structural fields (names, timings, tokens) are kept."""
|
|
30
|
+
if self.rx is None and self.store_content:
|
|
31
|
+
return run
|
|
32
|
+
run["title"] = self.text(run.get("title"))
|
|
33
|
+
for s in run.get("steps", []):
|
|
34
|
+
for f in TEXT_FIELDS:
|
|
35
|
+
if s.get(f):
|
|
36
|
+
# tool targets such as file paths are metadata, keep them unless content storage is off
|
|
37
|
+
if f == "target" and self.store_content:
|
|
38
|
+
s[f] = self.rx.sub("[REDACTED]", s[f]) if self.rx else s[f]
|
|
39
|
+
else:
|
|
40
|
+
s[f] = self.text(s[f])
|
|
41
|
+
return run
|
agentdynamics/sdk.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tiny tracing SDK for custom agents. Stdlib only.
|
|
2
|
+
|
|
3
|
+
tracer = Tracer(agent="support-bot", endpoint="http://127.0.0.1:8787")
|
|
4
|
+
with tracer.task("Refund order #42") as task:
|
|
5
|
+
task.record_anthropic(response) # Anthropic SDK Message
|
|
6
|
+
with task.tool("lookup_order", {"id": 42}) as call:
|
|
7
|
+
call.output(result)
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _ToolCall:
|
|
16
|
+
def __init__(self, run, name, inp):
|
|
17
|
+
self.step = {"kind": "tool", "name": name, "input": inp, "ts": time.time(), "is_error": False, "output_chars": 0}
|
|
18
|
+
run._steps.append(self.step)
|
|
19
|
+
|
|
20
|
+
def output(self, value):
|
|
21
|
+
self.step["output_chars"] = len(value if isinstance(value, str) else json.dumps(value, default=str))
|
|
22
|
+
|
|
23
|
+
def error(self, message):
|
|
24
|
+
self.step["is_error"] = True
|
|
25
|
+
self.step["error"] = str(message)[:300]
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, et, ev, tb):
|
|
31
|
+
self.step["end_ts"] = time.time()
|
|
32
|
+
if ev is not None:
|
|
33
|
+
self.error(ev)
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Run:
|
|
38
|
+
def __init__(self, tracer, prompt, run_id=None, parent_id=None):
|
|
39
|
+
self.tracer = tracer
|
|
40
|
+
self.id = run_id or f"{tracer.agent}-{uuid.uuid4().hex[:12]}"
|
|
41
|
+
self.parent_id = parent_id
|
|
42
|
+
self._steps = []
|
|
43
|
+
self._last = time.time()
|
|
44
|
+
if prompt is not None:
|
|
45
|
+
self.prompt(prompt)
|
|
46
|
+
|
|
47
|
+
def prompt(self, text):
|
|
48
|
+
"""Start a new task inside this run (a follow-up user request)."""
|
|
49
|
+
self._steps.append({"kind": "prompt", "ts": time.time(), "text": text})
|
|
50
|
+
self._last = time.time()
|
|
51
|
+
|
|
52
|
+
def llm(self, model, input_tokens=0, output_tokens=0, cache_read=0, cache_write=0, stop_reason=None,
|
|
53
|
+
text="", start_ts=None, end_ts=None, cost=None):
|
|
54
|
+
end_ts = end_ts or time.time()
|
|
55
|
+
self._steps.append({"kind": "llm", "model": model, "ts": start_ts or self._last, "end_ts": end_ts,
|
|
56
|
+
"input_tokens": input_tokens, "output_tokens": output_tokens, "cache_read": cache_read,
|
|
57
|
+
"cache_write": cache_write, "stop_reason": stop_reason, "text": (text or "")[:600], "cost": cost})
|
|
58
|
+
self._last = end_ts
|
|
59
|
+
|
|
60
|
+
def record_anthropic(self, message, start_ts=None):
|
|
61
|
+
"""Record an Anthropic SDK `Message` (or dict) using its usage block."""
|
|
62
|
+
m = message if isinstance(message, dict) else message.model_dump()
|
|
63
|
+
u = m.get("usage") or {}
|
|
64
|
+
text = "".join(b.get("text", "") for b in m.get("content") or [] if b.get("type") == "text")
|
|
65
|
+
self.llm(m.get("model"), u.get("input_tokens") or 0, u.get("output_tokens") or 0,
|
|
66
|
+
u.get("cache_read_input_tokens") or 0, u.get("cache_creation_input_tokens") or 0,
|
|
67
|
+
m.get("stop_reason"), text, start_ts=start_ts)
|
|
68
|
+
|
|
69
|
+
def tool(self, name, inp=None):
|
|
70
|
+
return _ToolCall(self, name, inp or {})
|
|
71
|
+
|
|
72
|
+
def payload(self):
|
|
73
|
+
return {"id": self.id, "agent": self.tracer.agent, "project": self.tracer.project, "parent_id": self.parent_id,
|
|
74
|
+
"source": "sdk", "steps": self._steps}
|
|
75
|
+
|
|
76
|
+
def flush(self):
|
|
77
|
+
return self.tracer.send(self.payload())
|
|
78
|
+
|
|
79
|
+
def __enter__(self):
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, et, ev, tb):
|
|
83
|
+
if ev is not None:
|
|
84
|
+
self._steps.append({"kind": "notice", "name": "api_error", "ts": time.time(), "text": repr(ev)[:300]})
|
|
85
|
+
try:
|
|
86
|
+
self.flush()
|
|
87
|
+
except OSError as ex: # monitoring must never break the agent
|
|
88
|
+
print(f"[agentdynamics] could not send run {self.id}: {ex}")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Tracer:
|
|
93
|
+
def __init__(self, agent, project=None, endpoint="http://127.0.0.1:8787"):
|
|
94
|
+
self.agent = agent
|
|
95
|
+
self.project = project or agent
|
|
96
|
+
self.endpoint = endpoint.rstrip("/")
|
|
97
|
+
|
|
98
|
+
def task(self, prompt, run_id=None, parent_id=None):
|
|
99
|
+
return Run(self, prompt, run_id, parent_id)
|
|
100
|
+
|
|
101
|
+
def send(self, payload):
|
|
102
|
+
req = urllib.request.Request(f"{self.endpoint}/api/ingest", data=json.dumps(payload, default=str).encode(),
|
|
103
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
104
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
105
|
+
return json.loads(r.read())
|