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.
- agentloop/__init__.py +68 -0
- agentloop/audit.py +34 -0
- agentloop/autoinstrument.py +102 -0
- agentloop/ci.py +135 -0
- agentloop/cli.py +876 -0
- agentloop/client.py +170 -0
- agentloop/config.py +50 -0
- agentloop/costs.py +15 -0
- agentloop/decorators.py +160 -0
- agentloop/demo.py +283 -0
- agentloop/doctor.py +344 -0
- agentloop/events.py +49 -0
- agentloop/exporters.py +60 -0
- agentloop/findings.py +215 -0
- agentloop/graph.py +312 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/langgraph.py +140 -0
- agentloop/integrations/openai.py +369 -0
- agentloop/integrations/openai_agents.py +212 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/issues.py +88 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +123 -0
- agentloop/optimizer.py +323 -0
- agentloop/otel.py +302 -0
- agentloop/otel_ids.py +81 -0
- agentloop/patches.py +583 -0
- agentloop/plan_export.py +72 -0
- agentloop/quality.py +222 -0
- agentloop/replay.py +424 -0
- agentloop/runtime.py +244 -0
- agentloop/savings.py +135 -0
- agentloop/server.py +292 -0
- agentloop/store.py +840 -0
- agentloop/timing.py +124 -0
- agentloop/tracer.py +356 -0
- agentloop/value.py +243 -0
- agentloop/version.py +3 -0
- agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
- agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
- agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
- agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +1 -0
- dashboard/app.py +698 -0
- dashboard/value_view.py +101 -0
agentloop/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from agentloop.autoinstrument import (
|
|
2
|
+
DetectionResult,
|
|
3
|
+
InstrumentationResult,
|
|
4
|
+
auto_instrument,
|
|
5
|
+
detect_integrations,
|
|
6
|
+
)
|
|
7
|
+
from agentloop.client import AgentLoopClient, AgentLoopClientError
|
|
8
|
+
from agentloop.decorators import trace_model, trace_tool, traceable
|
|
9
|
+
from agentloop.doctor import run_doctor
|
|
10
|
+
from agentloop.findings import build_diagnosis
|
|
11
|
+
from agentloop.otel import trace_from_otel, trace_to_otel
|
|
12
|
+
from agentloop.patches import build_patch_plan
|
|
13
|
+
from agentloop.replay import ReplayGates, build_replay_report
|
|
14
|
+
from agentloop.runtime import (
|
|
15
|
+
CLEAR,
|
|
16
|
+
FinalizationError,
|
|
17
|
+
get_last_error,
|
|
18
|
+
get_runtime_config,
|
|
19
|
+
init,
|
|
20
|
+
reset_runtime,
|
|
21
|
+
)
|
|
22
|
+
from agentloop.tracer import (
|
|
23
|
+
AgentTrace,
|
|
24
|
+
current_trace,
|
|
25
|
+
record_model_call,
|
|
26
|
+
record_tool_call,
|
|
27
|
+
trace_agent,
|
|
28
|
+
trace_model_call,
|
|
29
|
+
trace_retry,
|
|
30
|
+
trace_tool_call,
|
|
31
|
+
)
|
|
32
|
+
from agentloop.value import build_value_report
|
|
33
|
+
from agentloop.version import __version__
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AgentLoopClient",
|
|
37
|
+
"AgentLoopClientError",
|
|
38
|
+
"AgentTrace",
|
|
39
|
+
"CLEAR",
|
|
40
|
+
"FinalizationError",
|
|
41
|
+
"DetectionResult",
|
|
42
|
+
"InstrumentationResult",
|
|
43
|
+
"ReplayGates",
|
|
44
|
+
"auto_instrument",
|
|
45
|
+
"detect_integrations",
|
|
46
|
+
"build_value_report",
|
|
47
|
+
"build_diagnosis",
|
|
48
|
+
"build_patch_plan",
|
|
49
|
+
"build_replay_report",
|
|
50
|
+
"current_trace",
|
|
51
|
+
"get_last_error",
|
|
52
|
+
"get_runtime_config",
|
|
53
|
+
"init",
|
|
54
|
+
"record_model_call",
|
|
55
|
+
"record_tool_call",
|
|
56
|
+
"reset_runtime",
|
|
57
|
+
"run_doctor",
|
|
58
|
+
"trace_agent",
|
|
59
|
+
"trace_from_otel",
|
|
60
|
+
"trace_model",
|
|
61
|
+
"trace_model_call",
|
|
62
|
+
"trace_tool",
|
|
63
|
+
"trace_tool_call",
|
|
64
|
+
"trace_retry",
|
|
65
|
+
"traceable",
|
|
66
|
+
"trace_to_otel",
|
|
67
|
+
"__version__",
|
|
68
|
+
]
|
agentloop/audit.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agentloop.exporters import export_report_markdown
|
|
8
|
+
from agentloop.tracer import AgentTrace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AuditResult:
|
|
13
|
+
report: dict[str, Any]
|
|
14
|
+
markdown_path: Path | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_audit(trace_path: str | Path, out_path: str | Path | None = None) -> AuditResult:
|
|
18
|
+
trace = AgentTrace.from_json(trace_path)
|
|
19
|
+
report = trace.report()
|
|
20
|
+
md_path = export_report_markdown(report, out_path) if out_path else None
|
|
21
|
+
return AuditResult(report=report, markdown_path=md_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def estimate_improvement(report: dict[str, Any]) -> dict[str, float]:
|
|
25
|
+
latency_savings = 0.0
|
|
26
|
+
for item in report.get("parallelism_opportunities", []):
|
|
27
|
+
latency_savings += item.get("estimated_savings_ms", 0.0)
|
|
28
|
+
retry_savings = report.get("retry_time_ms", 0.0) * 0.8
|
|
29
|
+
repeated_ratio = report.get("repeated_context_ratio", 0.0)
|
|
30
|
+
cost_savings = report.get("estimated_cost_usd", 0.0) * min(0.5, repeated_ratio)
|
|
31
|
+
return {
|
|
32
|
+
"estimated_latency_savings_ms": round(latency_savings + retry_savings, 3),
|
|
33
|
+
"estimated_cost_savings_usd": round(cost_savings, 6),
|
|
34
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import warnings
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Framework integrations AgentLoop ships helpers for, mapped to the import name
|
|
9
|
+
# used to detect whether that framework's SDK is installed.
|
|
10
|
+
_DETECTABLE_INTEGRATIONS: dict[str, str] = {
|
|
11
|
+
"openai": "openai",
|
|
12
|
+
"langgraph": "langgraph",
|
|
13
|
+
"crewai": "crewai",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Integrations that are not detectable as an installed Python package because
|
|
17
|
+
# instrumentation happens outside this process, mapped to guidance.
|
|
18
|
+
_NON_PYTHON_INTEGRATIONS: dict[str, str] = {
|
|
19
|
+
"vercel_ai_sdk": "JavaScript-side; call trace_from_vercel_ai_events(...) on exported telemetry",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DetectionResult:
|
|
25
|
+
"""Which framework integrations are available in the current environment.
|
|
26
|
+
|
|
27
|
+
This is a *detection* result. It reports whether each integration's SDK is
|
|
28
|
+
importable in this process; it does not wrap, patch, or register anything.
|
|
29
|
+
Instrument an application by applying the helpers in
|
|
30
|
+
:mod:`agentloop.integrations` (or the generic decorator SDK) from that
|
|
31
|
+
application's own startup.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
available: list[str] = field(default_factory=list)
|
|
35
|
+
unavailable: dict[str, str] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def ok(self) -> bool:
|
|
39
|
+
return bool(self.available)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return {"ok": self.ok, "available": self.available, "unavailable": self.unavailable}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _module_available(module_name: str) -> bool:
|
|
46
|
+
"""Return True if ``module_name`` is importable, without importing it."""
|
|
47
|
+
try:
|
|
48
|
+
return importlib.util.find_spec(module_name) is not None
|
|
49
|
+
except (ImportError, ValueError):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def detect_integrations() -> DetectionResult:
|
|
54
|
+
"""Detect which AgentLoop framework integrations are available here.
|
|
55
|
+
|
|
56
|
+
Detection only checks package availability with
|
|
57
|
+
:func:`importlib.util.find_spec` and never imports the framework, so it is
|
|
58
|
+
cheap and side-effect free and safe to run at application startup. The result
|
|
59
|
+
is deterministic for a given environment, so repeated calls return equal
|
|
60
|
+
results.
|
|
61
|
+
|
|
62
|
+
This function does not enable instrumentation. To record traces, apply the
|
|
63
|
+
helpers in :mod:`agentloop.integrations` (for example
|
|
64
|
+
``instrument_openai_client``) or the generic decorator SDK from the
|
|
65
|
+
application process you want to trace.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
result = DetectionResult()
|
|
69
|
+
|
|
70
|
+
for name, module_name in _DETECTABLE_INTEGRATIONS.items():
|
|
71
|
+
if _module_available(module_name):
|
|
72
|
+
result.available.append(name)
|
|
73
|
+
else:
|
|
74
|
+
result.unavailable[name] = "package not installed"
|
|
75
|
+
|
|
76
|
+
result.unavailable.update(_NON_PYTHON_INTEGRATIONS)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Backwards-compatible aliases. The previous names implied that calling this
|
|
81
|
+
# enabled instrumentation, which it never did.
|
|
82
|
+
InstrumentationResult = DetectionResult
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def auto_instrument(**kwargs: Any) -> DetectionResult:
|
|
86
|
+
"""Deprecated alias for :func:`detect_integrations`.
|
|
87
|
+
|
|
88
|
+
The old name and its ``enabled`` field wrongly implied that calling it
|
|
89
|
+
activated instrumentation; it only ever detected installed frameworks. Use
|
|
90
|
+
:func:`detect_integrations` instead and apply
|
|
91
|
+
:mod:`agentloop.integrations` helpers from your application startup. Any
|
|
92
|
+
keyword arguments are ignored and were never used.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
warnings.warn(
|
|
96
|
+
"auto_instrument() never enabled instrumentation; it only detects "
|
|
97
|
+
"installed frameworks. It is deprecated: use detect_integrations() and "
|
|
98
|
+
"apply agentloop.integrations helpers from your application startup.",
|
|
99
|
+
DeprecationWarning,
|
|
100
|
+
stacklevel=2,
|
|
101
|
+
)
|
|
102
|
+
return detect_integrations()
|
agentloop/ci.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agentloop.findings import build_diagnosis
|
|
6
|
+
from agentloop.markdown import markdown_table_cell, markdown_text
|
|
7
|
+
from agentloop.replay import ReplayGates, build_replay_report
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_ci_report(
|
|
11
|
+
baseline_trace: Any,
|
|
12
|
+
candidate_trace: Any,
|
|
13
|
+
*,
|
|
14
|
+
gates: ReplayGates | None = None,
|
|
15
|
+
quality_report: dict[str, Any] | None = None,
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
"""Build the PR-facing AgentLoop performance report."""
|
|
18
|
+
replay = build_replay_report(
|
|
19
|
+
baseline_trace, candidate_trace, gates=gates, quality_report=quality_report
|
|
20
|
+
)
|
|
21
|
+
diagnosis = build_diagnosis(candidate_trace)
|
|
22
|
+
summary = _summary(replay, diagnosis)
|
|
23
|
+
return {
|
|
24
|
+
"passed": replay["gates"]["passed"],
|
|
25
|
+
"status": "passed" if replay["gates"]["passed"] else "failed",
|
|
26
|
+
"summary": summary,
|
|
27
|
+
"replay": replay,
|
|
28
|
+
"diagnosis": diagnosis,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ci_report_to_markdown(report: dict[str, Any]) -> str:
|
|
33
|
+
summary = report["summary"]
|
|
34
|
+
replay = report["replay"]
|
|
35
|
+
diagnosis = report["diagnosis"]
|
|
36
|
+
gate_status = "passed" if replay["gates"]["passed"] else "failed"
|
|
37
|
+
|
|
38
|
+
lines = [
|
|
39
|
+
"# AgentLoop CI Report",
|
|
40
|
+
"",
|
|
41
|
+
f"- Status: {gate_status}",
|
|
42
|
+
f"- Replay: {markdown_text(replay['summary'])}",
|
|
43
|
+
f"- Merge recommendation: {markdown_text(summary['merge_recommendation'])}",
|
|
44
|
+
(
|
|
45
|
+
f"- Findings: {summary['finding_count']} total, "
|
|
46
|
+
f"{summary['high_severity_count']} high severity, "
|
|
47
|
+
f"{summary['patchable_count']} patchable"
|
|
48
|
+
),
|
|
49
|
+
"",
|
|
50
|
+
"## Performance Gates",
|
|
51
|
+
"",
|
|
52
|
+
"| Gate | Status | Detail |",
|
|
53
|
+
"|---|---|---|",
|
|
54
|
+
]
|
|
55
|
+
for gate in replay["gates"]["results"]:
|
|
56
|
+
status = "pass" if gate["passed"] else "fail"
|
|
57
|
+
lines.append(
|
|
58
|
+
f"| {markdown_table_cell(gate['name'])} | {status} | "
|
|
59
|
+
f"{markdown_table_cell(gate['detail'])} |"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
lines.extend(
|
|
63
|
+
[
|
|
64
|
+
"",
|
|
65
|
+
"## Metric Impact",
|
|
66
|
+
"",
|
|
67
|
+
"| Metric | Impact |",
|
|
68
|
+
"|---|---:|",
|
|
69
|
+
f"| Latency improvement | {summary['latency_improvement_pct']:.2f}% |",
|
|
70
|
+
f"| Cost improvement | {summary['cost_improvement_pct']:.2f}% |",
|
|
71
|
+
f"| Retry delta | {summary['retry_count_delta']} |",
|
|
72
|
+
f"| Candidate schema validity | {summary['candidate_schema_validity']} |",
|
|
73
|
+
f"| Candidate quality score | {summary['candidate_quality_score']} |",
|
|
74
|
+
"",
|
|
75
|
+
"## Top Findings",
|
|
76
|
+
"",
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
findings = diagnosis.get("findings", [])
|
|
81
|
+
if not findings:
|
|
82
|
+
lines.append("No machine-actionable optimization findings detected.")
|
|
83
|
+
else:
|
|
84
|
+
lines.extend(
|
|
85
|
+
[
|
|
86
|
+
"| Finding | Severity | Type | Rewrite |",
|
|
87
|
+
"|---|---|---|---|",
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
for finding in findings[:5]:
|
|
91
|
+
lines.append(
|
|
92
|
+
"| "
|
|
93
|
+
f"{markdown_table_cell(finding['finding_id'])}: "
|
|
94
|
+
f"{markdown_table_cell(finding['title'])} | "
|
|
95
|
+
f"{markdown_table_cell(finding['severity'])} | "
|
|
96
|
+
f"{markdown_table_cell(finding['type'])} | "
|
|
97
|
+
f"{markdown_table_cell(finding['rewrite']['hint'])} |"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _summary(replay: dict[str, Any], diagnosis: dict[str, Any]) -> dict[str, Any]:
|
|
104
|
+
deltas = replay["deltas"]
|
|
105
|
+
diagnosis_summary = diagnosis["summary"]
|
|
106
|
+
candidate = replay["candidate"]
|
|
107
|
+
gates_passed = replay["gates"]["passed"]
|
|
108
|
+
merge_recommendation = (
|
|
109
|
+
"safe to merge by configured AgentLoop gates"
|
|
110
|
+
if gates_passed and diagnosis_summary["high_severity_count"] == 0
|
|
111
|
+
else "review before merge; configured gates or high-severity findings need attention"
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
"latency_improvement_pct": deltas["latency_improvement_pct"],
|
|
115
|
+
"cost_improvement_pct": deltas["cost_improvement_pct"],
|
|
116
|
+
"retry_count_delta": deltas["retry_count_delta"],
|
|
117
|
+
"candidate_schema_validity": _format_optional(
|
|
118
|
+
candidate.get("schema_validity_pct"), suffix="%"
|
|
119
|
+
)
|
|
120
|
+
if candidate.get("schema_validity_pct") is not None
|
|
121
|
+
else _format_optional(candidate.get("schema_valid")),
|
|
122
|
+
"candidate_quality_score": _format_optional(candidate.get("quality_score")),
|
|
123
|
+
"finding_count": diagnosis_summary["finding_count"],
|
|
124
|
+
"high_severity_count": diagnosis_summary["high_severity_count"],
|
|
125
|
+
"patchable_count": diagnosis_summary["patchable_count"],
|
|
126
|
+
"merge_recommendation": merge_recommendation,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _format_optional(value: Any, suffix: str = "") -> str:
|
|
131
|
+
if value is None:
|
|
132
|
+
return "not provided"
|
|
133
|
+
if isinstance(value, float):
|
|
134
|
+
return f"{value:.4f}{suffix}"
|
|
135
|
+
return f"{value}{suffix}"
|