Post-prompt normalization

View as MarkdownOpen in Claude

One conversation can run over voice and over text chat, and both produce a post-prompt body, but not the same shape. The signalwire.core.post_prompt module absorbs that divergence so your application sees one artifact regardless of which engine finished the conversation.

FieldVoiceChat
app_name"swml app""ai_chat"
conversation_idabsentpresent at top level
Full lograw_call_lograw_messages
Summary arrives asa summarize_conversation tool calla bare role: assistant turn inside call_log
post_prompt_dataparsed object{"raw": "<fenced JSON string>"}

conversation_type is a reliable top-level discriminator on both. The voice engine can also deliver post_prompt_data as {"parsed": [ {...} ], "raw": "..."}, an object wrapped in a list, which passes structural checks and misses every field lookup. The parser unwraps it.

The module doesn’t decide what a summary should contain. The schema is whatever your post-prompt text asked the model to produce, so parsing is schema-agnostic and returns the dict as found. Nothing here raises: the conversation that produced the body is already over.

from signalwire.core.post_prompt import NormalizedPostPrompt, normalize_post_prompt

Properties

NormalizedPostPrompt is a frozen dataclass, one finished conversation leg in a shape that doesn’t vary by engine.

medium
strDefaults to ""

conversation_type as reported, such as "voice" or "chat". Empty when the engine didn’t say.

conversation_id
str | NoneDefaults to None

Present on chat, absent on voice. When None, fall back to your own key from global_data or call_id rather than treating this as authoritative.

summary
dict[str, Any]Defaults to {}

The parsed post_prompt_data, with whatever keys your post-prompt asked for. {} when there was none or it couldn’t be parsed. A model that answered in prose instead of JSON yields {"summary": "<the prose>"}.

dialogue
list[dict[str, str]]Defaults to []

user and assistant turns only, as {"role", "content"} pairs, with tool calls and the chat engine’s summary echo removed.

call_id
str | NoneDefaults to None

The platform call ID, when present.

raw
dict[str, Any]Defaults to {}

The complete request body, untouched.

Functions

Example

Store every finished leg the same way, whether it came from on_summary() or on_call_end():

from signalwire import AgentBase
from signalwire.core.post_prompt import normalize_post_prompt
class DispatchAgent(AgentBase):
def __init__(self):
super().__init__(name="dispatch", route="/dispatch")
self.set_prompt_text("You are Ada, the dispatcher for Bayview Taxi.")
self.set_post_prompt("Summarize the call as JSON with keys intent and resolved.")
def on_summary(self, summary, raw_data=None):
leg = normalize_post_prompt(raw_data)
if leg.dialogue:
# Write to your system of record.
print(leg.medium, leg.conversation_id or leg.call_id, leg.summary)
DispatchAgent().serve()