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.
@@ -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
@@ -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())