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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""AgentDynamics: application performance monitoring for AI agents.
|
|
2
|
+
|
|
3
|
+
import agentdynamics
|
|
4
|
+
agentdynamics.init() # auto-instruments LangChain/LangGraph, OpenTelemetry, Anthropic and OpenAI
|
|
5
|
+
|
|
6
|
+
See agentdynamics.autotrace for @trace / @tool / span.
|
|
7
|
+
"""
|
|
8
|
+
__version__ = "0.4.0"
|
|
9
|
+
|
|
10
|
+
from .autotrace import flush, init, llm_call, record_llm, span, tool, trace # noqa: E402,F401
|
|
11
|
+
|
|
12
|
+
__all__ = ["init", "trace", "tool", "span", "llm_call", "record_llm", "flush", "__version__"]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""CLI.
|
|
2
|
+
|
|
3
|
+
agentdynamics serve start console + receivers (default command)
|
|
4
|
+
agentdynamics run <cmd ...> run a Python program with zero-code instrumentation
|
|
5
|
+
agentdynamics connect <framework> print the exact setup for a framework
|
|
6
|
+
agentdynamics doctor check connectivity/auth and send a test trace
|
|
7
|
+
agentdynamics keys create|list|revoke
|
|
8
|
+
agentdynamics report | ingest | push <file>
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import secrets
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
import webbrowser
|
|
20
|
+
|
|
21
|
+
from .collectors.claude_code import DEFAULT_ROOT
|
|
22
|
+
|
|
23
|
+
DEFAULT_DATA = os.path.join(os.path.expanduser("~"), ".agentdynamics")
|
|
24
|
+
DEFAULT_URL = os.environ.get("AGENTDYNAMICS_URL", "http://127.0.0.1:8787")
|
|
25
|
+
|
|
26
|
+
SNIPPETS = {
|
|
27
|
+
"python": ("Any Python agent (Anthropic / OpenAI SDKs, LangChain, LangGraph, OpenTelemetry auto-detected)", """\
|
|
28
|
+
pip install agentdynamics
|
|
29
|
+
|
|
30
|
+
# in your entry point, before creating clients:
|
|
31
|
+
import agentdynamics
|
|
32
|
+
agentdynamics.init(url="{url}", project="my-agent") # api_key=... if auth is on
|
|
33
|
+
|
|
34
|
+
# optional: group each request into one task and name its stages
|
|
35
|
+
@agentdynamics.trace
|
|
36
|
+
def handle(question):
|
|
37
|
+
with agentdynamics.span("plan"):
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
# or, with no code changes at all:
|
|
41
|
+
agentdynamics run python app.py"""),
|
|
42
|
+
"langgraph": ("LangGraph / LangChain (Python or JS): environment variables only", """\
|
|
43
|
+
export LANGSMITH_TRACING=true
|
|
44
|
+
export LANGSMITH_ENDPOINT={url}/langsmith
|
|
45
|
+
export LANGSMITH_API_KEY=<ingest key, or anything when auth is off>
|
|
46
|
+
export LANGSMITH_PROJECT=my-agent
|
|
47
|
+
# works for langchain / langgraph (Python) and langchainjs / @langchain/langgraph (JS)"""),
|
|
48
|
+
"otel": ("Anything that speaks OpenTelemetry (OpenAI Agents SDK, Strands, Semantic Kernel, Vercel AI SDK, CrewAI/LlamaIndex via OpenInference, Java/Go/.NET/JS)", """\
|
|
49
|
+
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT={url}/v1/traces
|
|
50
|
+
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer <ingest key>"
|
|
51
|
+
export OTEL_SERVICE_NAME=my-agent
|
|
52
|
+
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production"""),
|
|
53
|
+
"langsmith": ("Keep LangSmith, copy runs into AgentDynamics (pull)", """\
|
|
54
|
+
# <data>/agentdynamics.toml
|
|
55
|
+
[[sources]]
|
|
56
|
+
type = "langsmith_api"
|
|
57
|
+
project = "my-agent-prod"
|
|
58
|
+
api_key_env = "LANGSMITH_API_KEY\""""),
|
|
59
|
+
"langfuse": ("Keep Langfuse, copy traces into AgentDynamics (pull)", """\
|
|
60
|
+
# <data>/agentdynamics.toml
|
|
61
|
+
[[sources]]
|
|
62
|
+
type = "langfuse_api"
|
|
63
|
+
host = "https://cloud.langfuse.com"
|
|
64
|
+
public_key_env = "LANGFUSE_PUBLIC_KEY"
|
|
65
|
+
secret_key_env = "LANGFUSE_SECRET_KEY\""""),
|
|
66
|
+
"logs": ("Log pipelines: Fluent Bit / Vector / Logstash HTTP output, or files", """\
|
|
67
|
+
POST {url}/api/ingest/records (NDJSON or JSON array; OTLP JSON, LangSmith runs, Langfuse traces, spans)
|
|
68
|
+
Authorization: Bearer <ingest key>
|
|
69
|
+
|
|
70
|
+
# or tail a directory: [[sources]] type = "inbox" path = "/var/log/agent-traces\""""),
|
|
71
|
+
"http": ("Any language, plain HTTP", """\
|
|
72
|
+
curl -X POST {url}/api/ingest -H 'Content-Type: application/json' -d '{{
|
|
73
|
+
"workflow": "support_bot", "project": "helpdesk",
|
|
74
|
+
"steps": [
|
|
75
|
+
{{"kind": "prompt", "ts": 1726700000, "text": "Refund order 42"}},
|
|
76
|
+
{{"kind": "llm", "ts": 1726700000, "end_ts": 1726700002, "model": "claude-sonnet-5",
|
|
77
|
+
"input_tokens": 1200, "output_tokens": 300, "stop_reason": "end_turn"}}]}}'"""),
|
|
78
|
+
"aegis": ("Agents governed by Aegis: every decision recorded, model spend gated, watchdog kill switch", """\
|
|
79
|
+
pip install agentdynamics aegis-kernel
|
|
80
|
+
|
|
81
|
+
import agentdynamics
|
|
82
|
+
from agentdynamics.integrations import aegis as governance
|
|
83
|
+
from aegis import build_kernel, load_policy
|
|
84
|
+
|
|
85
|
+
agentdynamics.init(url="{url}", project="my-agent")
|
|
86
|
+
kernel, root = build_kernel(load_policy("policy.yaml"), registry)
|
|
87
|
+
governance.instrument(kernel, root, watchdog=governance.Watchdog(max_repeated_denials=3))
|
|
88
|
+
|
|
89
|
+
# later: least-privilege policy from what the agent actually did
|
|
90
|
+
agentdynamics policy export --workflow my_workflow --base policy.yaml --out tightened.yaml"""),
|
|
91
|
+
"claude-code": ("Claude Code", "Nothing to do: sessions in ~/.claude/projects are read automatically by `agentdynamics serve`."),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _http(method, url, key=None, body=None, timeout=10):
|
|
96
|
+
headers = {"Content-Type": "application/json"}
|
|
97
|
+
if key:
|
|
98
|
+
headers["Authorization"] = f"Bearer {key}"
|
|
99
|
+
req = urllib.request.Request(url, data=json.dumps(body).encode() if body is not None else None, headers=headers, method=method)
|
|
100
|
+
try:
|
|
101
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
102
|
+
return r.status, json.loads(r.read() or b"null")
|
|
103
|
+
except urllib.error.HTTPError as e:
|
|
104
|
+
return e.code, None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_doctor(a):
|
|
108
|
+
url, key = a.url.rstrip("/"), a.key or os.environ.get("AGENTDYNAMICS_API_KEY")
|
|
109
|
+
ok = True
|
|
110
|
+
|
|
111
|
+
def line(good, msg, hint=""):
|
|
112
|
+
nonlocal ok
|
|
113
|
+
ok &= good
|
|
114
|
+
print(f" [{'ok' if good else '!!'}] {msg}" + (f"\n -> {hint}" if hint and not good else ""))
|
|
115
|
+
|
|
116
|
+
print(f"AgentDynamics doctor - {url}")
|
|
117
|
+
try:
|
|
118
|
+
st, h = _http("GET", url + "/healthz")
|
|
119
|
+
except OSError as ex:
|
|
120
|
+
line(False, f"server reachable ({ex})", "start it with: agentdynamics serve (or set AGENTDYNAMICS_URL)")
|
|
121
|
+
return 1
|
|
122
|
+
line(st == 200, f"server reachable (status {h and h.get('status')})")
|
|
123
|
+
st, who = _http("GET", url + "/api/whoami", key)
|
|
124
|
+
role = (who or {}).get("role") if st == 200 else ("ingest" if st == 403 else None)
|
|
125
|
+
line(st in (200, 403), f"credentials accepted (role: {role or 'none'})",
|
|
126
|
+
"set AGENTDYNAMICS_API_KEY, or create a key on the server: agentdynamics keys create --role ingest")
|
|
127
|
+
run_id = f"doctor-{int(time.time())}"
|
|
128
|
+
st, _ = _http("POST", url + "/api/ingest", key, {"id": run_id, "workflow": "doctor_check", "project": "agentdynamics-doctor", "steps": [
|
|
129
|
+
{"kind": "prompt", "ts": time.time() - 2, "text": "connectivity check"},
|
|
130
|
+
{"kind": "llm", "ts": time.time() - 2, "end_ts": time.time(), "model": "claude-haiku-4-5", "input_tokens": 10, "output_tokens": 5,
|
|
131
|
+
"stop_reason": "end_turn"}]})
|
|
132
|
+
line(st == 200, "test trace accepted", "the key needs the 'ingest' (or 'admin') role")
|
|
133
|
+
if st == 200 and role in ("read", "admin"):
|
|
134
|
+
_http("POST", url + "/api/refresh", key, {})
|
|
135
|
+
st, d = _http("GET", url + "/api/tasks?project=agentdynamics-doctor&days=&limit=5", key)
|
|
136
|
+
seen = st == 200 and any(t["run_id"] == run_id for t in (d or {}).get("tasks", []))
|
|
137
|
+
line(seen, "test trace analyzed and visible in the console")
|
|
138
|
+
if ok:
|
|
139
|
+
print(f"\nAll good. Open {url} and look for project 'agentdynamics-doctor'.")
|
|
140
|
+
else:
|
|
141
|
+
print("\nFix the items above and run again.")
|
|
142
|
+
return 0 if ok else 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_keys(a, data):
|
|
146
|
+
from .config import keys_path, load_keys, save_keys
|
|
147
|
+
keys = load_keys(data)
|
|
148
|
+
if a.action == "create":
|
|
149
|
+
k = {"name": a.name or f"{a.role}-{len(keys) + 1}", "role": a.role, "key": f"ad_{a.role[0]}_{secrets.token_urlsafe(24)}",
|
|
150
|
+
"created": int(time.time())}
|
|
151
|
+
keys.append(k)
|
|
152
|
+
save_keys(data, keys)
|
|
153
|
+
print(f"Created {k['role']} key '{k['name']}':\n\n {k['key']}\n\nAuth is now ON for the server using {keys_path(data)} (restart it).")
|
|
154
|
+
if a.role == "ingest":
|
|
155
|
+
print(f"Use it in your app: export AGENTDYNAMICS_API_KEY={k['key']}")
|
|
156
|
+
elif a.action == "list":
|
|
157
|
+
if not keys:
|
|
158
|
+
print("No keys: auth is off (local mode).")
|
|
159
|
+
for k in keys:
|
|
160
|
+
print(f" {k['name']:<20} {k['role']:<7} {k['key'][:8]}... created {time.strftime('%Y-%m-%d', time.localtime(k.get('created', 0)))}")
|
|
161
|
+
elif a.action == "revoke":
|
|
162
|
+
left = [k for k in keys if k["name"] != a.name and not k["key"].startswith(a.name or "\0")]
|
|
163
|
+
save_keys(data, left)
|
|
164
|
+
print(f"Revoked {len(keys) - len(left)} key(s).")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_policy(a, eng):
|
|
168
|
+
from .server import Api
|
|
169
|
+
api = Api(eng)
|
|
170
|
+
q = {k: v for k, v in (("workflow", a.workflow), ("project", a.project), ("environment", a.environment),
|
|
171
|
+
("days", a.days), ("policy", a.policy), ("headroom", a.headroom)) if v}
|
|
172
|
+
if a.base:
|
|
173
|
+
try:
|
|
174
|
+
from aegis import dump_policy, load_policy
|
|
175
|
+
except ImportError:
|
|
176
|
+
print("--base needs aegis-kernel: pip install aegis-kernel", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
q["base_doc"] = dump_policy(load_policy(a.base))
|
|
179
|
+
if a.action == "report":
|
|
180
|
+
g = api.governance(q)
|
|
181
|
+
k = g["kpis"]
|
|
182
|
+
print(f"Governed tasks {k['governed_tasks']} - decisions {k['decisions']} - denials {k['denials']} "
|
|
183
|
+
f"({k['denial_rate']:.1%} of tool calls) - budget stops {k['budget_stops']} - revocations {k['revocations']}")
|
|
184
|
+
for p in g["policies"]:
|
|
185
|
+
print()
|
|
186
|
+
print(f"{p['policy']}: {p['tasks']} tasks, success {p['success_rate']:.0%}, {p['denials']} denials")
|
|
187
|
+
print(f" granted {len(p['granted'])} tools, used {len(p['used'])}; unused: {', '.join(p['unused']) or 'none'}")
|
|
188
|
+
print(" budget headroom (limit / p95 used): " + ", ".join(f"{k2} {v}x" for k2, v in p["headroom"].items() if v))
|
|
189
|
+
for r in g["by_rule"][:8]:
|
|
190
|
+
print(f" {r['n']:>5} {r['rule']}")
|
|
191
|
+
return 0
|
|
192
|
+
res = api.export_policy(q)
|
|
193
|
+
if res.get("error"):
|
|
194
|
+
print(res["error"], file=sys.stderr)
|
|
195
|
+
return 1
|
|
196
|
+
if a.out:
|
|
197
|
+
with open(a.out, "w", encoding="utf-8") as f:
|
|
198
|
+
f.write(res["yaml"])
|
|
199
|
+
print(f"wrote {a.out}: {len(res['changes'])} change(s) vs {res['base'] or 'no base'}", file=sys.stderr)
|
|
200
|
+
for c in res["changes"]:
|
|
201
|
+
print(f" - {c}", file=sys.stderr)
|
|
202
|
+
else:
|
|
203
|
+
print(res["yaml"])
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def cmd_run(a):
|
|
208
|
+
if not a.command:
|
|
209
|
+
print("usage: agentdynamics run python app.py [args...]")
|
|
210
|
+
return 2
|
|
211
|
+
boot = os.path.join(os.path.dirname(__file__), "bootstrap")
|
|
212
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
213
|
+
env = dict(os.environ)
|
|
214
|
+
env["PYTHONPATH"] = os.pathsep.join(p for p in (boot, root, env.get("PYTHONPATH")) if p)
|
|
215
|
+
if a.project:
|
|
216
|
+
env["AGENTDYNAMICS_PROJECT"] = a.project
|
|
217
|
+
cmd = a.command[1:] if a.command[0] == "--" else a.command
|
|
218
|
+
return subprocess.call(cmd, env=env)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv=None):
|
|
222
|
+
from . import __version__
|
|
223
|
+
ap = argparse.ArgumentParser(prog="agentdynamics", description="APM for AI agents")
|
|
224
|
+
ap.add_argument("--version", action="version", version=f"agentdynamics {__version__}")
|
|
225
|
+
ap.add_argument("--data", default=os.environ.get("AGENTDYNAMICS_DATA", DEFAULT_DATA), help="data directory")
|
|
226
|
+
ap.add_argument("--claude-root", default=DEFAULT_ROOT, help="Claude Code projects dir ('' to disable)")
|
|
227
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
228
|
+
s = sub.add_parser("serve", help="run the console and receivers")
|
|
229
|
+
s.add_argument("--host", default=None, help="default from config [server] or 127.0.0.1")
|
|
230
|
+
s.add_argument("--port", type=int, default=None)
|
|
231
|
+
s.add_argument("--interval", type=int, default=None, help="seconds between source re-scans")
|
|
232
|
+
s.add_argument("--open", action="store_true", help="open the browser")
|
|
233
|
+
r = sub.add_parser("run", help="run a Python program with zero-code instrumentation")
|
|
234
|
+
r.add_argument("--project", default=None)
|
|
235
|
+
r.add_argument("command", nargs=argparse.REMAINDER)
|
|
236
|
+
c = sub.add_parser("connect", help="print setup instructions for a framework")
|
|
237
|
+
c.add_argument("framework", nargs="?", choices=sorted(SNIPPETS), default=None)
|
|
238
|
+
c.add_argument("--url", default=DEFAULT_URL)
|
|
239
|
+
d = sub.add_parser("doctor", help="check connectivity and auth, send a test trace")
|
|
240
|
+
d.add_argument("--url", default=DEFAULT_URL)
|
|
241
|
+
d.add_argument("--key", default=None)
|
|
242
|
+
k = sub.add_parser("keys", help="manage API keys")
|
|
243
|
+
k.add_argument("action", choices=["create", "list", "revoke"])
|
|
244
|
+
k.add_argument("--role", choices=["ingest", "read", "admin"], default="ingest")
|
|
245
|
+
k.add_argument("--name", default=None)
|
|
246
|
+
po = sub.add_parser("policy", help="Aegis policy from observed behaviour (observe -> govern)")
|
|
247
|
+
po.add_argument("action", choices=["export", "report"])
|
|
248
|
+
po.add_argument("--workflow")
|
|
249
|
+
po.add_argument("--project")
|
|
250
|
+
po.add_argument("--environment")
|
|
251
|
+
po.add_argument("--days", type=float)
|
|
252
|
+
po.add_argument("--policy", help="policy label as shown in the console (name@vN#digest)")
|
|
253
|
+
po.add_argument("--base", help="Aegis policy file to tighten (needs aegis-kernel installed)")
|
|
254
|
+
po.add_argument("--headroom", type=float, default=1.5)
|
|
255
|
+
po.add_argument("--out", help="write the YAML here (default: stdout)")
|
|
256
|
+
sub.add_parser("ingest", help="scan sources once and rebuild the database")
|
|
257
|
+
rp = sub.add_parser("report", help="print a text summary")
|
|
258
|
+
rp.add_argument("--project")
|
|
259
|
+
rp.add_argument("--days", type=float)
|
|
260
|
+
p = sub.add_parser("push", help="ingest a JSON file (OTLP export, LangSmith runs, Langfuse traces or generic run)")
|
|
261
|
+
p.add_argument("file")
|
|
262
|
+
a = ap.parse_args(argv)
|
|
263
|
+
cmd = a.cmd or "serve"
|
|
264
|
+
|
|
265
|
+
if cmd == "run":
|
|
266
|
+
return cmd_run(a)
|
|
267
|
+
if cmd == "doctor":
|
|
268
|
+
return cmd_doctor(a)
|
|
269
|
+
if cmd == "connect":
|
|
270
|
+
items = [a.framework] if a.framework else list(SNIPPETS)
|
|
271
|
+
for name in items:
|
|
272
|
+
title, body = SNIPPETS[name]
|
|
273
|
+
print(f"\n== {name}: {title}\n\n{body.format(url=a.url.rstrip('/'))}\n")
|
|
274
|
+
return 0
|
|
275
|
+
os.makedirs(a.data, exist_ok=True)
|
|
276
|
+
if cmd == "keys":
|
|
277
|
+
return cmd_keys(a, a.data)
|
|
278
|
+
|
|
279
|
+
from .engine import Engine
|
|
280
|
+
eng = Engine(a.data, a.claude_root or None)
|
|
281
|
+
if cmd == "push":
|
|
282
|
+
with open(a.file, encoding="utf-8") as f:
|
|
283
|
+
doc = json.load(f)
|
|
284
|
+
from .collectors.inbox import detect
|
|
285
|
+
recs = doc if isinstance(doc, list) else [doc]
|
|
286
|
+
print("accepted", eng.ingest_records([(detect(x), x) for x in recs if detect(x)]))
|
|
287
|
+
eng.refresh()
|
|
288
|
+
return 0
|
|
289
|
+
eng.refresh(force=True)
|
|
290
|
+
print(f"indexed in {eng.last_duration}s -> {os.path.join(a.data, 'agentdynamics.db')}", file=sys.stderr)
|
|
291
|
+
if cmd == "ingest":
|
|
292
|
+
return 0
|
|
293
|
+
if cmd == "policy":
|
|
294
|
+
return cmd_policy(a, eng)
|
|
295
|
+
if cmd == "report":
|
|
296
|
+
from .server import Api
|
|
297
|
+
api = Api(eng)
|
|
298
|
+
q = {k2: v for k2, v in (("project", a.project), ("days", a.days)) if v}
|
|
299
|
+
o = api.overview(q)
|
|
300
|
+
kp = o["kpis"]
|
|
301
|
+
print(f"\nTasks {kp['tasks']} sessions {kp['sessions']} cost ${kp['cost']:.2f} Apdex {kp['apdex']} "
|
|
302
|
+
f"success {kp['success_rate'] or 0:.0%} tool errors {kp['tool_error_rate']:.1%} waste ${kp['waste_cost']:.2f}")
|
|
303
|
+
print("\nTask types:")
|
|
304
|
+
for t in o["types"]:
|
|
305
|
+
print(f" {t['type']:<20} {t['tasks']:>4} tasks ${t['cost']:>9.2f} apdex {t['apdex']} {t['health']}")
|
|
306
|
+
print("\nProcess review:")
|
|
307
|
+
for i in api.process(q)["insights"]:
|
|
308
|
+
print(f" [{i['severity']:>7}] {i['title']}: {i['metric']}\n {i['detail']}")
|
|
309
|
+
return 0
|
|
310
|
+
host = a.host or os.environ.get("AGENTDYNAMICS_HOST") or eng.cfg["server"]["host"]
|
|
311
|
+
port = a.port or int(os.environ.get("AGENTDYNAMICS_PORT") or eng.cfg["server"]["port"])
|
|
312
|
+
if (a.interval or 1) > 0:
|
|
313
|
+
eng.watch(a.interval)
|
|
314
|
+
from .server import serve
|
|
315
|
+
if a.open:
|
|
316
|
+
webbrowser.open(f"http://{host}:{port}/#/start")
|
|
317
|
+
serve(eng, host, port)
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
sys.exit(main())
|