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,83 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.events import AgentEvent, new_event_id, utc_now_iso
6
+ from agentloop.tracer import AgentTrace
7
+
8
+
9
+ def _get(data: Any, key: str, default: Any = None) -> Any:
10
+ if isinstance(data, dict):
11
+ return data.get(key, default)
12
+ return getattr(data, key, default)
13
+
14
+
15
+ def trace_from_vercel_ai_events(
16
+ events: list[dict[str, Any]],
17
+ *,
18
+ name: str = "vercel_ai_run",
19
+ run_id: str | None = None,
20
+ metadata: dict[str, Any] | None = None,
21
+ ) -> AgentTrace:
22
+ """Convert Vercel AI SDK-style telemetry events into an AgentLoop trace.
23
+
24
+ The adapter intentionally accepts plain dictionaries so JS/TS apps can POST or
25
+ export telemetry without importing Python code. Expected fields are flexible:
26
+ `type`/`event_type`, `name`, `duration_ms`, `model`, `input_tokens`,
27
+ `output_tokens`, `status`, and `metadata`.
28
+ """
29
+
30
+ trace = AgentTrace(
31
+ name=name, run_id=run_id, metadata={"integration": "vercel_ai", **(metadata or {})}
32
+ )
33
+ for item in events:
34
+ event_type = str(_get(item, "event_type", _get(item, "type", "model_call")))
35
+ if event_type in {"text", "generate", "stream", "model"}:
36
+ event_type = "model_call"
37
+ elif event_type in {"tool", "tool-call", "step"}:
38
+ event_type = "tool_call"
39
+
40
+ trace.add_event(
41
+ AgentEvent(
42
+ event_id=str(_get(item, "event_id", new_event_id())),
43
+ run_id=trace.run_id,
44
+ event_type=event_type,
45
+ name=str(_get(item, "name", event_type)),
46
+ started_at=str(_get(item, "started_at", utc_now_iso())),
47
+ ended_at=str(_get(item, "ended_at", utc_now_iso())),
48
+ duration_ms=float(_get(item, "duration_ms", 0.0) or 0.0),
49
+ parent_id=_get(item, "parent_id"),
50
+ model=_get(item, "model"),
51
+ input_tokens=int(_get(item, "input_tokens", _get(item, "prompt_tokens", 0)) or 0),
52
+ output_tokens=int(
53
+ _get(item, "output_tokens", _get(item, "completion_tokens", 0)) or 0
54
+ ),
55
+ input_text=_get(item, "input_text"),
56
+ output_text=_get(item, "output_text"),
57
+ status=str(_get(item, "status", "ok")),
58
+ error=_get(item, "error"),
59
+ metadata={"integration": "vercel_ai", **(_get(item, "metadata", {}) or {})},
60
+ )
61
+ )
62
+ return trace
63
+
64
+
65
+ TYPESCRIPT_SNIPPET = r"""
66
+ // Minimal Vercel AI SDK telemetry bridge for AgentLoop.
67
+ // Push these events to your backend, then convert them with
68
+ // agentloop.integrations.vercel_ai.trace_from_vercel_ai_events(...).
69
+
70
+ const agentloopEvents: any[] = [];
71
+
72
+ export function recordAgentLoopEvent(event: any) {
73
+ agentloopEvents.push({
74
+ ...event,
75
+ started_at: event.started_at ?? new Date().toISOString(),
76
+ ended_at: event.ended_at ?? new Date().toISOString(),
77
+ });
78
+ }
79
+
80
+ export function getAgentLoopEvents() {
81
+ return agentloopEvents;
82
+ }
83
+ """
agentloop/issues.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.markdown import markdown_code_span, markdown_heading, markdown_text
6
+
7
+ # Generated drafts target this repository, so use only deliberately maintained
8
+ # labels that already exist. Finding type and severity remain structured fields
9
+ # in the draft body instead of creating an unbounded dynamic label taxonomy.
10
+ ISSUE_DRAFT_LABELS = ("enhancement",)
11
+
12
+
13
+ def build_issue_drafts(queue: list[dict[str, Any]], *, limit: int = 5) -> list[dict[str, Any]]:
14
+ drafts = []
15
+ for item in queue:
16
+ if item.get("patchable_count", 0) <= 0:
17
+ continue
18
+ drafts.append(_issue_for_queue_item(item))
19
+ if len(drafts) >= limit:
20
+ break
21
+ return drafts
22
+
23
+
24
+ def issue_drafts_to_markdown(drafts: list[dict[str, Any]]) -> str:
25
+ lines = ["# AgentLoop GitHub Issue Drafts", ""]
26
+ if not drafts:
27
+ lines.append("No patchable optimization queue items found.")
28
+ return "\n".join(lines).rstrip() + "\n"
29
+
30
+ for draft in drafts:
31
+ lines.extend(
32
+ [
33
+ f"## {markdown_heading(draft['title'])}",
34
+ "",
35
+ f"- Labels: {markdown_text(', '.join(map(str, draft['labels'])))}",
36
+ "",
37
+ draft["body"],
38
+ "",
39
+ ]
40
+ )
41
+ return "\n".join(lines).rstrip() + "\n"
42
+
43
+
44
+ def _issue_for_queue_item(item: dict[str, Any]) -> dict[str, Any]:
45
+ title = f"Optimize agent workflow: {item['title']}"
46
+ labels = list(ISSUE_DRAFT_LABELS)
47
+ body = "\n".join(
48
+ [
49
+ "AgentLoop detected a recurring patchable optimization opportunity.",
50
+ "",
51
+ "## Finding",
52
+ "",
53
+ f"- Type: {markdown_code_span(item['type'])}",
54
+ f"- Severity: {markdown_code_span(item['severity'])}",
55
+ f"- Occurrences: {item['occurrence_count']}",
56
+ f"- Affected runs: {item['run_count']}",
57
+ f"- Patchable findings: {item['patchable_count']}",
58
+ f"- Quality risk: {markdown_code_span(item.get('quality_risk', 'unknown'))}",
59
+ f"- Requires scorer: {item.get('requires_scorer', True)}",
60
+ f"- Safe to auto-patch: {item.get('safe_to_auto_patch', False)}",
61
+ f"- Estimated latency savings: {item['estimated_latency_savings_ms'] / 1000:.2f}s",
62
+ f"- Estimated cost savings: ${item['estimated_cost_savings_usd']:.4f}",
63
+ f"- Priority score: {item['priority_score']:.1f}",
64
+ "",
65
+ "## Affected Runs",
66
+ "",
67
+ "\n".join(
68
+ f"- {markdown_code_span(run_id)}" for run_id in item.get("affected_runs", [])[:20]
69
+ ),
70
+ "",
71
+ "## Acceptance Criteria",
72
+ "",
73
+ "- Generate or inspect the AgentLoop patch plan for one affected run.",
74
+ "- Apply a constrained workflow rewrite.",
75
+ "- Run `agentloop replay` against baseline and candidate traces.",
76
+ "- Attach a quality fixture or scorer when quality risk is medium or high.",
77
+ "- Run `agentloop ci` and confirm cost, latency, retry, schema, and quality gates pass.",
78
+ ]
79
+ )
80
+ return {
81
+ "title": title,
82
+ "body": body,
83
+ "labels": labels,
84
+ "queue_id": item["queue_id"],
85
+ "type": item["type"],
86
+ "severity": item["severity"],
87
+ "priority_score": item["priority_score"],
88
+ }
agentloop/markdown.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ _LINE_BREAKS = re.compile(r"\r\n?|\n|\x85|\u2028|\u2029")
7
+ _BACKTICK_RUNS = re.compile(r"`+")
8
+ _SAFE_INFO_STRING = re.compile(r"[A-Za-z0-9_.+-]*\Z")
9
+
10
+ # Markdown treats these characters as inline syntax, while HTML parsers give
11
+ # angle brackets and ampersands additional meaning. Character references keep
12
+ # the rendered text unchanged without relying on escape ordering.
13
+ _INLINE_ESCAPES = {
14
+ "&": "&amp;",
15
+ "<": "&lt;",
16
+ ">": "&gt;",
17
+ "\\": "&#92;",
18
+ "`": "&#96;",
19
+ "*": "&#42;",
20
+ "_": "&#95;",
21
+ "[": "&#91;",
22
+ "]": "&#93;",
23
+ "~": "&#126;",
24
+ }
25
+ _TABLE_TRANSLATION = str.maketrans({"|": "&#124;"})
26
+
27
+
28
+ def markdown_text(value: Any) -> str:
29
+ """Render an untrusted value as single-line Markdown text."""
30
+
31
+ content = _single_line(value)
32
+ return "".join(
33
+ character
34
+ if character == "_" and _underscore_is_inside_identifier(content, index)
35
+ else _INLINE_ESCAPES.get(character, character)
36
+ for index, character in enumerate(content)
37
+ )
38
+
39
+
40
+ def markdown_heading(value: Any) -> str:
41
+ """Render an untrusted value inside a heading without allowing new blocks."""
42
+
43
+ return markdown_text(value)
44
+
45
+
46
+ def markdown_table_cell(value: Any) -> str:
47
+ """Render an untrusted value without adding Markdown table columns or rows."""
48
+
49
+ return markdown_text(value).translate(_TABLE_TRANSLATION)
50
+
51
+
52
+ def markdown_code_span(value: Any) -> str:
53
+ """Render an untrusted value in an inline code span with a safe delimiter."""
54
+
55
+ content = _single_line(value)
56
+ if not content:
57
+ return ""
58
+
59
+ delimiter = "`" * (_longest_backtick_run(content) + 1)
60
+ needs_padding = content.startswith("`") or content.endswith("`")
61
+ if content.strip(" ") and (content.startswith(" ") or content.endswith(" ")):
62
+ needs_padding = True
63
+ padding = " " if needs_padding else ""
64
+ return f"{delimiter}{padding}{content}{padding}{delimiter}"
65
+
66
+
67
+ def markdown_fenced_code(value: Any, *, language: str = "") -> str:
68
+ """Render untrusted multiline code using a fence longer than its contents."""
69
+
70
+ if not _SAFE_INFO_STRING.fullmatch(language):
71
+ raise ValueError("Markdown fence language contains unsafe characters")
72
+
73
+ content = _normalize_line_breaks(_string(value))
74
+ fence = "`" * max(3, _longest_backtick_run(content) + 1)
75
+ opening = f"{fence}{language}"
76
+ separator = "" if content.endswith("\n") else "\n"
77
+ return f"{opening}\n{content}{separator}{fence}"
78
+
79
+
80
+ def _single_line(value: Any) -> str:
81
+ return _LINE_BREAKS.sub(" ", _string(value))
82
+
83
+
84
+ def _normalize_line_breaks(value: str) -> str:
85
+ return _LINE_BREAKS.sub("\n", value)
86
+
87
+
88
+ def _longest_backtick_run(value: str) -> int:
89
+ return max((len(match.group(0)) for match in _BACKTICK_RUNS.finditer(value)), default=0)
90
+
91
+
92
+ def _underscore_is_inside_identifier(value: str, index: int) -> bool:
93
+ return (
94
+ index > 0
95
+ and index + 1 < len(value)
96
+ and value[index - 1].isalnum()
97
+ and value[index + 1].isalnum()
98
+ )
99
+
100
+
101
+ def _string(value: Any) -> str:
102
+ return "" if value is None else str(value)
agentloop/metrics.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+ from typing import Any
5
+
6
+ from agentloop.costs import estimate_cost_usd
7
+ from agentloop.timing import cumulative_span_time_ms, elapsed_runtime_ms
8
+
9
+
10
+ def build_report(trace: Any) -> dict[str, Any]:
11
+ events = trace.events
12
+ model_events = [e for e in events if e.event_type == "model_call"]
13
+ tool_events = [e for e in events if e.event_type == "tool_call"]
14
+ retry_events = [e for e in events if e.event_type == "retry"]
15
+
16
+ repeated = repeated_context_stats(model_events)
17
+ parallel = parallelism_opportunities(tool_events)
18
+ cumulative_time = cumulative_span_time_ms(events)
19
+
20
+ return {
21
+ "run_id": trace.run_id,
22
+ "name": trace.name,
23
+ "event_count": len(events),
24
+ "model_call_count": len(model_events),
25
+ "tool_call_count": len(tool_events),
26
+ "retry_count": len(retry_events),
27
+ "total_runtime_ms": round(elapsed_runtime_ms(trace), 3),
28
+ "cumulative_span_time_ms": round(cumulative_time, 3),
29
+ "model_time_ms": round(sum(e.duration_ms for e in model_events), 3),
30
+ "tool_time_ms": round(sum(e.duration_ms for e in tool_events), 3),
31
+ "retry_time_ms": round(sum(e.duration_ms for e in retry_events), 3),
32
+ "input_tokens": sum(e.input_tokens for e in model_events),
33
+ "output_tokens": sum(e.output_tokens for e in model_events),
34
+ "estimated_cost_usd": round(
35
+ sum(estimate_cost_usd(e.model, e.input_tokens, e.output_tokens) for e in model_events),
36
+ 6,
37
+ ),
38
+ "repeated_context_tokens": repeated["repeated_context_tokens"],
39
+ "repeated_context_ratio": repeated["repeated_context_ratio"],
40
+ "parallelism_opportunities": parallel,
41
+ "recommendations": build_recommendations(model_events, retry_events, repeated, parallel),
42
+ "events": [e.to_dict() for e in events],
43
+ }
44
+
45
+
46
+ def repeated_context_stats(model_events: list[Any]) -> dict[str, Any]:
47
+ prefixes = []
48
+ total_tokens = sum(e.input_tokens for e in model_events)
49
+ for event in model_events:
50
+ words = (event.input_text or "").split()
51
+ if words:
52
+ prefixes.append((" ".join(words[:80]), min(len(words), 80)))
53
+ counts = Counter(prefix for prefix, _ in prefixes)
54
+ repeated_tokens = sum(tokens for prefix, tokens in prefixes if counts[prefix] > 1)
55
+ avoidable = max(0, repeated_tokens // 2)
56
+ ratio = avoidable / total_tokens if total_tokens else 0.0
57
+ return {"repeated_context_tokens": avoidable, "repeated_context_ratio": round(ratio, 4)}
58
+
59
+
60
+ def parallelism_opportunities(tool_events: list[Any]) -> list[dict[str, Any]]:
61
+ grouped: dict[str, list[Any]] = defaultdict(list)
62
+ for event in tool_events:
63
+ grouped[event.name].append(event)
64
+ out = []
65
+ for name, items in grouped.items():
66
+ if len(items) >= 3:
67
+ sequential = sum(item.duration_ms for item in items)
68
+ parallel = max(item.duration_ms for item in items)
69
+ out.append(
70
+ {
71
+ "tool_name": name,
72
+ "count": len(items),
73
+ "sequential_time_ms": round(sequential, 3),
74
+ "estimated_parallel_time_ms": round(parallel, 3),
75
+ "estimated_savings_ms": round(max(0.0, sequential - parallel), 3),
76
+ }
77
+ )
78
+ return out
79
+
80
+
81
+ def build_recommendations(
82
+ model_events: list[Any],
83
+ retry_events: list[Any],
84
+ repeated: dict[str, Any],
85
+ parallel: list[dict[str, Any]],
86
+ ) -> list[dict[str, str]]:
87
+ recs = []
88
+ if repeated["repeated_context_ratio"] >= 0.10:
89
+ recs.append(
90
+ {
91
+ "title": "Cache repeated context",
92
+ "description": "Stable instructions appear across multiple model calls. Use cached prefixes or reusable summaries.",
93
+ }
94
+ )
95
+ if parallel:
96
+ recs.append(
97
+ {
98
+ "title": "Parallelize tool calls",
99
+ "description": "Repeated tool calls appear independent. Run them concurrently to lower end-to-end latency.",
100
+ }
101
+ )
102
+ if retry_events:
103
+ recs.append(
104
+ {
105
+ "title": "Use structured outputs",
106
+ "description": "Retries were recorded. Add schema validation or smaller repair prompts.",
107
+ }
108
+ )
109
+ if model_events:
110
+ largest = max(model_events, key=lambda e: e.input_tokens + e.output_tokens)
111
+ if largest.total_tokens >= 4000:
112
+ recs.append(
113
+ {
114
+ "title": "Compress largest model step",
115
+ "description": f"The {largest.name} step used {largest.total_tokens} tokens. Consider staged summarization.",
116
+ }
117
+ )
118
+ return recs or [
119
+ {
120
+ "title": "No major pattern detected",
121
+ "description": "Collect more traces for stronger recommendations.",
122
+ }
123
+ ]