Skip to content

Commit 95bf54e

Browse files
authored
Merge pull request #30 from AnswerDotAI/feat/29-reconstruct-tool-call-message-history
reconstruct tool calls
2 parents 16ae279 + 451134d commit 95bf54e

File tree

4 files changed

+505
-53
lines changed

4 files changed

+505
-53
lines changed

cachy.jsonl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,8 @@
330330
{"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"}
331331
{"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\"}}"}
332332
{"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\"}}"}
333+
{"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\"}}"}
334+
{"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\"}}"}
335+
{"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\"}}"}
336+
{"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\"}}"}
337+
{"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\"}}"}

lisette/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
'lisette.core.Chat.print_hist': ('core.html#chat.print_hist', 'lisette/core.py'),
1717
'lisette.core._add_cache_control': ('core.html#_add_cache_control', 'lisette/core.py'),
1818
'lisette.core._alite_call_func': ('core.html#_alite_call_func', 'lisette/core.py'),
19+
'lisette.core._build_tool_hist': ('core.html#_build_tool_hist', 'lisette/core.py'),
1920
'lisette.core._bytes2content': ('core.html#_bytes2content', 'lisette/core.py'),
2021
'lisette.core._clean_str': ('core.html#_clean_str', 'lisette/core.py'),
22+
'lisette.core._details_extract': ('core.html#_details_extract', 'lisette/core.py'),
2123
'lisette.core._has_cache': ('core.html#_has_cache', 'lisette/core.py'),
2224
'lisette.core._has_search': ('core.html#_has_search', 'lisette/core.py'),
2325
'lisette.core._lite_call_func': ('core.html#_lite_call_func', 'lisette/core.py'),

lisette/core.py

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
44

55
# %% auto 0
6-
__all__ = ['sonn45', 'effort', 'patch_litellm', 'mk_msg', 'mk_msgs', 'stream_with_complete', 'lite_mk_func', 'cite_footnote',
7-
'cite_footnotes', 'Chat', 'random_tool_id', 'mk_tc', 'mk_tc_req', 'mk_tc_result', 'mk_tc_results',
6+
__all__ = ['sonn45', 'effort', 'patch_litellm', 'mk_msg', 'stream_with_complete', 'lite_mk_func', 'cite_footnote',
7+
'cite_footnotes', 'Chat', 'random_tool_id', 'mk_tc', 'mk_tc_req', 'mk_tc_result', 'mk_tc_results', 'mk_msgs',
88
'astream_with_complete', 'AsyncChat', 'aformat_stream', 'adisplay_stream']
99

1010
# %% ../nbs/00_core.ipynb
@@ -117,23 +117,6 @@ def mk_msg(content, # Content: str, bytes (image), list of mixed content, o
117117
msg = {"role": role, "content": c}
118118
return _add_cache_control(msg, ttl=ttl) if cache else msg
119119

120-
# %% ../nbs/00_core.ipynb
121-
def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields)
122-
cache=False, # Enable Anthropic caching
123-
ttl=None, # Cache TTL: '5m' (default) or '1h'
124-
):
125-
"Create a list of LiteLLM compatible messages."
126-
if not msgs: return []
127-
if not isinstance(msgs, list): msgs = [msgs]
128-
res,role = [],'user'
129-
for m in msgs:
130-
res.append(msg:=mk_msg(m, role=role))
131-
role = 'assistant' if msg['role'] in ('user','function', 'tool') else 'user'
132-
if cache:
133-
res[-1] = _add_cache_control(res[-1], ttl)
134-
res[-2] = _add_cache_control(res[-2], ttl)
135-
return res
136-
137120
# %% ../nbs/00_core.ipynb
138121
def stream_with_complete(gen, postproc=noop):
139122
"Extend streaming response chunks with the complete response"
@@ -269,11 +252,9 @@ def random_tool_id():
269252
return f'toolu_{random_part}'
270253

271254
# %% ../nbs/00_core.ipynb
272-
def mk_tc(func, idx=1, **kwargs):
273-
args = json.dumps(kwargs)
274-
if callable(func): func = func.__name__
275-
id = random_tool_id()
276-
return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': id, 'type': 'function'}
255+
def mk_tc(func, args, tcid=None, idx=1):
256+
if not tcid: tcid = random_tool_id()
257+
return {'index': idx, 'function': {'arguments': args, 'name': func}, 'id': tcid, 'type': 'function'}
277258

278259
# %% ../nbs/00_core.ipynb
279260
def mk_tc_req(content, tcs):
@@ -287,6 +268,48 @@ def mk_tc_result(tc, result): return {'tool_call_id': tc['id'], 'role': 'tool',
287268
# %% ../nbs/00_core.ipynb
288269
def mk_tc_results(tcq, results): return [mk_tc_result(a,b) for a,b in zip(tcq.tool_calls, results)]
289270

271+
# %% ../nbs/00_core.ipynb
272+
def _details_extract(x):
273+
"Extract fn, args, tool_call_id, result from <details>"
274+
m = re.search(r'<details.*?>(.*?)</details>', x, re.DOTALL)
275+
tc, tcid, res = re.findall(r'-\s*`([^`]+)`', m.group(1))
276+
fn, args = re.search(r'(\w+)\((.*?)\)', tc).groups()
277+
return fn, args, res, tcid
278+
279+
# %% ../nbs/00_core.ipynb
280+
def _build_tool_hist(msg):
281+
"Build original tool call messages from the tool call summary."
282+
hist = []
283+
parts = re.split(r'(<details.*?>.*?</details>)', msg, flags=re.DOTALL)
284+
for islast, (i,o) in loop_last(enumerate(parts)):
285+
if "<details class='tool-usage-details'>" not in o:
286+
if islast: hist.append(o.strip())
287+
continue
288+
fn, args, res, tcid = _details_extract(o)
289+
tc = mk_tc(fn, args, tcid)
290+
tcq = mk_tc_req(parts[i-1].strip() if i>0 else "", [tc])
291+
tcr = first(mk_tc_results(tcq, [res]))
292+
hist.extend([tcq, tcr])
293+
return hist
294+
295+
# %% ../nbs/00_core.ipynb
296+
def mk_msgs(msgs, # List of messages (each: str, bytes, list, or dict w 'role' and 'content' fields)
297+
cache=False, # Enable Anthropic caching
298+
ttl=None, # Cache TTL: '5m' (default) or '1h'
299+
):
300+
"Create a list of LiteLLM compatible messages."
301+
if not msgs: return []
302+
if not isinstance(msgs, list): msgs = [msgs]
303+
res,role = [],'user'
304+
msgs = L(msgs).map(lambda m: _build_tool_hist(m) if "<details class='tool-usage-details'>" in m else [m]).concat()
305+
for m in msgs:
306+
res.append(msg:=mk_msg(m, role=role))
307+
role = 'assistant' if msg['role'] in ('user','function', 'tool') else 'user'
308+
if cache:
309+
res[-1] = _add_cache_control(res[-1], ttl)
310+
res[-2] = _add_cache_control(res[-2], ttl)
311+
return res
312+
290313
# %% ../nbs/00_core.ipynb
291314
async def _alite_call_func(tc, ns, raise_on_err=True):
292315
try: fargs = json.loads(tc.function.arguments)
@@ -385,9 +408,9 @@ async def aformat_stream(rs, include_usage=False):
385408
if include_usage: yield f"\nUsage: {o.usage}"
386409
if (c := getattr(o.choices[0].message, 'tool_calls', None)):
387410
fn = first(c).function
388-
yield f"\n<details class='tool-usage-details'>\n\n `{fn.name}({_trunc_str(fn.arguments)})`\n"
411+
yield f"\n<details class='tool-usage-details'>\n\n - `{fn.name}({_trunc_str(fn.arguments, replace='<TRUNCATED>')})`\n"
389412
elif isinstance(o, dict) and 'tool_call_id' in o:
390-
yield f" - `{_trunc_str(_clean_str(o.get('content')))}`\n\n</details>\n\n"
413+
yield f" - `{o['tool_call_id']}`\n\n - `{_trunc_str(_clean_str(o.get('content')),replace='<TRUNCATED>')}`\n\n</details>\n\n"
391414

392415
# %% ../nbs/00_core.ipynb
393416
async def adisplay_stream(rs):

0 commit comments

Comments
 (0)