From 451134df773fd32ecb0192e08249eb2854da36fe Mon Sep 17 00:00:00 2001 From: Tommy Date: Wed, 15 Oct 2025 18:04:02 +0100 Subject: [PATCH] reconstruct tool calls --- cachy.jsonl | 5 + lisette/_modidx.py | 2 + lisette/core.py | 75 ++++--- nbs/00_core.ipynb | 476 ++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 505 insertions(+), 53 deletions(-) diff --git a/cachy.jsonl b/cachy.jsonl index 4926d71..641286d 100644 --- a/cachy.jsonl +++ b/cachy.jsonl @@ -330,3 +330,8 @@ {"key": "5c8485a6", "response": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01FAdRVTQeQTovybY7EVW2Fr\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":4,\"cache_creation_input_tokens\":14,\"cache_read_input_tokens\":1622,\"cache_creation\":{\"ephemeral_5m_input_tokens\":14,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":9,\"service_tier\":\"standard\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Goodbye! Feel free to come back if\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" you have any questions.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":4,\"cache_creation_input_tokens\":14,\"cache_read_input_tokens\":1622,\"output_tokens\":17}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"} {"key": "fc5ede02", "response": "{\"model\":\"claude-sonnet-4-20250514\",\"id\":\"msg_01M2HV41ZonZ1AAJg5FUy6zr\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"\\n\\nThe result of 47 + 23 is 70.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":573,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":18,\"service_tier\":\"standard\"}}"} {"key": "df4ef045", "response": "{\"model\":\"claude-sonnet-4-20250514\",\"id\":\"msg_0139iCPpDfH1itSAtoBGNV4V\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"\\n\\nThe answer is **129**.\\n\\nI calculated this by first adding 47 + 23 = 70, then adding 70 + 59 = 129.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":702,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":41,\"service_tier\":\"standard\"}}"} +{"key": "d4589fc2", "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_019GSku9pTraoyhUZZxkDbws\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01MCSt43tUWZKqaoFXbn7krH\",\"name\":\"simple_add\",\"input\":{\"a\":5,\"b\":3}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":618,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":70,\"service_tier\":\"standard\"}}"} +{"key": "20a4c712", "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01ErxCc3aGgypa4ndgTwMuMf\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"\\n\\nThe result of 5 + 3 is **8**.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":742,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":18,\"service_tier\":\"standard\"}}"} +{"key": "71b7f751", "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01YPXAnNErrUzxUZpLxqP7rT\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Here's a joke based on the number 8:\\n\\nWhy was 6 afraid of 7?\\n\\nBecause 7 8 (ate) 9!\\n\\nBut since we got 8 as our answer, here's another one:\\n\\nWhat did the number 8 say when it looked in the mirror?\\n\\n\\\"Nice belt!\\\"\\n\\n(Because 8 looks like it's wearing a belt around its middle! \ud83d\ude04)\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":774,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":97,\"service_tier\":\"standard\"}}"} +{"key": "8ff05b20", "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01UkSZXczvtptdBLJemBkwnv\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"# Image Description\\n\\nThis adorable image shows a **Cavalier King Charles Spaniel puppy** with the classic Blenheim coloring (chestnut brown and white markings). \\n\\n## Key features visible:\\n- **Expressive brown eyes** looking directly at the camera\\n- **Soft, fluffy ears** with rich brown fur\\n- **White blaze** down the center of the face\\n- **White chest and paws**\\n- The puppy is lying on **green grass**\\n- **Purple flowers** (appear to be asters or similar) in the background\\n- Warm, soft lighting creating a charming portrait effect\\n\\nThe puppy has that irresistibly sweet, gentle expression that Cavalier King Charles Spaniels are famous for. This looks like a professional or carefully composed photograph, possibly for a breeder, pet portrait, or greeting card.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":105,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":195,\"service_tier\":\"standard\"}}"} +{"key": "130a52f1", "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01NsVrovfY7JrTr5dPygRJhb\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\" D A C T E D\\n\\nI don't actually know your name - you haven't told me what it is yet! If you'd like me to spell your name, please let me know what it is first.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":16,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":47,\"service_tier\":\"standard\"}}"} diff --git a/lisette/_modidx.py b/lisette/_modidx.py index e961fb0..b874db6 100644 --- a/lisette/_modidx.py +++ b/lisette/_modidx.py @@ -16,8 +16,10 @@ 'lisette.core.Chat.print_hist': ('core.html#chat.print_hist', 'lisette/core.py'), 'lisette.core._add_cache_control': ('core.html#_add_cache_control', 'lisette/core.py'), 'lisette.core._alite_call_func': ('core.html#_alite_call_func', 'lisette/core.py'), + 'lisette.core._build_tool_hist': ('core.html#_build_tool_hist', 'lisette/core.py'), 'lisette.core._bytes2content': ('core.html#_bytes2content', 'lisette/core.py'), 'lisette.core._clean_str': ('core.html#_clean_str', 'lisette/core.py'), + 'lisette.core._details_extract': ('core.html#_details_extract', 'lisette/core.py'), 'lisette.core._has_cache': ('core.html#_has_cache', 'lisette/core.py'), 'lisette.core._has_search': ('core.html#_has_search', 'lisette/core.py'), 'lisette.core._lite_call_func': ('core.html#_lite_call_func', 'lisette/core.py'), diff --git a/lisette/core.py b/lisette/core.py index 13ba936..fd9b0a5 100644 --- a/lisette/core.py +++ b/lisette/core.py @@ -3,8 +3,8 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb. # %% auto 0 -__all__ = ['sonn45', 'effort', 'patch_litellm', 'mk_msg', 'mk_msgs', 'stream_with_complete', 'lite_mk_func', 'cite_footnote', - 'cite_footnotes', 'Chat', 'random_tool_id', 'mk_tc', 'mk_tc_req', 'mk_tc_result', 'mk_tc_results', +__all__ = ['sonn45', 'effort', 'patch_litellm', 'mk_msg', 'stream_with_complete', 'lite_mk_func', 'cite_footnote', + 'cite_footnotes', 'Chat', 'random_tool_id', 'mk_tc', 'mk_tc_req', 'mk_tc_result', 'mk_tc_results', 'mk_msgs', 'astream_with_complete', 'AsyncChat', 'aformat_stream', 'adisplay_stream'] # %% ../nbs/00_core.ipynb @@ -117,23 +117,6 @@ def mk_msg(content, # Content: str, bytes (image), list of mixed content, o msg = {"role": role, "content": c} return _add_cache_control(msg, ttl=ttl) if cache else msg -# %% ../nbs/00_core.ipynb -def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields) - cache=False, # Enable Anthropic caching - ttl=None, # Cache TTL: '5m' (default) or '1h' - ): - "Create a list of LiteLLM compatible messages." - if not msgs: return [] - if not isinstance(msgs, list): msgs = [msgs] - res,role = [],'user' - for m in msgs: - res.append(msg:=mk_msg(m, role=role)) - role = 'assistant' if msg['role'] in ('user','function', 'tool') else 'user' - if cache: - res[-1] = _add_cache_control(res[-1], ttl) - res[-2] = _add_cache_control(res[-2], ttl) - return res - # %% ../nbs/00_core.ipynb def stream_with_complete(gen, postproc=noop): "Extend streaming response chunks with the complete response" @@ -269,11 +252,9 @@ def random_tool_id(): return f'toolu_{random_part}' # %% ../nbs/00_core.ipynb -def mk_tc(func, idx=1, **kwargs): - args = json.dumps(kwargs) - if callable(func): func = func.__name__ - id = random_tool_id() - return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': id, 'type': 'function'} +def mk_tc(func, args, tcid=None, idx=1): + if not tcid: tcid = random_tool_id() + return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': tcid, 'type': 'function'} # %% ../nbs/00_core.ipynb def mk_tc_req(content, tcs): @@ -287,6 +268,48 @@ def mk_tc_result(tc, result): return {'tool_call_id': tc['id'], 'role': 'tool', # %% ../nbs/00_core.ipynb def mk_tc_results(tcq, results): return [mk_tc_result(a,b) for a,b in zip(tcq.tool_calls, results)] +# %% ../nbs/00_core.ipynb +def _details_extract(x): + "Extract fn, args, tool_call_id, result from
" + m = re.search(r'(.*?)
', x, re.DOTALL) + tc, tcid, res = re.findall(r'-\s*`([^`]+)`', m.group(1)) + fn, args = re.search(r'(\w+)\((.*?)\)', tc).groups() + return fn, args, res, tcid + +# %% ../nbs/00_core.ipynb +def _build_tool_hist(msg): + "Build original tool call messages from the tool call summary." + hist = [] + parts = re.split(r'(.*?)', msg, flags=re.DOTALL) + for islast, (i,o) in loop_last(enumerate(parts)): + if "
" not in o: + if islast: hist.append(o.strip()) + continue + fn, args, res, tcid = _details_extract(o) + tc = mk_tc(fn, args, tcid) + tcq = mk_tc_req(parts[i-1].strip() if i>0 else "", [tc]) + tcr = first(mk_tc_results(tcq, [res])) + hist.extend([tcq, tcr]) + return hist + +# %% ../nbs/00_core.ipynb +def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields) + cache=False, # Enable Anthropic caching + ttl=None, # Cache TTL: '5m' (default) or '1h' + ): + "Create a list of LiteLLM compatible messages." + if not msgs: return [] + if not isinstance(msgs, list): msgs = [msgs] + res,role = [],'user' + msgs = L(msgs).map(lambda m: _build_tool_hist(m) if "
" in m else [m]).concat() + for m in msgs: + res.append(msg:=mk_msg(m, role=role)) + role = 'assistant' if msg['role'] in ('user','function', 'tool') else 'user' + if cache: + res[-1] = _add_cache_control(res[-1], ttl) + res[-2] = _add_cache_control(res[-2], ttl) + return res + # %% ../nbs/00_core.ipynb async def _alite_call_func(tc, ns, raise_on_err=True): try: fargs = json.loads(tc.function.arguments) @@ -385,9 +408,9 @@ async def aformat_stream(rs, include_usage=False): if include_usage: yield f"\nUsage: {o.usage}" if (c := getattr(o.choices[0].message, 'tool_calls', None)): fn = first(c).function - yield f"\n
\n\n `{fn.name}({_trunc_str(fn.arguments)})`\n" + yield f"\n
\n\n - `{fn.name}({_trunc_str(fn.arguments, replace='')})`\n" elif isinstance(o, dict) and 'tool_call_id' in o: - yield f" - `{_trunc_str(_clean_str(o.get('content')))}`\n\n
\n\n" + yield f" - `{o['tool_call_id']}`\n\n - `{_trunc_str(_clean_str(o.get('content')),replace='')}`\n\n
\n\n" # %% ../nbs/00_core.ipynb async def adisplay_stream(rs): diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index b7bba40..728f884 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -672,10 +672,9 @@ "metadata": {}, "outputs": [], "source": [ - "#| export\n", - "def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields)\n", - " cache=False, # Enable Anthropic caching\n", - " ttl=None, # Cache TTL: '5m' (default) or '1h'\n", + "def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields)\n", + " cache=False, # Enable Anthropic caching\n", + " ttl=None, # Cache TTL: '5m' (default) or '1h'\n", " ):\n", " \"Create a list of LiteLLM compatible messages.\"\n", " if not msgs: return []\n", @@ -2042,11 +2041,9 @@ "outputs": [], "source": [ "#| export\n", - "def mk_tc(func, idx=1, **kwargs):\n", - " args = json.dumps(kwargs)\n", - " if callable(func): func = func.__name__\n", - " id = random_tool_id()\n", - " return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': id, 'type': 'function'}" + "def mk_tc(func, args, tcid=None, idx=1):\n", + " if not tcid: tcid = random_tool_id()\n", + " return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': tcid, 'type': 'function'}" ] }, { @@ -2070,7 +2067,7 @@ } ], "source": [ - "tc = mk_tc(simple_add, a=5, b=7)\n", + "tc = mk_tc(simple_add.__name__, json.dumps(dict(a=5, b=7)))\n", "tc" ] }, @@ -2488,7 +2485,10 @@ "metadata": {}, "outputs": [], "source": [ - "tcs = [mk_tc(simple_add, a=5, b=7), mk_tc(simple_add, a=6, b=7)]" + "tcs = [\n", + " mk_tc(simple_add.__name__, json.dumps({\"a\": 5, \"b\": 7})), \n", + " mk_tc(simple_add.__name__, json.dumps({\"a\": 6, \"b\": 7})), \n", + "]" ] }, { @@ -2593,6 +2593,403 @@ "c.print_hist()" ] }, + { + "cell_type": "markdown", + "id": "9720f80f", + "metadata": {}, + "source": [ + "**Reconstructing Tool Call Messages**\n", + "\n", + "A rendered tool call has the following structure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3465ea4", + "metadata": {}, + "outputs": [], + "source": [ + "tc_rend = \"\"\"\n", + "I'll help you with that! Let me use the tools to perform these calculations.\n", + "
\n", + "\n", + " - `addy({\"a\": 5, \"b\": 7})`\n", + " - `12`\n", + " - `toolu_018LkjHQE9peK85MjXPcjaSm`\n", + "\n", + "
\n", + "\n", + "Now I'll subtract 3 from that result:\n", + "
\n", + "\n", + " - `subby({\"a\": 12, \"b\": 3})`\n", + " - `9`\n", + " - `toolu_01CkEqtBtBjBvrNBJ9ATaqRt`\n", + "\n", + "
\n", + "\n", + "Perfect! First, adding 5 and 7 gives us 12, and then subtracting 3 from 12 gives us the final result of **9**.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "d864e1dd", + "metadata": {}, + "source": [ + "*Note: The function arguments and outputs are truncated to minimise token usage.*" + ] + }, + { + "cell_type": "markdown", + "id": "22985193", + "metadata": {}, + "source": [ + "To prevent tool call hallucinations we want to reconstruct the original messages.\n", + "\n", + "```python\n", + "[\n", + "\tMessage(\n", + "\t\tcontent=\"I'll help you with that! Let me use the tools to perform these calculations.\", role='assistant', \n", + " function_call=None, provider_specific_fields=None\n", + " tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 5, \"b\": 7}', 'name': 'addy'}, \n", + "\t\t'id': 'toolu_018LkjHQE9peK85MjXPcjaSm', 'type': 'function'}]\n", + " ),\n", + "\t{'tool_call_id': 'toolu_018LkjHQE9peK85MjXPcjaSm', 'role': 'tool', 'name': 'addy', 'content': '12'},\n", + " \tMessage(\n", + "\t\tcontent=\"Now I'll subtract 3 from that result:\", role='assistant', \n", + " function_call=None, provider_specific_fields=None\n", + " tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 12, \"b\": 3}', 'name': 'subby'}, \n", + "\t\t'id': 'toolu_01CkEqtBtBjBvrNBJ9ATaqRt', 'type': 'function'}]\n", + " ),\n", + " \t{'tool_call_id': 'toolu_01CkEqtBtBjBvrNBJ9ATaqRt', 'role': 'tool', 'name': 'subby', 'content': '9'},\n", + "\t{\"role\": \"assistant\", \"content\": \"Perfect! First, adding 5 and 7 gives us 12, and then subtracting 3 from 12 gives us the final result of **9**\"}.\n", + "]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "99e61fb6", + "metadata": {}, + "source": [ + "The first thing we need to do is split `tc_rend` into separate pieces." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "771ff927", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "I'll help you with that! Let me use the tools to \n", + "
\n", + "\n", + " - `addy({\"a\n", + "\n", + "\n", + "Now I'll subtract 3 from that result:\n", + "\n", + "
\n", + "\n", + " - `subby({\"\n", + "\n", + "\n", + "Perfect! First, adding 5 and 7 gives us 12, and \n" + ] + } + ], + "source": [ + "parts = [o for o in re.split(r'(.*?
)', tc_rend, flags=re.DOTALL)]\n", + "for p in parts: print(p[:50])" + ] + }, + { + "cell_type": "markdown", + "id": "cb27773b", + "metadata": {}, + "source": [ + "Next, we need to extract the function, args, tool_call_id and result from the details element." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a5236f", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _details_extract(x):\n", + " \"Extract fn, args, tool_call_id, result from
\"\n", + " m = re.search(r'(.*?)
', x, re.DOTALL)\n", + " tc, tcid, res = re.findall(r'-\\s*`([^`]+)`', m.group(1))\n", + " fn, args = re.search(r'(\\w+)\\((.*?)\\)', tc).groups()\n", + " return fn, args, res, tcid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af34a9dc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('addy', '{\"a\": 5, \"b\": 7}', 'toolu_018LkjHQE9peK85MjXPcjaSm', '12')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_details_extract(parts[1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de28bb38", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _build_tool_hist(msg):\n", + " \"Build original tool call messages from the tool call summary.\"\n", + " hist = []\n", + " parts = re.split(r'(.*?
)', msg, flags=re.DOTALL)\n", + " for islast, (i,o) in loop_last(enumerate(parts)):\n", + " if \"
\" not in o:\n", + " if islast: hist.append(o.strip())\n", + " continue\n", + " fn, args, res, tcid = _details_extract(o)\n", + " tc = mk_tc(fn, args, tcid)\n", + " tcq = mk_tc_req(parts[i-1].strip() if i>0 else \"\", [tc])\n", + " tcr = first(mk_tc_results(tcq, [res]))\n", + " hist.extend([tcq, tcr])\n", + " return hist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143ebce5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Message(content=\"I'll help you with that! Let me use the tools to perform these calculations.\", role='assistant', tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 5, \"b\": 7}', 'name': 'addy'}, 'id': '12', 'type': 'function'}], function_call=None, provider_specific_fields=None),\n", + " {'tool_call_id': '12',\n", + " 'role': 'tool',\n", + " 'name': 'addy',\n", + " 'content': 'toolu_018LkjHQE9peK85MjXPcjaSm'},\n", + " Message(content=\"Now I'll subtract 3 from that result:\", role='assistant', tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 12, \"b\": 3}', 'name': 'subby'}, 'id': '9', 'type': 'function'}], function_call=None, provider_specific_fields=None),\n", + " {'tool_call_id': '9',\n", + " 'role': 'tool',\n", + " 'name': 'subby',\n", + " 'content': 'toolu_01CkEqtBtBjBvrNBJ9ATaqRt'},\n", + " 'Perfect! First, adding 5 and 7 gives us 12, and then subtracting 3 from 12 gives us the final result of **9**.']" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_build_tool_hist(tc_rend)" + ] + }, + { + "cell_type": "markdown", + "id": "001f2188", + "metadata": {}, + "source": [ + "It would be handy if our `Chat` automatically rebuilt our tool calls when creating our history. One way we can do this is by updating `mk_msgs`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f32d397a", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields)\n", + " cache=False, # Enable Anthropic caching\n", + " ttl=None, # Cache TTL: '5m' (default) or '1h'\n", + " ):\n", + " \"Create a list of LiteLLM compatible messages.\"\n", + " if not msgs: return []\n", + " if not isinstance(msgs, list): msgs = [msgs]\n", + " res,role = [],'user'\n", + " msgs = L(msgs).map(lambda m: _build_tool_hist(m) if \"
\" in m else [m]).concat()\n", + " for m in msgs:\n", + " res.append(msg:=mk_msg(m, role=role))\n", + " role = 'assistant' if msg['role'] in ('user','function', 'tool') else 'user'\n", + " if cache:\n", + " res[-1] = _add_cache_control(res[-1], ttl)\n", + " res[-2] = _add_cache_control(res[-2], ttl)\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "135ab847", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Message(content=\"I'll help you with that! Let me use the tools to perform these calculations.\", role='assistant', tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 5, \"b\": 7}', 'name': 'addy'}, 'id': '12', 'type': 'function'}], function_call=None, provider_specific_fields=None),\n", + " {'tool_call_id': '12',\n", + " 'role': 'tool',\n", + " 'name': 'addy',\n", + " 'content': 'toolu_018LkjHQE9peK85MjXPcjaSm'},\n", + " Message(content=\"Now I'll subtract 3 from that result:\", role='assistant', tool_calls=[{'index': 1, 'function': {'arguments': '{\"a\": 12, \"b\": 3}', 'name': 'subby'}, 'id': '9', 'type': 'function'}], function_call=None, provider_specific_fields=None),\n", + " {'tool_call_id': '9',\n", + " 'role': 'tool',\n", + " 'name': 'subby',\n", + " 'content': 'toolu_01CkEqtBtBjBvrNBJ9ATaqRt'},\n", + " {'role': 'assistant',\n", + " 'content': 'Perfect! First, adding 5 and 7 gives us 12, and then subtracting 3 from 12 gives us the final result of **9**.'}]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mk_msgs(tc_rend)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e5b97b6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "\n", + "\n", + "The result of 5 + 3 is **8**.\n", + "\n", + "
\n", + "\n", + "- id: `chatcmpl-xxx`\n", + "- model: `claude-sonnet-4-5-20250929`\n", + "- finish_reason: `stop`\n", + "- usage: `Usage(completion_tokens=18, prompt_tokens=742, total_tokens=760, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", + "\n", + "
" + ], + "text/plain": [ + "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content='\\n\\nThe result of 5 + 3 is **8**.', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=18, prompt_tokens=742, total_tokens=760, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat = Chat(ms[1], tools=[simple_add])\n", + "res = chat(\"What's 5 + 3? Use the `simple_add` tool.\")\n", + "res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c84d6ef", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "Here's a joke based on the number 8:\n", + "\n", + "Why was 6 afraid of 7?\n", + "\n", + "Because 7 8 (ate) 9!\n", + "\n", + "But since we got 8 as our answer, here's another one:\n", + "\n", + "What did the number 8 say when it looked in the mirror?\n", + "\n", + "\"Nice belt!\"\n", + "\n", + "(Because 8 looks like it's wearing a belt around its middle! 😄)\n", + "\n", + "
\n", + "\n", + "- id: `chatcmpl-xxx`\n", + "- model: `claude-sonnet-4-5-20250929`\n", + "- finish_reason: `stop`\n", + "- usage: `Usage(completion_tokens=97, prompt_tokens=774, total_tokens=871, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", + "\n", + "
" + ], + "text/plain": [ + "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content='Here\\'s a joke based on the number 8:\\n\\nWhy was 6 afraid of 7?\\n\\nBecause 7 8 (ate) 9!\\n\\nBut since we got 8 as our answer, here\\'s another one:\\n\\nWhat did the number 8 say when it looked in the mirror?\\n\\n\"Nice belt!\"\\n\\n(Because 8 looks like it\\'s wearing a belt around its middle! 😄)', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=97, prompt_tokens=774, total_tokens=871, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = chat(\"Now, tell me a joke based on that result.\")\n", + "res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6a8bec1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'role': 'user', 'content': \"What's 5 + 3? Use the `simple_add` tool.\"},\n", + " Message(content=None, role='assistant', tool_calls=[{'index': 0, 'function': {'arguments': '{\"a\": 5, \"b\": 3}', 'name': 'simple_add'}, 'id': 'toolu_01MCSt43tUWZKqaoFXbn7krH', 'type': 'function'}], function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}),\n", + " {'tool_call_id': 'toolu_01MCSt43tUWZKqaoFXbn7krH',\n", + " 'role': 'tool',\n", + " 'name': 'simple_add',\n", + " 'content': '8'},\n", + " {'role': 'assistant',\n", + " 'content': 'You have no more tool uses. Please summarize your findings. If you did not complete your goal please tell the user what further work needs to be done so they can choose how best to proceed.'},\n", + " Message(content='\\n\\nThe result of 5 + 3 is **8**.', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}),\n", + " {'role': 'user', 'content': 'Now, tell me a joke based on that result.'},\n", + " Message(content='Here\\'s a joke based on the number 8:\\n\\nWhy was 6 afraid of 7?\\n\\nBecause 7 8 (ate) 9!\\n\\nBut since we got 8 as our answer, here\\'s another one:\\n\\nWhat did the number 8 say when it looked in the mirror?\\n\\n\"Nice belt!\"\\n\\n(Because 8 looks like it\\'s wearing a belt around its middle! 😄)', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None})]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat.hist" + ] + }, { "cell_type": "markdown", "id": "0678268a", @@ -2610,19 +3007,32 @@ { "data": { "text/markdown": [ - "This image shows an adorable Cavalier King Charles Spaniel puppy! The puppy has the classic brown (chestnut) and white coloring, with beautiful long, fluffy ears. It's lying on grass next to some purple flowers (they look like asters or similar flowers), and appears to be looking directly at the camera with those sweet, soulful eyes that the breed is known for. It's a really cute photo!\n", + "# Image Description\n", + "\n", + "This adorable image shows a **Cavalier King Charles Spaniel puppy** with the classic Blenheim coloring (chestnut brown and white markings). \n", + "\n", + "## Key features visible:\n", + "- **Expressive brown eyes** looking directly at the camera\n", + "- **Soft, fluffy ears** with rich brown fur\n", + "- **White blaze** down the center of the face\n", + "- **White chest and paws**\n", + "- The puppy is lying on **green grass**\n", + "- **Purple flowers** (appear to be asters or similar) in the background\n", + "- Warm, soft lighting creating a charming portrait effect\n", + "\n", + "The puppy has that irresistibly sweet, gentle expression that Cavalier King Charles Spaniels are famous for. This looks like a professional or carefully composed photograph, possibly for a breeder, pet portrait, or greeting card.\n", "\n", "
\n", "\n", "- id: `chatcmpl-xxx`\n", "- model: `claude-sonnet-4-5-20250929`\n", "- finish_reason: `stop`\n", - "- usage: `Usage(completion_tokens=96, prompt_tokens=168, total_tokens=264, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", + "- usage: `Usage(completion_tokens=195, prompt_tokens=105, total_tokens=300, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", "\n", "
" ], "text/plain": [ - "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content=\"This image shows an adorable Cavalier King Charles Spaniel puppy! The puppy has the classic brown (chestnut) and white coloring, with beautiful long, fluffy ears. It's lying on grass next to some purple flowers (they look like asters or similar flowers), and appears to be looking directly at the camera with those sweet, soulful eyes that the breed is known for. It's a really cute photo!\", role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=96, prompt_tokens=168, total_tokens=264, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" + "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content='# Image Description\\n\\nThis adorable image shows a **Cavalier King Charles Spaniel puppy** with the classic Blenheim coloring (chestnut brown and white markings). \\n\\n## Key features visible:\\n- **Expressive brown eyes** looking directly at the camera\\n- **Soft, fluffy ears** with rich brown fur\\n- **White blaze** down the center of the face\\n- **White chest and paws**\\n- The puppy is lying on **green grass**\\n- **Purple flowers** (appear to be asters or similar) in the background\\n- Warm, soft lighting creating a charming portrait effect\\n\\nThe puppy has that irresistibly sweet, gentle expression that Cavalier King Charles Spaniels are famous for. This looks like a professional or carefully composed photograph, possibly for a breeder, pet portrait, or greeting card.', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=195, prompt_tokens=105, total_tokens=300, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" ] }, "execution_count": null, @@ -2631,6 +3041,7 @@ } ], "source": [ + "chat = Chat(ms[1])\n", "chat(['Whats in this img?',img_fn.read_bytes()])" ] }, @@ -2659,19 +3070,21 @@ { "data": { "text/markdown": [ - "Your name is R E N S\n", + "Your name is R E D A C T E D\n", + "\n", + "I don't actually know your name - you haven't told me what it is yet! If you'd like me to spell your name, please let me know what it is first.\n", "\n", "
\n", "\n", "- id: `chatcmpl-xxx`\n", "- model: `claude-sonnet-4-5-20250929`\n", "- finish_reason: `stop`\n", - "- usage: `Usage(completion_tokens=5, prompt_tokens=276, total_tokens=281, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", + "- usage: `Usage(completion_tokens=47, prompt_tokens=16, total_tokens=63, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", "\n", "
" ], "text/plain": [ - "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content='Your name is R E N S', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=5, prompt_tokens=276, total_tokens=281, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" + "ModelResponse(id='chatcmpl-xxx', created=1000000000, model='claude-sonnet-4-5-20250929', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content=\"Your name is R E D A C T E D\\n\\nI don't actually know your name - you haven't told me what it is yet! If you'd like me to spell your name, please let me know what it is first.\", role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=47, prompt_tokens=16, total_tokens=63, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" ] }, "execution_count": null, @@ -2680,6 +3093,7 @@ } ], "source": [ + "chat = Chat(ms[1])\n", "chat(\"Spell my name\",prefill=\"Your name is R E\")" ] }, @@ -2700,7 +3114,7 @@ { "data": { "text/plain": [ - "Message(content='Your name is R E N S', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None})" + "Message(content=\"Your name is R E D A C T E D\\n\\nI don't actually know your name - you haven't told me what it is yet! If you'd like me to spell your name, please let me know what it is first.\", role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None})" ] }, "execution_count": null, @@ -4021,9 +4435,9 @@ " if include_usage: yield f\"\\nUsage: {o.usage}\"\n", " if (c := getattr(o.choices[0].message, 'tool_calls', None)):\n", " fn = first(c).function\n", - " yield f\"\\n
\\n\\n `{fn.name}({_trunc_str(fn.arguments)})`\\n\"\n", + " yield f\"\\n
\\n\\n - `{fn.name}({_trunc_str(fn.arguments, replace='')})`\\n\"\n", " elif isinstance(o, dict) and 'tool_call_id' in o: \n", - " yield f\" - `{_trunc_str(_clean_str(o.get('content')))}`\\n\\n
\\n\\n\"" + " yield f\" - `{o['tool_call_id']}`\\n\\n - `{_trunc_str(_clean_str(o.get('content')),replace='')}`\\n\\n
\\n\\n\"" ] }, { @@ -4078,8 +4492,10 @@ "I'll use the async_add function to calculate 5 + 7 for you.\n", "
\n", "\n", - " `async_add({\"a\": 5, \"b\": 7})`\n", - " - `12`\n", + " - `async_add({\"a\": 5, \"b\": 7})`\n", + " - `toolu_017wwCC1KYmDiSo2zDVeG8Dy`\n", + "\n", + " - `12`\n", "\n", "
\n", "\n", @@ -4189,20 +4605,26 @@ "Let me perform these calculations:\n", "
\n", "\n", - " `simple_add({\"a\": 10, \"b\": 5})`\n", - " - `15`\n", + " - `simple_add({\"a\": 10, \"b\": 5})`\n", + " - `toolu_018BGyenjiRkDQFU1jWP6qRo`\n", + "\n", + " - `15`\n", "\n", "
\n", "\n", - " - `3`\n", + " - `toolu_01CWqrNQvoRjf1Q1GLpTUgQR`\n", + "\n", + " - `3`\n", "\n", "
\n", "\n", "\n", "
\n", "\n", - " `multiply({\"a\": 15, \"b\": 3})`\n", - " - `45`\n", + " - `multiply({\"a\": 15, \"b\": 3})`\n", + " - `toolu_01V492CZD6QqavfVZdLAgFwH`\n", + "\n", + " - `45`\n", "\n", "
\n", "\n"