Install
$ agentstack add skill-patterai-skills-inspect-calls-and-metrics ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Inspect calls and metrics with Patter
Patter persists every call to an in-memory MetricsStore (500-call ring buffer by default), optionally backed by disk. The dashboard surfaces live calls, transcripts, latency breakdowns, per-leg cost, and recordings. You can also pull CallMetrics programmatically and export to CSV/JSON for offline analysis.
Mount the live dashboard
Patter ships a dashboard route you can mount on the same server as your agent. Visit http://localhost:8000/dashboard to see live calls.
Python
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
async def main():
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="...",
first_message="Hi!",
)
# dashboard=True mounts /dashboard (UI) + /api/calls (REST) + /sse (live stream)
await phone.serve(agent, tunnel=True, dashboard=True)
asyncio.run(main())
TypeScript
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "...",
firstMessage: "Hi!",
});
await phone.serve({ agent, tunnel: true, dashboard: true });
Open http://localhost:8000/dashboard. Live calls appear at the top, with real-time transcript, current cost, and latency p50/p90/p95/p99.
Read metrics in code
CallMetrics is the canonical model — every finished call produces one. The hook is on_call_end passed as a kwarg to phone.serve(...). It receives a dict (the CallMetrics serialized form), and is async.
Python
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
async def on_end(metrics: dict) -> None:
print(f"Call {metrics['call_id']} | {metrics['duration_seconds']:.1f}s")
cost = metrics.get("cost", {})
print(f" Cost ${cost.get('total_usd', 0):.4f}: "
f"STT ${cost.get('stt_usd', 0):.4f} · "
f"LLM ${cost.get('llm_usd', 0):.4f} · "
f"TTS ${cost.get('tts_usd', 0):.4f} · "
f"Realtime ${cost.get('realtime_usd', 0):.4f} · "
f"Telephony ${cost.get('telephony_usd', 0):.4f}")
print(f" Latency p99: {metrics.get('latency_p99', 0):.0f} ms")
agent = phone.agent(engine=OpenAIRealtime2(), system_prompt="...", first_message="Hi!")
asyncio.run(phone.serve(agent, tunnel=True, on_call_end=on_end))
TypeScript
import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "...",
firstMessage: "Hi!",
});
await phone.serve({
agent,
tunnel: true,
onCallEnd: async (metrics) => {
console.log(`Call ${metrics.call_id} | ${metrics.duration_seconds.toFixed(1)}s`);
console.log(` Cost $${metrics.cost?.total_usd?.toFixed(4) ?? "0"}`);
console.log(` Latency p99: ${metrics.latency_p99?.toFixed(0) ?? "0"} ms`);
},
});
You can also pull live data programmatically from the MetricsStore attached to the server — phone.metrics_store (Python) / phone.metricsStore (TypeScript). It returns None/null until serve() has bound; once the server is live, you can iterate calls.
Export call history
calls_to_csv and calls_to_json operate on the MetricsStore exposed on the running server.
Python
from getpatter import calls_to_csv, calls_to_json
store = phone.metrics_store # available after serve() has bound
if store is not None:
with open("calls.csv", "w") as f:
f.write(calls_to_csv(store))
import json
with open("calls.json", "w") as f:
json.dump(calls_to_json(store), f, indent=2)
TypeScript
import { callsToCsv, callsToJson } from "getpatter";
import { writeFileSync } from "fs";
const store = phone.metricsStore; // available after serve() has bound
if (store) {
writeFileSync("calls.csv", callsToCsv(store));
writeFileSync("calls.json", JSON.stringify(callsToJson(store), null, 2));
}
REST API (when dashboard is mounted)
| Route | Purpose | |---|---| | GET /api/calls | List recent calls (newest first). | | GET /api/calls/{call_id} | Detailed metrics + transcript for one call. | | GET /sse | Server-Sent Events stream of live call updates. |
Example with curl:
curl http://localhost:8000/api/calls | jq '.[] | {id: .call_id, cost: .cost.total_usd, duration: .duration_seconds}'
Persist to disk (survive restarts)
By default the ring buffer is in-memory. Pass persist=True (or a path) to mirror to disk:
Python
phone = Patter(
carrier=Twilio(),
phone_number="+15550001234",
persist=True, # writes to ./.patter/calls.db
# persist="/var/patter/calls.db", # or a custom path
)
TypeScript
const phone = new Patter({
carrier: new Twilio(),
phoneNumber: "+15550001234",
persist: true, // writes to ./.patter/calls.db
});
Lock the dashboard down (bearer token)
When exposing the dashboard beyond 127.0.0.1, gate it with a bearer token. Patter ships token auth out of the box via the dashboard_token kwarg — all /dashboard, /api/calls, and /sse routes then require Authorization: Bearer .
Python
await phone.serve(agent, tunnel=True, dashboard_token="hunter2-rotate-me")
TypeScript
await phone.serve({ agent, tunnel: true, dashboardToken: "hunter2-rotate-me" });
For programmatic FastAPI/Express embedding (not just phone.serve), the lower-level make_auth_dependency(...) (Python) / makeAuthMiddleware(...) (TypeScript) factories are also exported — useful when you mount the dashboard onto your own app and need a custom auth scheme.
Without auth, do not bind to 0.0.0.0 — call transcripts are PII.
What's inside CallMetrics
Key fields (see models.py / metrics.ts for the full list):
| Field | Meaning | |---|---| | call_id | UUID, persistent across the call. | | duration_seconds | Total call wall time. | | turns | List of TurnMetrics (user/agent each). | | cost | CostBreakdown(stt_usd, llm_usd, tts_usd, realtime_usd, telephony_usd, total_usd). | | latency_avg / p50 / p90 / p95 / p99 | Turn-time percentiles in ms. | | provider_mode | "openai_realtime", "elevenlabs_convai", "pipeline". | | stt_provider / llm_provider / tts_provider / telephony_provider | Provider identifiers. | | stt_model / llm_model / tts_model | Model strings. | | transcript | List of (role, text) tuples — set when input_audio_transcription_model is configured. | | context_tokens | (0.7.0, pipeline mode) Peak estimated prompt size across the call, in the chars / 4 token unit — chart it to spot context growth on long calls. |
Recording URLs live in the carrier's payload, surfaced in the call.recording.saved log line — they are not exposed as a typed CallMetrics field since 0.6.3. Enable recording with phone.serve(..., recording=True).
Gotchas
- Ring buffer is 500 calls by default. Older calls are evicted from
memory (still on disk if persist=True). For long-running production, use the on-disk SQLite to query history.
- Dashboard exposes transcripts (PII) — always gate with auth + HTTPS
in production.
on_call_endruns synchronously in the event loop. Don't block —
push to a queue for slow exporters.
- Latency p99 == infinity for short calls with ·
- Metrics reference: ·
- Call logging:
- Pricing module:
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: PatterAI
- Source: PatterAI/skills
- License: MIT
- Homepage: https://www.skills.sh/patterai/skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.