# hist


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Imports

``` python
from fastllm.chat import mk_msgs, mk_msg, tool_dtls_tag, hist2fmt
from fastcore.test import *
import random, json
from IPython.display import Markdown
```

``` python
def tool():
    "dummmy tool"
    pass
```

``` python
random.seed(6)
```

### Overview

<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 25%" />
<col style="width: 45%" />
</colgroup>
<thead>
<tr>
<th>Function</th>
<th>Purpose</th>
<th>Input → Output</th>
</tr>
</thead>
<tbody>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#render_outputs_ai"><code>render_outputs_ai</code></a></td>
<td>Render Jupyter outputs as plain text</td>
<td><code>list[dict]</code> → <code>str</code></td>
</tr>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#ai_fmt"><code>ai_fmt</code></a></td>
<td>Format an AI reply for chat history</td>
<td><code>str</code> → <code>str</code></td>
</tr>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#message.to_xml"><code>Message.to_xml</code></a></td>
<td>Convert message to XML</td>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/dialog.html#message"><code>Message</code></a>
→ <code>str</code></td>
</tr>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#message.to_media"><code>Message.to_media</code></a></td>
<td>Gather media from message</td>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/dialog.html#message"><code>Message</code></a>
→ <code>list[str\|bytes]</code></td>
</tr>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#message.to_parts"><code>Message.to_parts</code></a></td>
<td>Convert message to history parts</td>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/dialog.html#message"><code>Message</code></a>
→ <code>list[str\|bytes]</code></td>
</tr>
<tr>
<td><a
href="https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2hist"><code>dlg2hist</code></a></td>
<td><strong>Main entry point</strong></td>
<td><code>list[Message], dict</code> → <code>hist</code></td>
</tr>
</tbody>
</table>

## Test helpers

`mk` is a convenience function to create conformant dummy data for
testing. For `msg_type=='code'` cells, output text is a serialized
structure following nbformat v4. For creating tests, we have a helper
function
[`jwrap`](https://AnswerDotAI.github.io/llmsurgery/hist.html#jwrap) that
wraps output text in this jupyter v4 format.

``` python
dname = 'backend'
dlg = Dialog(dname)
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L28"
target="_blank" style="float:right; font-size:smaller">source</a>

### jwrap

``` python
def jwrap(
    txts:list=['']
):
```

*Wrap plain `txt` in a minimal Jupyter-style cell output*

``` python
def mk(msg_type: str, content=None, output='', id=None, attachments=None):
    "Return a `Message`, ensuring `output` is always valid JSON. Uses timestamp-based id if not provided."
    if msg_type==scode: output = jwrap(output)
    if msg_type==sprompt: output = prompt_output(output)
    res = Message(msg_type=msg_type, output=output, attachments=attachments)
    res.content = content or f'{msg_type}>{res.id}'
    res.dlg = dlg # to avoid weakref issue
    return res
```

``` python
jwrap("test output")
```

    [{'output_type': 'stream', 'name': 'stdout', 'text': 'test output'}]

Code cells automatically
[`jwrap`](https://AnswerDotAI.github.io/llmsurgery/hist.html#jwrap) the
output value:

``` python
mk('code', 'print("hello")', 'hello\n')
```

<div class="prose" markdown="1">

print(“hello”) ⇒ \[{‘output_type’: ‘stream’, ‘name’: ‘stdout’, ‘text’:
‘hello’}\]

<details>

- id: \_29f88512
- meta: {}
- msg_type: code

</details>

</div>

``` python
mk('prompt', 'What is 2+2?', 'The answer is 4')
```

<div class="prose" markdown="1">

What is 2+2? ⇒ \[{‘output_type’: ‘display_data’, ‘metadata’:
{‘is_ai_res’: True}, ‘data’: {‘text/markdown’: ‘The answer is 4’}}\]

<details>

- id: \_004af0bf
- meta: {}
- msg_type: prompt

</details>

</div>

## Rendering outputs for the AI

The model sees a plain-text rendering of Jupyter outputs: streams
concatenated as they would appear on a terminal screen, ANSI codes
stripped, markdown preferred over HTML, images excluded (they travel
separately, as media parts). The Jupyter-output primitives —
`preferred_out`, `preferred_msg_out`, `concat_streams`, and the
screen-semantics cleaning they build on — live in `fastcore.nbio` and
`fastcore.xtras`; this module only adds the choices specific to
model-facing text.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L33"
target="_blank" style="float:right; font-size:smaller">source</a>

### join_out

``` python
def join_out(
    d
):
```

*Join Jupyter’s list-of-lines output data into one string*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L52"
target="_blank" style="float:right; font-size:smaller">source</a>

### render_outputs_ai

``` python
def render_outputs_ai(
    outputs, renderers:NoneType=None
):
```

*Plain-text rendering of a Jupyter output list for LLM context*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L46"
target="_blank" style="float:right; font-size:smaller">source</a>

### render_output_ai

``` python
def render_output_ai(
    out, renderers:NoneType=None
):
```

*Plain-text rendering of one Jupyter output, as the AI sees it;
`renderers` overrides/extends the per-mime table*

Each output type has a sensible plain-text form; note the stream
applying the `\r` overwrite, and the error rendering its traceback:

``` python
test_eq(render_outputs_ai([{'output_type': 'stream', 'name': 'stdout', 'text': 'Hello\rBye\n'}]), 'Bye\n')  # chkstyle: ignore
test_eq(render_output_ai({'output_type': 'execute_result', 'data': {'text/plain': '"42"'}}), '"42"')
test_eq(render_output_ai({'output_type': 'display_data', 'data': {'text/markdown': '**bold**'}}), '**bold**')
test_eq(render_output_ai({'output_type': 'display_data', 'data': {'text/latex': '$x^2$'}}), '$x^2$')  # verbatim by default
test_eq(render_output_ai({'output_type': 'display_data', 'data': {'text/latex': '$x^2$'}}, renderers={'text/latex': str.upper}), '$X^2$')
test_eq(render_output_ai({'output_type': 'error', 'traceback': ['Error: something broke']}), 'Error: something broke')
test_eq(render_output_ai({'output_type': 'display_data', 'data': {'text/html': '<b>hi</b>', 'text/plain': 'hi'}}), '<b>hi</b>')
assert not render_output_ai({'output_type': 'execute_result', 'data': {'text/plain': ''}})
```

## Reply formatting

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L64"
target="_blank" style="float:right; font-size:smaller">source</a>

### ai_fmt

``` python
def ai_fmt(
    out, strip_tools:bool=False
):
```

*Format AI response as it will be attached to the chat history*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L60"
target="_blank" style="float:right; font-size:smaller">source</a>

### strip_tool_blocks

``` python
def strip_tool_blocks(
    s
):
```

*Remove the tool-call and token-usage details blocks (the fastllm reply
conventions) from reply text `s`*

Interrupted-response markers are stripped, and an empty reply becomes an
explicit pending placeholder so the history never carries a blank turn.
`strip_tools` also removes embedded tool-call and token-usage details
blocks (the fastllm reply conventions).

``` python
test_eq(ai_fmt('Hi.\n*[Response interrupted]*'), 'Hi.')
test_eq(ai_fmt(''), '<output result="pending" reason="incomplete"/>')
dtl = f"{tool_dtls_tag}\n\n```json\n{{}}\n```\n\n</details>"
test_eq(strip_tool_blocks(f'{dtl}\nDone.'), '\nDone.')  # verbatim minus the blocks
test_eq(ai_fmt(f'{dtl}\nDone.', strip_tools=True), 'Done.')
```

## Message AI views

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L73"
target="_blank" style="float:right; font-size:smaller">source</a>

### try_eval

``` python
def try_eval(
    s, typ:str | None=None
):
```

*Like `literal_eval`, but wraps in dynamic class named `typ` if
succeeds, and returns `s` if fails*

`ai_output` is the model-facing text of a message’s output, computed
through `render_out` and cached in `_ai_rend`. `render_out` computes
only the AI side; a UI (like Solveit) overrides it on its
[`Message`](https://AnswerDotAI.github.io/llmsurgery/dialog.html#message)
subclass to compute and cache its HTML rendering too, exposed through
properties of its own.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L94"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.ai_output

``` python
def ai_output():
```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L85"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.render_out

``` python
def render_out():
```

*Compute and cache the for-AI rendering of this message’s output*

``` python
test_eq(Message('1+1', msg_type=scode, output=code_output('2')).ai_output, '2')
test_eq(Message('1+1', msg_type=scode).ai_output, '')  # No output yet
m = Message('test', msg_type=sprompt, output=prompt_output('Ok!\n\n*[Response interrupted]*\n'))
test_eq(m.ai_output, 'Ok!')
test_eq(Message('empty', msg_type=sprompt, output=[]).ai_output, '<output result="pending" reason="incomplete"/>')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L107"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.local_time

``` python
def local_time():
```

*Localized `time_run` for hosts that stamp one; ’’ when absent*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L99"
target="_blank" style="float:right; font-size:smaller">source</a>

### to_local_time

``` python
def to_local_time(
    time_str, tz:str='UTC'
):
```

*Convert ISO time string to local timezone*

``` python
test_eq(to_local_time(''), '')
test_eq(to_local_time(None), '')
result = to_local_time('2026-01-27T12:00:00+00:00', 'UTC')
assert '2026-01-27' in result

m = Message('test', msg_type=snote)
test_eq(m.local_time(), '')

m.time_run = '2026-01-27T12:00:00+00:00'
assert m.local_time()
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L115"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.todict

``` python
def todict():
```

*Call self as a function.*

## Media

Media in the LLM context is represented by an XML metadata tag followed
by raw bytes.

For successfully loaded media, the context contains:

- a `<media>` tag with `id`, `type`, and `mime` attributes
- the raw media bytes immediately after the tag

If media cannot be loaded or decoded, the context contains a
`<media-unavailable>` tag instead of silently dropping it. This lets the
AI tell the user that the media was referenced but was not available.

`id=` is:

- UUID for attachments referenced via `attachment:uuid` in markdown
- variable name for media in variables
- content hash for code output images

`type=` is one of:

- `output` — media from code cell output
- `variable` — media data in a variable
- `content` — media embedded in markdown message content

### Media context tags

Media are included via a `<media>` tag followed immediately by the raw
bytes. The tag gives the AI a stable id, media type, and MIME type.

For unavailable media, we add a `<media-unavailable>` tag instead. This
is used when media was referenced by the user but could not be loaded,
decoded, resized, or safely resolved. The tags instructs the AI to tell
the user that it could not see the referenced file.

Model info has the following fields `supports_vision`,
`supports_video_input`, `supports_pdf_input`, `supports_audio_input` to
indicate specific media type support

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L148"
target="_blank" style="float:right; font-size:smaller">source</a>

### media_item

``` python
def media_item(
    media_type, media_id, data, aim_info, mime:NoneType=None, max_im_sz:NoneType=None, prep:NoneType=None,
    unavail_msg:str='An unsupported media type was included in the context: {mime}. Please instruct the user to switch to a model that supports this media type.'
):
```

*Load/prepare/package media, returning \[xml_tag, data\] or
\[media-unavailable\].*

``` python
detect_mime('https://image.jpg')
```

A helper function and small dummy img for testing purposes.

``` python
def _mk_img(imgb):
    "Image bytes from a base64 str"
    return base64.b64decode(imgb)
```

``` python
_imgb64 = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGP4DwABAQEAsTj2FAAAAABJRU5ErkJggg=='
```

Valid media should produce a `<media>` tag plus bytes

``` python
png = _mk_img(_imgb64)
res = media_item('content', 'img', png, dict(supports_vision=True))
test_eq(res[0], '<media id="img" type="content" mime="image/png"></media>')
test(res[1], bytes, isinstance)
```

Expected media-loading failures should produce `<media-unavailable>`

``` python
for data in (None, 'not bytes', b'not media'): test(media_item('content', 'x', data, {})[0], '<media-unavailable', operator.contains)

def bad_b64_loader(): raise binascii.Error('bad base64')
test(media_item('output', 'bad', bad_b64_loader, {})[0], '<media-unavailable', operator.contains)
```

``` python
def bad_loader(): raise RuntimeError('boom')
with expect_fail(Exception, 'boom'): media_item('content', 'x', bad_loader, {})
```

``` python
test(media_item('content', 'img', png, dict(supports_vision=False))[0], "An unsupported media type was", operator.contains)
test(media_item('content', 'img', png, dict(supports_vision=False))[0], 'mime="image/png"', operator.contains)
```

### [`_media_atts`](https://AnswerDotAI.github.io/llmsurgery/hist.html#_media_atts)

These are attachments found in a message such as the ones uploaded from
the clipboard or the screenshot of the last output.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L168"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.prep_img

``` python
def prep_img(
    data, mime, max_im_sz:NoneType=None
):
```

*Prepare image bytes before they enter LLM context; the base class
passes them through unchanged*

``` python
def parse_att_id(s):
    "Extract id attribute from XML tag string"
    return re.search(r' id="([^"]*)"', s).group(1)
```

``` python
_mk_img(_imgb64)[:40]
```

    b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00:~\x9bU\x00\x00\x00\nIDA'

Only valid image attachments should be included:

``` python
att1,att2,att3 = Attachment(png, 'image/png'), Attachment(b'not an image', 'text/plain'), Attachment(_mk_img(_imgb64), 'image/png')
m = Message(f'![](attachment:{att1.id}) and ![](attachment:{att3.id})', attachments=[att1, att2, att3])
res = _media_atts(m, dict(supports_vision=True))
```

``` python
test_eq(len(m.attachments), 3)
test_eq(len(res), 4)
test_eq(L(res[::2]).map(parse_att_id), [att1.id, att3.id])
```

Only the attachments referenced in the message content should be
included:

``` python
m = Message(f'Only ![](attachment:{att3.id})', attachments=[att1,att3])
res = _media_atts(m, dict(supports_vision=True))

test_eq(len(m.attachments), 2)
test_eq(L(res[::2]).map(parse_att_id), [att3.id])
```

``` python
bad = Attachment(b'not an image', 'image/png')
res = _media_atts(Message(f'![](attachment:{bad.id})', attachments=[bad]), dict(supports_vision=True))
test(res[0], '<media-unavailable', operator.contains)
```

These are images from the message outputs, such as a code output
displaying a plot

``` python
test_eq(_img_id(_mk_img(_imgb64)), _img_id(_mk_img(_imgb64)))
_img_id(_mk_img(_imgb64))
```

    '4P52II3F'

``` python
img_out = mk_code_output({'image/png': base64.b64encode(png).decode()})
m = Message(msg_type='code', output=img_out)
```

``` python
res = _img_output(m, dict(supports_vision=True), None)
test_eq(len(res), 2)
test_eq(res[0], _media_xml('output', _img_id(png), 'image/png'))
```

``` python
m = Message(msg_type='code', output=mk_code_output({'image/png': 'not-base64'}))
res = _img_output(m, dict(supports_vision=True), None)
test(res[0], '<media-unavailable', operator.contains)
```

## Message XML Helpers

These methods convert
[`Message`](https://AnswerDotAI.github.io/llmsurgery/dialog.html#message)
objects into XML for LLM context, using the shared `item2xml` grammar
(per-type tags, content bare inside, an `<out>` section when output is
present). Prompts render as their bare content via the `prompt_txt` host
hook: in history, the reply is the next turn, not part of the message.

`to_xml` combines the msg input and output. A prompt renders through
`prompt_txt` — bare content here, with `last` marking the live request;
a host that wants an envelope around its prompts patches `prompt_txt`
alone:

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L205"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.to_xml

``` python
def to_xml(
    last:bool=False
):
```

*Create XML representation of this message*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L200"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.prompt_txt

``` python
def prompt_txt(
    last:bool=False
):
```

*History text for a prompt message: the content, bare. Hosts patch this
to add their own envelope*

``` python
# Combines content and output into full XML representation
m = mk('code', 'print("hi")', 'hi\n')
test_eq(m.to_xml(), f'<code id="{m.id}">print("hi")<out>hi\n</out></code>')
n = mk('note', '# hey')
test_eq(n.to_xml(), f'<markdown id="{n.id}"># hey</markdown>')  # notes render as markdown; no out, no extra tags
```

    '<message type="code" id="_624f8907" time=""><cell-source>print("hi")</cell-source><output>hi\n</output></message>'

``` python
# Prompts render bare
test_eq(mk('prompt', 'question').to_xml(), 'question')
test_eq(mk('prompt', 'question').prompt_txt(last=True), 'question')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L218"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.to_media

``` python
def to_media(
    aim_info, max_im_sz:NoneType=None
):
```

*Aggregate media from attachments, `media_extra`, and code outputs*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L213"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.media_extra

``` python
def media_extra(
    aim_info, max_im_sz:NoneType=None
):
```

*Additional media sources contributed by subclasses (e.g. app-specific
file references)*

``` python
# No media for plain messages
test_eq(mk('note', 'hello').to_media({}), [])
```

``` python
# Code output images
m = Message(msg_type='code', output=mk_code_output({'image/png': base64.b64encode(png).decode()}))
media = m.to_media(dict(supports_vision=True))
test_eq(len(media), 2)  # xml tag, bytes
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L229"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.to_parts

``` python
def to_parts(
    aim_info:dict, last:bool=False
):
```

*Convert message to a list of history parts: media, then XML text.*

``` python
att = Attachment(_mk_img(_imgb64), 'image/png')
m = mk('prompt', f'What is in ![](attachment:{att.id})?', attachments=[att])
res = m.to_parts(dict(supports_vision=True))
['...bytes...' if isinstance(o,bytes) else o for o in res]
```

    ['<media id="f4ec230f-412a-4287-b7ff-0a4660e1f5e1" type="content" mime="image/png"></media>',
     '...bytes...',
     'What is in ![](attachment:f4ec230f-412a-4287-b7ff-0a4660e1f5e1)?']

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L237"
target="_blank" style="float:right; font-size:smaller">source</a>

### dlg2hist

``` python
def dlg2hist(
    dlg, # A `Dialog`, or iterable of messages, ending with a prompt
    aim_info:dict, # Model capability dict for media handling
):
```

*Convert `dlg` to LLM history. The final prompt renders with
`last=True`.*

## Dialogs to canonical messages

[`dlg2hist`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2hist)
produces alternating user parts and AI reply strings. For surgery we
usually want the replies’ tool calls back as structured messages, and
fastllm’s `fmt2hist` inverts the reply format exactly.
[`dlg2chat`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2chat)
composes the two into the canonical form every provider conversion
starts from, and rejects duplicate tool-call ids, which hand-authored
dialogs can accidentally produce. Thinking blocks survive only in the
stored reply string: explode/implode
([`reply2dlg`](https://AnswerDotAI.github.io/llmsurgery/hist.html#reply2dlg)/[`dlg2reply`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2reply))
and this transmission projection both drop them, a deliberate loss since
nothing downstream keeps them.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L271"
target="_blank" style="float:right; font-size:smaller">source</a>

### dlg2chat

``` python
def dlg2chat(
    dlg, # A `Dialog`, ending with a prompt
    aim_info:NoneType=None, # Model capability dict for media handling; images enabled if None
):
```

*Canonical fastllm messages for `dlg`, with each reply’s tool calls
recovered as real parts*

The conversion tests below need replies carrying tool-call details
blocks, so first a fixture that builds one in fastllm’s reply format.

``` python
def _mk_dtl(func, args, result, tid='x1'):
    d = json.dumps(dict(id=tid, server=False, call=dict(function=func, arguments=args), result=result))
    return f"{tool_dtls_tag}\n<summary><code>{func}(...)</code></summary>\n\n```json\n{d}\n```\n\n</details>"

py_dtl = _mk_dtl('python', {'code':'1+1'}, '2')
```

``` python
ddlg = Dialog('canon')
ddlg.mk_message('What is 1+1?', msg_type=sprompt, output=f"Check:\n\n{py_dtl}\n\nIt is 2.")
cm = dlg2chat(ddlg)
test_eq([m.role for m in cm], ['user', 'assistant', 'tool', 'assistant'])
test_eq(cm[1].content[1].data['name'], 'python')
```

One reply often holds several sequential tool calls, each issued after
reading the previous result. `fmt2hist` returns them batched into a
single call message and a single result message, which reads as one
parallel volley;
[`dlg2chat`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2chat)
explodes the batch back into strict call/result alternation, the shape
live sessions record.

``` python
py_dtl2 = _mk_dtl('python', {'code':'2+2'}, '4', 'x2')
sdlg = Dialog('seq')
sdlg.mk_message('Two steps.', msg_type=sprompt, output=f"{py_dtl}\n\n{py_dtl2}\n\nDone.")
sm = dlg2chat(sdlg)
test_eq([(m.role, len(m.content)) for m in sm], [('user',1), ('assistant',1), ('tool',1), ('assistant',1), ('tool',1), ('assistant',1)])
test_eq(sm[2].content[0].data['id'], sm[1].content[0].data['id'])
test_eq(sm[4].content[0].data['id'], sm[3].content[0].data['id'])
```

``` python
bad = Dialog('dup')
bad.mk_message('a', msg_type=sprompt, output=py_dtl)
bad.mk_message('b', msg_type=sprompt, output=py_dtl)
with expect_fail(ValueError, 'duplicate tool call id'): dlg2chat(bad)
```

``` python
raw = [
    mk('prompt', 'hello there', output='Hello! How can I help?'),
    mk('note', 'a note between turns'),
    mk('code', output="aaa", content="'some source text'"),
    mk('prompt', 'add the numbers', output='They sum to 4.'),
    mk('note'), 
    mk('note'), 
    mk('code', output="333"),
    mk('prompt'),  # empty output on purpose
    mk('prompt', 'summarize the conversation so far', output='We greeted, then added some numbers.'),
    mk('note', 'below we insert an image'),
    mk('code', 'img=..some code to read image bytes'),
    mk('prompt', 'Please tell me what you see in the image', output='I can see...'),
    mk('note', 'lets add an image note'),
    mk('prompt', 'Please tell me what you see in the 2 images in the above msg',output='i see two puppies!'),
    mk('prompt', 'And one final question.')]
```

``` python
aim_info = dict(supports_vision=True)
```

A realistic message mix — prompts with and without preceding notes and
code, an empty AI response, media references — checks the turn
structure:

``` python
*hist,p,_ = dlg2hist(raw, aim_info)
```

Lets take a look at the history:

``` python
for idx, turn in enumerate(mk_msgs(hist)):
    display(Markdown(f"**======turn: {idx}======**"))
    display(turn)
```

<div class="prose" markdown="1">

**======turn: 0======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

hello $`x`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 1======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

Hello! I can see the variable `x`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 2======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

<message type="note" id="_101a6bdd" time=""><cell-source>$`not_variable1`</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

<message type="code" id="_af27503a" time=""><cell-source>‘$`not_variable2`’</cell-source>
<output>

aaa
</output>

</message>

<details>

- data: `None`

</details>

**Part** (`text`)

hi hi $`y`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 3======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

Hello! I can see the variable `y`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 4======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

<message type="note" id="_264125b3" time=""><cell-source>note\>\_264125b3</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

<message type="note" id="_0f5f4c5e" time=""><cell-source>note\>\_0f5f4c5e</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

<message type="code" id="_904dc672" time=""><cell-source>code\>\_904dc672</cell-source>
<output>

333
</output>

</message>

<details>

- data: `None`

</details>

**Part** (`text`)

prompt\>\_7140d653

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 5======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

<output result="pending" reason="incomplete"/>

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 6======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

$`x`, $`y`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 7======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

Hello! I can see the variables `x` and `y`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 8======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

<message type="note" id="_60a6c3e1" time=""><cell-source>below we insert
an image</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

<message type="code" id="_04cb4577" time=""><cell-source>img=..some code
to read image bytes</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

Please tell me what you see in $`img`

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 9======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

I can see…

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 10======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `user`

<contents>

**Part** (`text`)

<message type="note" id="_5444c426" time=""><cell-source>lets add an
image note</cell-source></message>

<details>

- data: `None`

</details>

**Part** (`text`)

Please tell me what you see in the 2 images in the above msg

<details>

- data: `None`

</details>

</contents>

</div>

<div class="prose" markdown="1">

**======turn: 11======**

</div>

<div class="prose" markdown="1">

**Msg**

- role: `assistant`

<contents>

**Part** (`text`)

i see two puppies!

<details>

- data: `None`

</details>

</contents>

</div>

``` python
p
```

    ['Whats variable $`y`?']

``` python
test_eq(len(hist),12)  # `raw` has 6 prompts which means 12 user+ai turns
```

``` python
hist[6]
```

    ['$`x`, $`y`']

``` python
# the last prompt is marked `last`, but core rendering is identical: bare content
test_eq(p, [raw[-1].content])
test_eq(hist[6], [raw[8].content])
```

A raw message carrying a `rec_kind` tag in its meta is session
bookkeeping some codec kept for reversibility (e.g. `llmsurgery.ant`’s
system records), not conversation content, so history projections skip
it:

``` python
tr = [mk('note', 'context'), mk('prompt', 'q', output='r')]
h1 = dlg2hist(tr, aim_info)
trd = Dialog('trd'); trd.messages = list(tr)
test_eq(dlg2hist(trd, aim_info), h1)  # a Dialog and its message list produce the same history
tr.insert(1, Message('', msg_type=sraw, meta=dict(rec_kind='system', rec=dict(type='system'))))
test_eq(dlg2hist(tr, aim_info), h1)  # tagged raws never leak into history
```

``` python
# test each user input for expected length
test_eq(len(hist[0]),1) # prompt w/o preceding msgs
test_eq(len(hist[2]),3) # prompt w preceding note and code
test_eq(len(hist[4]),4) # prompt w preceding notes (2) and code
test_eq(len(hist[6]),1) # prompt w/o preceding msgs
test_eq(len(hist[8]),3)  # [note, code, prompt]
test_eq(len(hist[10]),2)  # [note, prompt]
```

``` python
# Test each AI output for expected length
for i in (1,3,7,9,11): test(len(hist[i]),0,operator.gt)
test_eq(hist[5],'<output result="pending" reason="incomplete"/>')  # empty ai response
```

## Reply sub-dialogs

A prompt’s AI reply is one markdown string: prose interleaved with
tool-call details blocks.
[`reply2dlg`](https://AnswerDotAI.github.io/llmsurgery/hist.html#reply2dlg)
explodes that string into a
[`Dialog`](https://AnswerDotAI.github.io/llmsurgery/dialog.html#dialog)
of note and code messages, so a reply’s insides can be edited with the
ordinary message operations, and
[`dlg2reply`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2reply)
implodes the sub-dialog back into reply markdown. Parsing goes through
`fmt2hist`, so the projection normalizes a reply exactly where the
history path does: token-usage, think, and server-tool blocks are
dropped, and a reply ending in a call gains a trailing `.` note.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L286"
target="_blank" style="float:right; font-size:smaller">source</a>

### reply2dlg

``` python
def reply2dlg(
    pmsg
):
```

*Explode `pmsg`’s AI reply into a
[`Dialog`](https://AnswerDotAI.github.io/llmsurgery/dialog.html#dialog)
of note and code messages*

Each tool call becomes a code message whose source is the literal call
with `repr`’d argument values, so the source alone is the record: `ast`
recovers the exact call dict, and nothing else stores the tool name. The
result rides in the output list and the call id in `meta`. The fixture
builds a reply with `hist2fmt` from known `Msg`s, so the expected string
is constructed rather than scraped from a captured chat.

``` python
tu1 = Part(type=PartType.tool_use, text=None, data=dict(id='x1', name='python', arguments={'code':'1+1'}))
tr1 = Part(type=PartType.tool_result, text='2', data=dict(id='x1', name='python'))
rmsgs = [Msg(role='assistant', content=[Part(PartType.text, 'Check:'), tu1]), Msg(role='tool', content=[tr1]),
    Msg(role='assistant', content=[Part(PartType.text, 'It is 2.')])]
pm = Message('What is 1+1?', msg_type=sprompt, output=hist2fmt(rmsgs, mx=None))
Markdown(pm.ai_res)
```

``` python
sub = reply2dlg(pm)
sub
```

``` python
test_eq([m.msg_type for m in sub.messages], [snote, scode, snote])
test_eq(sub.messages[1].content, "python(code='1+1')")
test_eq(sub.messages[1].output, code_output('2'))
test_eq(sub.messages[1].meta['tool_id'], 'x1')
```

[`dlg2reply`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2reply)
renders each code message back into a details block through
`mk_tr_details` with truncation disabled (the summary line is
regenerated), passes note content through verbatim, and joins with blank
lines: the same bytes `hist2fmt` produces. For a fmt2hist-clean reply
the round trip is exact.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L317"
target="_blank" style="float:right; font-size:smaller">source</a>

### dlg2reply

``` python
def dlg2reply(
    sub
):
```

*Implode a sub-dialog of note and code messages back into reply
markdown*

``` python
test_eq(dlg2reply(sub), pm.ai_res)
```

Editing one argument in a code message changes exactly that argument in
the reparsed call, and the call id is untouched:

``` python
sub.messages[1].content = "python(code='1+2')"
tu = first(p for m in fmt2hist(dlg2reply(sub)) for p in m.content if p.type==PartType.tool_use)
test_eq(tu.data['arguments'], {'code':'1+2'})
test_eq(tu.data['id'], 'x1')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/hist.py#L324"
target="_blank" style="float:right; font-size:smaller">source</a>

### Message.tool_call

``` python
def tool_call():
```

*Parsed `dict(name=..., arguments=...)` when this code message’s source
is a literal tool call, else None*

`tool_call` is the read-only view of a code message’s call, for UIs and
review loops; unlike the render path it has no side effects (no id
generation) and returns None rather than raising for anything that isn’t
a literal call:

``` python
test_eq(sub.messages[1].tool_call, dict(name='python', arguments={'code':'1+2'}))
test_eq(sub.messages[0].tool_call, None)  # a note
test_eq(Message('x = 1', msg_type=scode).tool_call, None)  # not a call expression
```

A parallel volley renders as adjacent details blocks, and explodes to
sequential code messages in document order (the trailing `.` note is
`fmt2hist`’s guard against empty assistant text).
[`dlg2chat`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2chat)
sees equal histories before and after an explode/implode cycle:

``` python
tus = [Part(type=PartType.tool_use, text=None, data=dict(id=f'x{i}', name='simple_add', arguments=dict(a=i, b=1))) for i in (2,3,4)]
tres = [Part(type=PartType.tool_result, text=str(i+1), data=dict(id=f'x{i}', name='simple_add')) for i in (2,3,4)]
vmsgs = [Msg(role='assistant', content=[Part(PartType.text, 'Adding.'), *tus]), Msg(role='tool', content=tres)]
vdlg = Dialog('volley')
vpm = vdlg.mk_message('Add some numbers.', msg_type=sprompt, output=hist2fmt(vmsgs, mx=None))
vsub = reply2dlg(vpm)
test_eq([m.msg_type for m in vsub.messages], [snote, scode, scode, scode, snote])
```

``` python
v2 = Dialog('volley2')
v2.mk_message('Add some numbers.', msg_type=sprompt, output=dlg2reply(vsub))
test_eq(dlg2chat(v2), dlg2chat(vdlg))
```

New messages inserted into the sub-dialog appear in the reply; a new
call gets a generated id that doesn’t collide:

``` python
vsub.mk_message('One more:', msg_type=snote)
nc = vsub.mk_message('simple_add(a=5, b=1)', output=code_output('6'))
r3 = dlg2reply(vsub)
assert 'One more:' in r3
ids = [p.data['id'] for m in fmt2hist(r3) for p in m.content if p.type==PartType.tool_use]
test_eq(len(ids), len(set(ids)))
assert nc.meta['tool_id']
```

A reply with no tool calls round-trips as a single note. Tool names that
aren’t Python identifiers raise on explode, and a code message whose
source isn’t a keyword-only call raises on implode:

``` python
sub1 = reply2dlg(Message('hi', msg_type=sprompt, output='Just prose.'))
test_eq([m.msg_type for m in sub1.messages], [snote])
test_eq(dlg2reply(sub1), 'Just prose.')
```

``` python
badp = [Msg(role='assistant', content=[Part(type=PartType.tool_use, text=None, data=dict(id='b1', name='foo-bar', arguments={}))]),
    Msg(role='tool', content=[Part(type=PartType.tool_result, text='x', data=dict(id='b1', name='foo-bar'))])]
with expect_fail(ValueError, 'identifier'): reply2dlg(Message('p', msg_type=sprompt, output=hist2fmt(badp, mx=None)))
with expect_fail(ValueError, 'tool call'): dlg2reply(Dialog('b', [Message('1+1', msg_type=scode)]))
```
