Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aidialog/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
'aidialog.msg_parts.InputFile': ('msg_parts.html#inputfile', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.InputImage': ('msg_parts.html#inputimage', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.InputVideo': ('msg_parts.html#inputvideo', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.MdStr': ('msg_parts.html#mdstr', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.Media': ('msg_parts.html#media', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.Media.__init__': ('msg_parts.html#media.__init__', 'aidialog/msg_parts.py'),
'aidialog.msg_parts.Media.ctext': ('msg_parts.html#media.ctext', 'aidialog/msg_parts.py'),
Expand Down
6 changes: 4 additions & 2 deletions aidialog/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ def _code_span(txt):
_code_args = {'py':'code', 'python':'code', 'bash':'cmd'}

def tool_md(d):
"Display markdown for one parsed `{.tool}` block: a folded details div labeled `func(params)→result`, code tools shown as code"
"Display markdown for one parsed `{.tool}` block: a folded details div labeled `func(params)→result`, code shown as code, the result fenced unless the block marks it `md`"
params = ', '.join(f"{k}={_fmt_param(v)}" for k,v in (d.get('args') or {}).items())
res = d.get('result')
tail = f"→{_fmt_param(res)}" if res not in (None, '') else ''
Expand All @@ -696,7 +696,9 @@ def tool_md(d):
if code is not None:
body = f"Code:\n{fenced(str(code), d.get('name'))}"
if str(res or '').strip(): body += f"\n\nOutput:\n\n{fenced(str(res))}"
else: body = fenced(dumps(d, indent=2, ensure_ascii=False), 'json')
else:
body = f"Args:\n{fenced(dumps(d.get('args') or {}, indent=2, ensure_ascii=False), 'json')}"
if str(res or '').strip(): body += f"\n\n{res}" if d.get('md') else f"\n\nOutput:\n\n{fenced(str(res))}"
return fenced(f"## {label}\n\n{body}", ' {.details .tool-usage-details}', ch=':')

def usage_md(d):
Expand Down
11 changes: 7 additions & 4 deletions aidialog/msg_parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
'msg2dict', 'dict2msg', 'ToolUse', 'ToolResult', 'display_list', 'Completion', 'mk_tool_res_msg', 'sys_text',
'part_txt', 'data_url', 'url_mime', 'MediaUrl', 'mk_content', 'parse_tools', 'strip_tools', 'conv_tools',
'extract_fence_call', 'mk_result_fence', 'split_fence_msgs', 'tool_text', 'fmt2hist', 'ToolResponse',
'StopResponse', 'FullResponse', 'trunc_str', 'mk_tr_details', 'hist2fmt', 'mk_msg', 'mk_msgs']
'StopResponse', 'FullResponse', 'MdStr', 'trunc_str', 'mk_tr_details', 'hist2fmt', 'mk_msg', 'mk_msgs']

# %% ../nbs/00_msg_parts.ipynb #a616b4f5
import base64, json, copy
Expand Down Expand Up @@ -46,7 +46,7 @@ def replace(self, **kw):

# %% ../nbs/00_msg_parts.ipynb #48496c08
class Text(Part, tag=PartType.text):
"Plain text content."
"Plain text content; `citations` lists the sources it rests on as `url_citation` dicts (`url`, `title`, optional `start_index`/`end_index`)"
def __init__(self, text=None, citations=None, **kw):
super().__init__(**kw)
store_attr('text,citations')
Expand Down Expand Up @@ -268,7 +268,7 @@ def mk_content(o):
return o

# %% ../nbs/00_msg_parts.ipynb #ee9c825e
tool_info = 'json {.tool}' # fence info string of a tool block: {id, name, args, result} (+server; `error` reserved)
tool_info = 'json {.tool}' # fence info string of a tool block: {id, name, args, result} (+server, md; `error` reserved)
usage_info = 'json {.usage}' # fence info string of a usage block: UsageStats fields

def parse_tools(s):
Expand Down Expand Up @@ -384,7 +384,8 @@ def _extract_tool_parts(d:dict):
"Build (tool_use_part, tool_result_part) from a parsed `{.tool}` block"
if not d or d.get('id') is None: return None
tu = ToolUse (id=d['id'], name=d['name'], arguments=d.get('args') or {}, server=d.get('server', False))
tr = ToolResult(id=d['id'], name=d['name'], text=tool_text(d.get('result')), server=d.get('server', False))
text = tool_text(d.get('result'))
tr = ToolResult(id=d['id'], name=d['name'], text=MdStr(text) if d.get('md') else text, server=d.get('server', False))
return tu, tr

# %% ../nbs/00_msg_parts.ipynb #916f8df0
Expand Down Expand Up @@ -428,6 +429,7 @@ def __hash__(self): return hash(str(self.content))
# %% ../nbs/00_msg_parts.ipynb #86265805
class StopResponse(str): pass
class FullResponse(str): pass
class MdStr(str): pass

# %% ../nbs/00_msg_parts.ipynb #4f105e4d
def trunc_str(s, mx=2000, skip=10, replace="TRUNCATED"):
Expand Down Expand Up @@ -470,6 +472,7 @@ def mk_tr_details(tr, mx=2000):
args = {k:trunc_str(v, mx=None if mx is None else mx*5) if isinstance(v, str) else v for k,v in tr.arguments.items()}
res = dict(id=tr.id, name=tr.name, args=args, result=trunc_str(tool_text(tr.text), mx=mx))
if tr.server: res['server'] = True
if isinstance(tr.text, MdStr): res['md'] = True
return "\n\n" + fenced(dumps(res, indent=2, ensure_ascii=False), tool_info) + "\n\n"

# %% ../nbs/00_msg_parts.ipynb #70d1e8c3
Expand Down
19 changes: 11 additions & 8 deletions nbs/00_msg_parts.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
"source": [
"#| export\n",
"class Text(Part, tag=PartType.text):\n",
" \"Plain text content.\"\n",
" \"Plain text content; `citations` lists the sources it rests on as `url_citation` dicts (`url`, `title`, optional `start_index`/`end_index`)\"\n",
" def __init__(self, text=None, citations=None, **kw):\n",
" super().__init__(**kw)\n",
" store_attr('text,citations')\n",
Expand Down Expand Up @@ -1066,7 +1066,7 @@
"outputs": [],
"source": [
"#| export\n",
"tool_info = 'json {.tool}' # fence info string of a tool block: {id, name, args, result} (+server; `error` reserved)\n",
"tool_info = 'json {.tool}' # fence info string of a tool block: {id, name, args, result} (+server, md; `error` reserved)\n",
"usage_info = 'json {.usage}' # fence info string of a usage block: UsageStats fields\n",
"\n",
"def parse_tools(s):\n",
Expand Down Expand Up @@ -1635,7 +1635,8 @@
" \"Build (tool_use_part, tool_result_part) from a parsed `{.tool}` block\"\n",
" if not d or d.get('id') is None: return None\n",
" tu = ToolUse (id=d['id'], name=d['name'], arguments=d.get('args') or {}, server=d.get('server', False))\n",
" tr = ToolResult(id=d['id'], name=d['name'], text=tool_text(d.get('result')), server=d.get('server', False))\n",
" text = tool_text(d.get('result'))\n",
" tr = ToolResult(id=d['id'], name=d['name'], text=MdStr(text) if d.get('md') else text, server=d.get('server', False))\n",
" return tu, tr"
]
},
Expand Down Expand Up @@ -1774,7 +1775,7 @@
"id": "6a1e64c0",
"metadata": {},
"source": [
"`StopResponse` and `FullResponse` are `str` subclasses that mark a string's handling downstream: a `StopResponse` tool result ends a tool loop, and a `FullResponse` must never be truncated. `_trunc_str` honors the latter (along with fastcore's `Safe` and `PrettyString`, checked by class name so no import is needed), and the `𝍁...𝍁` marker is the same contract for strings that crossed a serialization boundary:"
"`StopResponse`, `FullResponse`, and `MdStr` are `str` subclasses that mark a string's handling downstream: a `StopResponse` tool result ends a tool loop, a `FullResponse` must never be truncated, and an `MdStr` is markdown a renderer may show as such rather than fenced (a tool that returns one has opted in, since untrusted text rendered as markdown can mislead). `_trunc_str` honors `FullResponse` (along with fastcore's `Safe` and `PrettyString`, checked by class name so no import is needed), and the `𝍁...𝍁` marker is the same contract for strings that crossed a serialization boundary:\n"
]
},
{
Expand All @@ -1786,7 +1787,8 @@
"source": [
"#| export\n",
"class StopResponse(str): pass\n",
"class FullResponse(str): pass"
"class FullResponse(str): pass\n",
"class MdStr(str): pass"
]
},
{
Expand Down Expand Up @@ -1876,6 +1878,7 @@
" args = {k:trunc_str(v, mx=None if mx is None else mx*5) if isinstance(v, str) else v for k,v in tr.arguments.items()}\n",
" res = dict(id=tr.id, name=tr.name, args=args, result=trunc_str(tool_text(tr.text), mx=mx))\n",
" if tr.server: res['server'] = True\n",
" if isinstance(tr.text, MdStr): res['md'] = True\n",
" return \"\\n\\n\" + fenced(dumps(res, indent=2, ensure_ascii=False), tool_info) + \"\\n\\n\""
]
},
Expand Down Expand Up @@ -2291,7 +2294,7 @@
"id": "bf492d17",
"metadata": {},
"source": [
"A server call, one the provider ran itself, has no result message of its own, so it renders as a completed block, with the call's `text` as the result when the provider gave one. Re-parsing gives a call and result pair with `server` kept, so a stored conversation replays the call as an ordinary tool call, and nothing tries to run it again:"
"A server call, one the provider ran itself, has no result message of its own, so it renders as a completed block, with the call's `text` as the result when the provider gave one. Re-parsing gives a call and result pair with `server` kept, so a stored conversation replays the call as an ordinary tool call, and nothing tries to run it again. A result that is markdown by construction, an `MdStr`, is marked `md` on the block and comes back as one:"
]
},
{
Expand All @@ -2301,12 +2304,12 @@
"metadata": {},
"outputs": [],
"source": [
"srv = Msg('assistant', [Text('Let me check.'), ToolUse(id='s1', name='web_search', arguments={'query': 'otters'}, server=True, text='Otter: https://example.com/otter')])\n",
"srv = Msg('assistant', [Text('Let me check.'), ToolUse(id='s1', name='web_search', arguments={'query': 'otters'}, server=True, text=MdStr('Otter: https://example.com/otter'))])\n",
"s = hist2fmt([srv])\n",
"h3 = fmt2hist(s)\n",
"test_eq([m.role for m in h3[:2]], ['assistant', 'tool'])\n",
"test_eq((h3[0].content[1].server, h3[1].content[0].server), (True, True))\n",
"test_eq(h3[1].content[0].text, 'Otter: https://example.com/otter')\n",
"test_eq((h3[1].content[0].text, type(h3[1].content[0].text)), ('Otter: https://example.com/otter', MdStr))\n",
"test_eq(hist2fmt(h3[:2]), s)\n",
"Markdown(s)"
]
Expand Down
34 changes: 19 additions & 15 deletions nbs/01_dialog.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,10 @@
{
"data": {
"text/markdown": [
"59c74047:n:in ⇒ out(3)"
"64ace59d:n:in ⇒ out(3)"
],
"text/plain": [
"Message(id='59c74047', content='in', output='out', msg_type='note')"
"Message(id='64ace59d', content='in', output='out', msg_type='note')"
]
},
"execution_count": null,
Expand All @@ -584,9 +584,9 @@
{
"data": {
"text/plain": [
"ac71fcba:p:q\n",
"4fdd721b:p:q\n",
"> Edited reply\n",
"c398e29c:c:x=1 ⇒ out(601)"
"95575dfa:c:x=1 ⇒ out(601)"
]
},
"execution_count": null,
Expand Down Expand Up @@ -771,10 +771,10 @@
{
"data": {
"text/markdown": [
"76fab414:c:8*8 ⇒ out(103)"
"d6b8bf64:c:8*8 ⇒ out(103)"
],
"text/plain": [
"Message(id='76fab414', content='8*8', output=[{'output_type': 'execute_result', 'metadata': {}, 'data': {'text/plain': '64'}, 'execution_count': 1}], msg_type='code')"
"Message(id='d6b8bf64', content='8*8', output=[{'output_type': 'execute_result', 'metadata': {}, 'data': {'text/plain': '64'}, 'execution_count': 1}], msg_type='code')"
]
},
"execution_count": null,
Expand Down Expand Up @@ -2325,7 +2325,7 @@
"id": "7e642860",
"metadata": {},
"source": [
"Solveit replies carry tool calls and usage stats as fenced JSON wire blocks (fastllm's format: a ```` ```json {.tool} ```` fence holding `{id, name, args, result}`, and ```` ```json {.usage} ```` holding usage fields). `fmt_tools` rewrites those blocks into folded `::: details` display markdown with a code-span label derived from the data, for renderers (solveit, viewmd) whose html exporter lowers `div.details` to a native `<details>` element. It is a pure formatter: parsing stays with the emitting library, and unparseable blocks are left untouched."
"Solveit replies carry tool calls and usage stats as fenced JSON wire blocks (fastllm's format: a ```` ```json {.tool} ```` fence holding `{id, name, args, result}`, and ```` ```json {.usage} ```` holding usage fields). `fmt_tools` rewrites those blocks into folded `::: details` display markdown with a code-span label derived from the data, for renderers (solveit, viewmd) whose html exporter lowers `div.details` to a native `<details>` element. A code tool shows its code and its output in fences. Any other tool shows its args as JSON and its result fenced, unless the block carries `\"md\": true`, the mark a tool result that is markdown by construction (a page fetch, a search) travels with as an `MdStr`; only then is it rendered as markdown. It is a pure formatter: parsing stays with the emitting library, and unparseable blocks are left untouched.\n"
]
},
{
Expand Down Expand Up @@ -2354,7 +2354,7 @@
"_code_args = {'py':'code', 'python':'code', 'bash':'cmd'}\n",
"\n",
"def tool_md(d):\n",
" \"Display markdown for one parsed `{.tool}` block: a folded details div labeled `func(params)→result`, code tools shown as code\"\n",
" \"Display markdown for one parsed `{.tool}` block: a folded details div labeled `func(params)→result`, code shown as code, the result fenced unless the block marks it `md`\"\n",
" params = ', '.join(f\"{k}={_fmt_param(v)}\" for k,v in (d.get('args') or {}).items())\n",
" res = d.get('result')\n",
" tail = f\"→{_fmt_param(res)}\" if res not in (None, '') else ''\n",
Expand All @@ -2363,7 +2363,9 @@
" if code is not None:\n",
" body = f\"Code:\\n{fenced(str(code), d.get('name'))}\"\n",
" if str(res or '').strip(): body += f\"\\n\\nOutput:\\n\\n{fenced(str(res))}\"\n",
" else: body = fenced(dumps(d, indent=2, ensure_ascii=False), 'json')\n",
" else:\n",
" body = f\"Args:\\n{fenced(dumps(d.get('args') or {}, indent=2, ensure_ascii=False), 'json')}\"\n",
" if str(res or '').strip(): body += f\"\\n\\n{res}\" if d.get('md') else f\"\\n\\nOutput:\\n\\n{fenced(str(res))}\"\n",
" return fenced(f\"## {label}\\n\\n{body}\", ' {.details .tool-usage-details}', ch=':')\n",
"\n",
"def usage_md(d):\n",
Expand Down Expand Up @@ -2403,7 +2405,9 @@
"assert 'Code:\\n```py\\n1+1\\n```' in disp # code tools show code + output fences\n",
"assert 'Output:' in disp\n",
"other = fmt_tools('```json {.tool}\\n{\"id\": \"t3\", \"name\": \"web\", \"args\": {\"url\": \"x\"}, \"result\": \"ok\"}\\n```')\n",
"assert '```json' in other # non-code tools show the JSON\n",
"assert '```json\\n{\\n \"url\": \"x\"\\n}\\n```' in other and 'Output:\\n\\n```\\nok\\n```' in other # other tools show their args as JSON and the result fenced\n",
"md = fmt_tools('```json {.tool}\\n{\"id\": \"t5\", \"name\": \"read_url\", \"args\": {\"url\": \"x\"}, \"result\": \"# Title\\\\n\\\\nBody\", \"md\": true}\\n```')\n",
"assert '\\n\\n# Title\\n\\nBody\\n' in md and '```\\n# Title' not in md # a result marked `md` renders as markdown\n",
"test_eq(fmt_tools(disp), disp) # idempotent: display form has no wire blocks\n",
"uwire = '```json {.usage}\\n{\"model\": \"m\", \"total_tokens\": 150, \"cost\": 0.0123}\\n```'\n",
"udisp = fmt_tools(uwire)\n",
Expand Down Expand Up @@ -2798,7 +2802,7 @@
{
"data": {
"text/plain": [
"c2599acf: ok\n",
"d9705aed: ok\n",
"run: 1 msg ok"
]
},
Expand Down Expand Up @@ -2833,7 +2837,7 @@
{
"data": {
"text/plain": [
"c2599acf: NameError: name 'y' is not defined\n",
"d9705aed: NameError: name 'y' is not defined\n",
"run: 0 msgs ok, 1 failed"
]
},
Expand Down Expand Up @@ -2866,7 +2870,7 @@
"data": {
"text/plain": [
"a9a03f99: ok\n",
"009b926b: ZeroDivisionError: division by zero\n",
"197f3ee9: ZeroDivisionError: division by zero\n",
"run: 1 msg ok, 1 failed"
]
},
Expand Down Expand Up @@ -2901,9 +2905,9 @@
"data": {
"text/plain": [
"a9a03f99: ok\n",
"009b926b: ZeroDivisionError: division by zero\n",
"197f3ee9: ZeroDivisionError: division by zero\n",
"9ed1a7ce: ok\n",
"c2599acf: ok\n",
"d9705aed: ok\n",
"run: 3 msgs ok, 1 failed"
]
},
Expand Down