Skip to main content

callharness-sdk

Python SDK for CallHarness — open-source call analytics for voice AI agents.

Send your agent's calls to a CallHarness server, which runs post-call LLM analysis (summary, sentiment, outcome, why a call transferred or didn't complete) and shows it on a dashboard. Self-hosted, so your transcripts stay on your own infrastructure.

Install

pip install callharness-sdk            # REST client + turn assembly
pip install "callharness-sdk[pipecat]" # also installs pipecat-ai for the observers

The [pipecat] extra only adds pipecat-ai. Skip it if you're on LiveKit, a custom stack, or calling the REST API directly — callharness_sdk.pipecat still imports cleanly without it, and only raises if you actually instantiate an observer.

Direct ingestion

from callharness_sdk import CallHarnessClient

client = CallHarnessClient("http://localhost:8010")
call = client.ingest_call(
    agent_id="my-agent",
    end_reason="completed",
    turns=[
        {"role": "assistant", "text": "Hi, how can I help?"},
        {"role": "user", "text": "I'd like to book an appointment."},
    ],
)
client.upload_recording(call["id"], "recording.wav")

Pipecat integration

from pipecat.processors.transcript_processor import TranscriptProcessor
from callharness_sdk.pipecat import create_recorder

transcript = TranscriptProcessor()
recorder = create_recorder("http://localhost:8010", agent_id="my-agent")
recorder.attach(transcript)

# include transcript.user() after STT and transcript.assistant() after TTS
# in your pipeline, then when the call ends:
await recorder.flush(end_reason="completed")

To capture per-turn STT/LLM/TTS latency, add the metrics observer to your task:

from callharness_sdk.pipecat import CallHarnessMetricsObserver

task = PipelineTask(
    pipeline,
    params=PipelineParams(enable_metrics=True),
    observers=[CallHarnessMetricsObserver(recorder)],
)

Pipecat without TranscriptProcessor

If your pipeline doesn't use TranscriptProcessor (e.g. you capture transcripts at the frame level), use the all-in-one frame observer instead — it captures transcript turns, end-to-end response latency, STT/LLM/TTS components, interruptions, tool calls, transfers, and a deterministic end_reason, all from one observer:

from callharness_sdk.pipecat import CallHarnessFrameObserver, create_recorder

recorder = create_recorder("http://localhost:8010", agent_id="my-agent")
observer = CallHarnessFrameObserver(
    recorder, stt=stt, tts=tts, transfer_tool_names={"transfer_to_human"}
)
# add `observer` to your PipelineTask/PipelineWorker observers, then on call end:
await recorder.flush(
    end_reason=observer.finalize_end_reason(),  # "completed" | "transferred" | "error" | ...
    transferred=observer.transferred,
    recording_bytes=wav_bytes,   # optional in-memory recording upload
)

finalize_end_reason() gives you the best reason available right now: "error" if a fatal ErrorFrame occurred, otherwise the explicit reason= you passed to EndTaskFrame/CancelTaskFrame if the pipeline already saw one before you called this (e.g. EndTaskFrame(reason="silence_timeout") from a UserIdleProcessor callback), otherwise "transferred" if a transfer fired, otherwise "completed" (pass a different default= if that's not right for your integration).

Call finalize_end_reason() — not the raw observer.end_reason attribute — from your own disconnect/teardown handler (e.g. a transport's on_client_disconnected). That fires before an EndFrame/CancelFrame has necessarily propagated through the pipeline, so .end_reason may still be None at that point; finalize_end_reason() only depends on state (fatal error, transferred) that's already known live during the call, so it's correct regardless of teardown ordering. observer.last_error holds the most recent error message, useful to drop into metadata for debugging.

Call recording

Two lines. The audio uploads itself when you flush() — there is no second call to remember:

from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor
from callharness_sdk.pipecat import attach_audio

audio_buffer = AudioBufferProcessor(num_channels=2)
attach_audio(recorder, audio_buffer)

pipeline = Pipeline([
    transport.input(), stt, llm, tts,
    transport.output(),
    audio_buffer,          # ← AFTER transport.output()
])
await audio_buffer.start_recording()

Two placement details decide whether the recording is worth having:

  • After transport.output(). Placed earlier it records what the bot intended to say, so a sentence the caller interrupted is captured in full even though nobody heard it — and the recording then disagrees with the transcript at exactly the moments you are most likely to be investigating.
  • num_channels=2 puts the caller on the left and the bot on the right. With a mono mix, overlapping speech is unusable precisely when there was an interruption.

The dashboard plays it inline on the call page, and clicking a transcript line seeks the audio to that moment. Recordings expire on the server after CALLHARNESS_RECORDING_RETENTION_DAYS (default 30); transcripts and analysis are kept indefinitely.

If your agent already has its own call pipeline

Mature agents usually already collect a transcript, their own record of tool calls, and write to their own database. Adopting CallRecorder would mean maintaining two sources of truth — so instead, hand CallHarness what you already have:

from callharness_sdk import CallHarnessClient, LatencyCollector, assemble_turns
from callharness_sdk.pipecat import CallHarnessMetricsObserver

latency = LatencyCollector()          # satisfies what the observer expects
task = PipelineTask(
    pipeline,
    params=PipelineParams(enable_metrics=True),   # required, or no metrics are emitted
    observers=[CallHarnessMetricsObserver(latency)],
)

# ...at the end of the call, from your own save routine:
turns = assemble_turns(
    transcript=my_transcript,        # [{role, content, timestamp}, ...]
    tool_calls=my_function_calls,    # [{function_name, parameters, result, timestamp}]
    latency=latency,
    started_at=call_started_at,
)
CallHarnessClient("http://localhost:8010").ingest_call(
    agent_id="my-agent", turns=turns, external_id=my_call_id,
    transferred=..., metadata={"my_own_verdict": ...},
)

assemble_turns() does the fiddly part: a tool call and a latency sample both happen while a reply is being produced, before its text exists, so both are matched by timestamp to the assistant turn they actually belong to. It accepts either field naming (name/function_name, arguments/parameters, content/text), truncates oversized tool results, and never records a tool as successful unless it can prove it.

Anything you send in metadata is stored alongside the call — useful if your agent already classifies its own calls and you want to compare that against CallHarness's independent verdict.

Full example

See examples/pipecat_bot.py for a complete working bot.

Licence

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

callharness_sdk-0.2.0.tar.gz (15.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

callharness_sdk-0.2.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file callharness_sdk-0.2.0.tar.gz.

File metadata

  • Download URL: callharness_sdk-0.2.0.tar.gz
  • Upload date:
  • Size: 15.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.10

File hashes

Hashes for callharness_sdk-0.2.0.tar.gz
Algorithm Hash digest
SHA256 b4d19e8302df5045a60d1789cbb9a1a9515544f0e2bed68741b61e1a9f5e363d
MD5 19224ffebb2bc4cdf336cf6161a41b6a
BLAKE2b-256 2de85c2bf57e8c2923fabe3093e09af33258b6c8aeb24cf9b573f1569e25bd42

See more details on using hashes here.

File details

Details for the file callharness_sdk-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for callharness_sdk-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7244b63f151bfd2b8fedc6e16f7c99978e8051022d9d8a0687878e2a225e7beb
MD5 a99f4841733805f766851b2228b7d701
BLAKE2b-256 1c8e8090ec6a58a464e9676d2f79ea86ecc5b26f359d79e357e47af96fc7b386

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page