agentsclimarketplace

Inspect calls and metrics

Skill PatterAI/skills/inspect-calls-and-metrics

Agent Skills for the Patter SDK — give your AI agent a phone number. Works in Claude Code, Cursor, OpenClaw, Hermes Agent, Codex, Cline, Goose, Amp, Windsurf, and any harness that consumes the Agent Skills standard. One CLI: npx skills add patterai/skills

Install
npx -y skills add PatterAI/skills --skill inspect-calls-and-metrics

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Inspect Patter call data — mount the live dashboard, read CallMetrics (duration, cost, latency, transcript), export call history to CSV/JSON, and track per-call provider costs. Use when the user wants to debug a call, see which calls cost the most, audit transcripts, monitor a live agent, find a recording URL, check latency p99, export call history for reporting, or watch the dashboard during a demo. Covers Patter 0.7.0's in-memory MetricsStore + 500-call ring buffer, the FastAPI/Express dashboard mount, the REST API and SSE stream — in both Python and TypeScript.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

10.2 KB, as published. Nobody here has run it

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)

RoutePurpose
GET /api/callsList recent calls (newest first).
GET /api/calls/{call_id}Detailed metrics + transcript for one call.
GET /sseServer-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 <token>.

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):

FieldMeaning
call_idUUID, persistent across the call.
duration_secondsTotal call wall time.
turnsList of TurnMetrics (user/agent each).
costCostBreakdown(stt_usd, llm_usd, tts_usd, realtime_usd, telephony_usd, total_usd).
latency_avg / p50 / p90 / p95 / p99Turn-time percentiles in ms.
provider_mode"openai_realtime", "elevenlabs_convai", "pipeline".
stt_provider / llm_provider / tts_provider / telephony_providerProvider identifiers.
stt_model / llm_model / tts_modelModel strings.
transcriptList 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_end runs synchronously in the event loop. Don't block — push to a queue for slow exporters.
  • Latency p99 == infinity for short calls with < 100 turns. The percentile estimator needs ~100 samples; for shorter calls use latency_avg.
  • Cost breakdown precision: provider price tables are baked into pricing.py at SDK release time. If a provider changes pricing mid-quarter, your cost.*_usd will drift. Use merge_pricing({...}) to override.

Common errors

SymptomFix
Dashboard 404sYou passed dashboard=False to phone.serve(...). The default is True — drop the kwarg.
Dashboard shows no callsServer restarted with persistence off. persist=True is the default since 0.6.3; verify you didn't override it.
Transcript is emptyRealtime mode without input_audio_transcription_model set, or guardrail blocked the response.
Recording URL is missing from logsrecording=True wasn't passed to phone.serve(...), or the carrier doesn't have recording enabled.
cost.total_usd is 0The provider isn't in Patter's pricing table. Override per-provider rates via merge_pricing({...}) on the Patter constructor.

Related skills

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.