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.1.tar.gz (16.2 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.1-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: callharness_sdk-0.2.1.tar.gz
  • Upload date:
  • Size: 16.2 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.1.tar.gz
Algorithm Hash digest
SHA256 0c45304a0d711e7fddd164a6c7dec7aad4cb938d5da117562bb8e8e979b61eb4
MD5 35b54f3d79fade8bd9d544b5a59ab42b
BLAKE2b-256 dc582e58b2b5bf69ff5232ab3ac7a92a3a292ced994f4698b7877fb1dda3a0b0

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for callharness_sdk-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8d6d2fec9c5a264c78269ac5b810f563a4250beb75d1ab7ddf0199a195153a41
MD5 180e5a1c0abd84c9fe7e0e431c4b3cf0
BLAKE2b-256 0476bee1aef81aa96f23fc644fc58626382151fa705d35d69af0fa46a371fa00

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