> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # Post-prompt normalization > Read voice and chat post-prompt bodies as one shape, with the summary parsed and the dialogue extracted. [normalize]: /docs/server-sdks/reference/python/core/post-prompt/normalize-post-prompt [parse]: /docs/server-sdks/reference/python/core/post-prompt/parse-post-prompt-data [dialogue]: /docs/server-sdks/reference/python/core/post-prompt/dialogue-turns [strip]: /docs/server-sdks/reference/python/core/post-prompt/strip-json-fence [on-summary]: /docs/server-sdks/reference/python/agents/agent-base/on-summary [on-call-end]: /docs/server-sdks/reference/python/agents/agent-base/on-call-end 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. | Field | Voice | Chat | | ------------------ | ------------------------------------ | ----------------------------------------------- | | `app_name` | `"swml app"` | `"ai_chat"` | | `conversation_id` | absent | present at top level | | Full log | `raw_call_log` | `raw_messages` | | Summary arrives as | a `summarize_conversation` tool call | a bare `role: assistant` turn inside `call_log` | | `post_prompt_data` | parsed object | `{"raw": ""}` | `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. ```python 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`** `str` — default: "" `conversation_type` as reported, such as `"voice"` or `"chat"`. Empty when the engine didn't say. --- **`conversation_id`** `str | None` — default: 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]` — default: \{} 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": ""}`. --- **`dialogue`** `list[dict[str, str]]` — default: \[] `user` and `assistant` turns only, as `{"role", "content"}` pairs, with tool calls and the chat engine's summary echo removed. --- **`call_id`** `str | None` — default: None The platform call ID, when present. --- **`raw`** `dict[str, Any]` — default: \{} The complete request body, untouched. --- ## Functions #### [normalize\_post\_prompt](/docs/server-sdks/reference/python/core/post-prompt/normalize-post-prompt) Normalize a post-prompt body from either engine. #### [parse\_post\_prompt\_data](/docs/server-sdks/reference/python/core/post-prompt/parse-post-prompt-data) Return post\_prompt\_data as a plain dict, whichever shape it arrived in. #### [dialogue\_turns](/docs/server-sdks/reference/python/core/post-prompt/dialogue-turns) Extract the user and assistant turns from a call log. #### [strip\_json\_fence](/docs/server-sdks/reference/python/core/post-prompt/strip-json-fence) Unwrap a fenced JSON code block. ## Example Store every finished leg the same way, whether it came from [`on_summary()`][on-summary] or [`on_call_end()`][on-call-end]: ```python {11-14} 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() ``` > Read voice and chat post-prompt bodies as one shape, with the summary parsed and the dialogue extracted. ## Docs - [dialogue_turns](https://signalwire.com/docs/server-sdks/reference/python/core/post-prompt/dialogue-turns.md): Extract the user and assistant turns from a call log, dropping tool traffic and the summary echo. - [normalize_post_prompt](https://signalwire.com/docs/server-sdks/reference/python/core/post-prompt/normalize-post-prompt.md): Normalize a post-prompt body from the voice or chat engine into one shape. - [parse_post_prompt_data](https://signalwire.com/docs/server-sdks/reference/python/core/post-prompt/parse-post-prompt-data.md): Return post_prompt_data as a plain dict, whichever of its three shapes arrived. - [strip_json_fence](https://signalwire.com/docs/server-sdks/reference/python/core/post-prompt/strip-json-fence.md): Unwrap a fenced JSON code block returned verbatim by the chat engine.