agentloop-profiler 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.

Potentially problematic release.


This version of agentloop-profiler might be problematic. Click here for more details.

Files changed (50) hide show
  1. agentloop/__init__.py +68 -0
  2. agentloop/audit.py +34 -0
  3. agentloop/autoinstrument.py +102 -0
  4. agentloop/ci.py +135 -0
  5. agentloop/cli.py +876 -0
  6. agentloop/client.py +170 -0
  7. agentloop/config.py +50 -0
  8. agentloop/costs.py +15 -0
  9. agentloop/decorators.py +160 -0
  10. agentloop/demo.py +283 -0
  11. agentloop/doctor.py +344 -0
  12. agentloop/events.py +49 -0
  13. agentloop/exporters.py +60 -0
  14. agentloop/findings.py +215 -0
  15. agentloop/graph.py +312 -0
  16. agentloop/integrations/__init__.py +1 -0
  17. agentloop/integrations/crewai.py +144 -0
  18. agentloop/integrations/langgraph.py +140 -0
  19. agentloop/integrations/openai.py +369 -0
  20. agentloop/integrations/openai_agents.py +212 -0
  21. agentloop/integrations/sdk.py +82 -0
  22. agentloop/integrations/vercel_ai.py +83 -0
  23. agentloop/issues.py +88 -0
  24. agentloop/markdown.py +102 -0
  25. agentloop/metrics.py +123 -0
  26. agentloop/optimizer.py +323 -0
  27. agentloop/otel.py +302 -0
  28. agentloop/otel_ids.py +81 -0
  29. agentloop/patches.py +583 -0
  30. agentloop/plan_export.py +72 -0
  31. agentloop/quality.py +222 -0
  32. agentloop/replay.py +424 -0
  33. agentloop/runtime.py +244 -0
  34. agentloop/savings.py +135 -0
  35. agentloop/server.py +292 -0
  36. agentloop/store.py +840 -0
  37. agentloop/timing.py +124 -0
  38. agentloop/tracer.py +356 -0
  39. agentloop/value.py +243 -0
  40. agentloop/version.py +3 -0
  41. agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
  42. agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
  43. agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
  44. agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
  45. agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
  46. agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
  47. agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
  48. dashboard/__init__.py +1 -0
  49. dashboard/app.py +698 -0
  50. dashboard/value_view.py +101 -0
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from agentloop.markdown import (
8
+ markdown_code_span,
9
+ markdown_heading,
10
+ markdown_table_cell,
11
+ markdown_text,
12
+ )
13
+
14
+
15
+ def export_optimization_json(plan: dict[str, Any], path: str | Path) -> Path:
16
+ out = Path(path)
17
+ out.parent.mkdir(parents=True, exist_ok=True)
18
+ out.write_text(json.dumps(plan, indent=2), encoding="utf-8")
19
+ return out
20
+
21
+
22
+ def export_optimization_markdown(plan: dict[str, Any], path: str | Path) -> Path:
23
+ out = Path(path)
24
+ out.parent.mkdir(parents=True, exist_ok=True)
25
+ current = plan["current"]
26
+ after = plan["estimated_after"]
27
+ lines = [
28
+ f"# AgentLoop Optimization Plan: {markdown_heading(plan['name'])}",
29
+ "",
30
+ f"- Run ID: {markdown_code_span(plan['run_id'])}",
31
+ f"- Current runtime: {current['runtime_ms'] / 1000:.2f}s",
32
+ f"- Estimated optimized runtime: {after['runtime_ms'] / 1000:.2f}s",
33
+ f"- Estimated latency reduction: {after['latency_reduction_pct']:.2f}%",
34
+ f"- Current cost: ${current['estimated_cost_usd']:.4f}",
35
+ f"- Estimated optimized cost: ${after['estimated_cost_usd']:.4f}",
36
+ f"- Estimated cost reduction: {after['cost_reduction_pct']:.2f}%",
37
+ f"- Repeated context ratio: {current['repeated_context_ratio']:.1%}",
38
+ f"- Retry count: {current['retry_count']}",
39
+ "",
40
+ "## Optimization cards",
41
+ "",
42
+ ]
43
+ cards = plan.get("optimization_cards", [])
44
+ if not cards:
45
+ lines.append(
46
+ "No major optimization opportunities detected yet. Collect more traces for stronger recommendations."
47
+ )
48
+ for index, card in enumerate(cards, start=1):
49
+ lines.extend(
50
+ [
51
+ f"### {index}. {markdown_heading(card['title'])}",
52
+ "",
53
+ f"- Type: {markdown_code_span(card['type'])}",
54
+ f"- Confidence: {markdown_text(card['confidence'])}",
55
+ f"- Why: {markdown_text(card['why'])}",
56
+ f"- Rewrite hint: {markdown_text(card['rewrite_hint'])}",
57
+ f"- Estimated latency savings: {card['estimated_latency_savings_ms'] / 1000:.2f}s",
58
+ f"- Estimated cost savings: ${card['estimated_cost_savings_usd']:.4f}",
59
+ "",
60
+ ]
61
+ )
62
+ lines.extend(
63
+ ["## Bottlenecks", "", "| Name | Type | Duration | Runtime share |", "|---|---|---:|---:|"]
64
+ )
65
+ for item in plan.get("graph", {}).get("bottlenecks", []):
66
+ lines.append(
67
+ f"| {markdown_table_cell(item['name'])} | "
68
+ f"{markdown_table_cell(item['event_type'])} | "
69
+ f"{item['duration_ms'] / 1000:.2f}s | {item['runtime_share']:.1%} |"
70
+ )
71
+ out.write_text("\n".join(lines) + "\n", encoding="utf-8")
72
+ return out
agentloop/quality.py ADDED
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import importlib
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agentloop.markdown import markdown_table_cell
10
+
11
+ _MAX_GLOB_PATTERN_LENGTH = 256
12
+ _MAX_GLOB_TEXT_LENGTH = 1_000_000
13
+
14
+
15
+ def load_quality_fixtures(path: str | Path) -> list[dict[str, Any]]:
16
+ payload = json.loads(Path(path).read_text(encoding="utf-8"))
17
+ if isinstance(payload, list):
18
+ return payload
19
+ return list(payload.get("fixtures", []))
20
+
21
+
22
+ def build_quality_report(
23
+ fixtures: list[dict[str, Any]],
24
+ *,
25
+ baseline_trace: Any | None = None,
26
+ candidate_trace: Any | None = None,
27
+ min_score: float | None = None,
28
+ ) -> dict[str, Any]:
29
+ cases = []
30
+ for index, fixture in enumerate(fixtures, start=1):
31
+ case_id = str(fixture.get("id") or f"case_{index:03d}")
32
+ baseline_output = _output_for_side(fixture, "baseline", baseline_trace)
33
+ candidate_output = _output_for_side(fixture, "candidate", candidate_trace)
34
+ scorer = fixture.get("scorer", {"type": "exact_match"})
35
+ baseline_result = score_output(baseline_output, fixture, scorer)
36
+ candidate_result = score_output(candidate_output, fixture, scorer)
37
+ cases.append(
38
+ {
39
+ "case_id": case_id,
40
+ "input": fixture.get("input"),
41
+ "scorer": scorer,
42
+ "baseline": baseline_result,
43
+ "candidate": candidate_result,
44
+ "passed": candidate_result["passed"],
45
+ }
46
+ )
47
+
48
+ baseline_score = _average(case["baseline"]["score"] for case in cases)
49
+ candidate_score = _average(case["candidate"]["score"] for case in cases)
50
+ failed_cases = [case for case in cases if not case["passed"]]
51
+ passed = not failed_cases
52
+ if min_score is not None:
53
+ passed = passed and candidate_score >= min_score
54
+
55
+ return {
56
+ "case_count": len(cases),
57
+ "passed": passed,
58
+ "baseline_score": round(baseline_score, 4),
59
+ "candidate_score": round(candidate_score, 4),
60
+ "quality_delta": round(candidate_score - baseline_score, 4),
61
+ "min_score": min_score,
62
+ "failed_case_count": len(failed_cases),
63
+ "failed_cases": failed_cases,
64
+ "cases": cases,
65
+ }
66
+
67
+
68
+ def score_output(output: Any, fixture: dict[str, Any], scorer: dict[str, Any]) -> dict[str, Any]:
69
+ scorer_type = str(scorer.get("type", "exact_match"))
70
+ if scorer_type == "exact_match":
71
+ expected = fixture.get("expected", scorer.get("expected"))
72
+ passed = _normalize(output) == _normalize(expected)
73
+ return _result(passed, "exact match" if passed else "output did not exactly match expected")
74
+ if scorer_type == "contains":
75
+ expected = str(fixture.get("expected", scorer.get("text", "")))
76
+ passed = expected in str(output or "")
77
+ return _result(
78
+ passed, "required text present" if passed else f"missing required text: {expected}"
79
+ )
80
+ if scorer_type == "glob":
81
+ pattern = str(scorer.get("pattern") or fixture.get("expected") or "")
82
+ text = str(output or "")
83
+ if len(pattern) > _MAX_GLOB_PATTERN_LENGTH:
84
+ return _result(False, "glob pattern exceeds the 256-character safety limit")
85
+ if len(text) > _MAX_GLOB_TEXT_LENGTH:
86
+ return _result(False, "output exceeds the 1,000,000-character glob safety limit")
87
+ passed = fnmatch.fnmatchcase(text, pattern)
88
+ return _result(passed, "glob matched" if passed else "glob did not match")
89
+ if scorer_type == "regex":
90
+ return _result(
91
+ False,
92
+ "raw regular-expression scorers are disabled; use glob, contains, or exact_match",
93
+ )
94
+ if scorer_type in {"required_fields", "json_schema"}:
95
+ return _score_required_fields(output, scorer)
96
+ if scorer_type == "json_subset":
97
+ return _score_json_subset(output, scorer.get("expected", fixture.get("expected", {})))
98
+ if scorer_type == "custom":
99
+ return _score_custom(output, fixture, scorer)
100
+ return _result(False, f"unknown scorer type: {scorer_type}")
101
+
102
+
103
+ def _score_required_fields(output: Any, scorer: dict[str, Any]) -> dict[str, Any]:
104
+ required = list(scorer.get("required") or scorer.get("required_fields") or [])
105
+ data = _json_value(output)
106
+ if not isinstance(data, dict):
107
+ return _result(False, "output is not a JSON object")
108
+ missing = [field for field in required if field not in data or _is_empty(data[field])]
109
+ return _result(
110
+ not missing,
111
+ "required fields present" if not missing else f"missing fields: {', '.join(missing)}",
112
+ )
113
+
114
+
115
+ def _score_json_subset(output: Any, expected: Any) -> dict[str, Any]:
116
+ data = _json_value(output)
117
+ expected_data = _json_value(expected)
118
+ if not isinstance(data, dict) or not isinstance(expected_data, dict):
119
+ return _result(False, "output and expected must be JSON objects")
120
+ mismatches = [key for key, value in expected_data.items() if data.get(key) != value]
121
+ return _result(
122
+ not mismatches,
123
+ "JSON subset matched" if not mismatches else f"mismatched keys: {', '.join(mismatches)}",
124
+ )
125
+
126
+
127
+ def _score_custom(output: Any, fixture: dict[str, Any], scorer: dict[str, Any]) -> dict[str, Any]:
128
+ target = str(scorer.get("callable") or "")
129
+ if ":" not in target:
130
+ return _result(False, "custom scorer callable must use module:function")
131
+ module_name, function_name = target.split(":", 1)
132
+ fn = getattr(importlib.import_module(module_name), function_name)
133
+ raw = fn(output, fixture, scorer)
134
+ if isinstance(raw, dict):
135
+ return {
136
+ "score": float(raw.get("score", 1.0 if raw.get("passed") else 0.0)),
137
+ "passed": bool(raw.get("passed")),
138
+ "detail": str(raw.get("detail", "")),
139
+ }
140
+ passed = bool(raw)
141
+ return _result(passed, "custom scorer passed" if passed else "custom scorer failed")
142
+
143
+
144
+ def quality_report_to_markdown(report: dict[str, Any]) -> str:
145
+ status = "passed" if report["passed"] else "failed"
146
+ lines = [
147
+ "# AgentLoop Quality Report",
148
+ "",
149
+ f"- Status: {status}",
150
+ f"- Cases: {report['case_count']}",
151
+ f"- Baseline score: {report['baseline_score']:.4f}",
152
+ f"- Candidate score: {report['candidate_score']:.4f}",
153
+ f"- Quality delta: {report['quality_delta']:.4f}",
154
+ f"- Failed cases: {report['failed_case_count']}",
155
+ "",
156
+ "## Cases",
157
+ "",
158
+ "| Case | Status | Baseline | Candidate | Detail |",
159
+ "|---|---|---:|---:|---|",
160
+ ]
161
+ for case in report["cases"]:
162
+ status = "pass" if case["passed"] else "fail"
163
+ lines.append(
164
+ "| "
165
+ f"{markdown_table_cell(case['case_id'])} | "
166
+ f"{status} | "
167
+ f"{case['baseline']['score']:.4f} | "
168
+ f"{case['candidate']['score']:.4f} | "
169
+ f"{markdown_table_cell(case['candidate']['detail'])} |"
170
+ )
171
+ return "\n".join(lines).rstrip() + "\n"
172
+
173
+
174
+ def _output_for_side(fixture: dict[str, Any], side: str, trace: Any | None) -> Any:
175
+ key = f"{side}_output"
176
+ if key in fixture:
177
+ return fixture[key]
178
+ if side == "candidate" and "output" in fixture:
179
+ return fixture["output"]
180
+ return _trace_output(trace)
181
+
182
+
183
+ def _trace_output(trace: Any | None) -> Any:
184
+ if trace is None:
185
+ return ""
186
+ metadata = getattr(trace, "metadata", {}) or {}
187
+ if "output" in metadata:
188
+ return metadata["output"]
189
+ for event in reversed(getattr(trace, "events", [])):
190
+ if getattr(event, "event_type", "") == "model_call" and getattr(event, "output_text", None):
191
+ return event.output_text
192
+ return ""
193
+
194
+
195
+ def _json_value(value: Any) -> Any:
196
+ if isinstance(value, str):
197
+ try:
198
+ return json.loads(value)
199
+ except json.JSONDecodeError:
200
+ return value
201
+ return value
202
+
203
+
204
+ def _normalize(value: Any) -> str:
205
+ if isinstance(value, dict | list):
206
+ return json.dumps(value, sort_keys=True)
207
+ return str(value or "").strip()
208
+
209
+
210
+ def _average(values: Any) -> float:
211
+ items = list(values)
212
+ if not items:
213
+ return 0.0
214
+ return sum(float(item) for item in items) / len(items)
215
+
216
+
217
+ def _result(passed: bool, detail: str) -> dict[str, Any]:
218
+ return {"score": 1.0 if passed else 0.0, "passed": passed, "detail": detail}
219
+
220
+
221
+ def _is_empty(value: Any) -> bool:
222
+ return value is None or value == "" or value == [] or value == {}