llmsurgery 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llmsurgery/__init__.py +1 -0
- llmsurgery/_modidx.py +166 -0
- llmsurgery/ant.py +427 -0
- llmsurgery/dialog.py +280 -0
- llmsurgery/hist.py +278 -0
- llmsurgery/ipynb.py +138 -0
- llmsurgery/oai.py +166 -0
- llmsurgery-0.0.1.dist-info/METADATA +40 -0
- llmsurgery-0.0.1.dist-info/RECORD +13 -0
- llmsurgery-0.0.1.dist-info/WHEEL +5 -0
- llmsurgery-0.0.1.dist-info/entry_points.txt +2 -0
- llmsurgery-0.0.1.dist-info/licenses/LICENSE +201 -0
- llmsurgery-0.0.1.dist-info/top_level.txt +1 -0
llmsurgery/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
llmsurgery/_modidx.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/llmsurgery',
|
|
5
|
+
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
|
+
'git_url': 'https://github.com/AnswerDotAI/llmsurgery',
|
|
7
|
+
'lib_path': 'llmsurgery'},
|
|
8
|
+
'syms': { 'llmsurgery.ant': { 'llmsurgery.ant.SessHits': ('ant.html#sesshits', 'llmsurgery/ant.py'),
|
|
9
|
+
'llmsurgery.ant.SessHits.__repr__': ('ant.html#sesshits.__repr__', 'llmsurgery/ant.py'),
|
|
10
|
+
'llmsurgery.ant._est_toks': ('ant.html#_est_toks', 'llmsurgery/ant.py'),
|
|
11
|
+
'llmsurgery.ant._norm_user': ('ant.html#_norm_user', 'llmsurgery/ant.py'),
|
|
12
|
+
'llmsurgery.ant._now': ('ant.html#_now', 'llmsurgery/ant.py'),
|
|
13
|
+
'llmsurgery.ant._preview': ('ant.html#_preview', 'llmsurgery/ant.py'),
|
|
14
|
+
'llmsurgery.ant._tr_txt': ('ant.html#_tr_txt', 'llmsurgery/ant.py'),
|
|
15
|
+
'llmsurgery.ant._trunc_deep': ('ant.html#_trunc_deep', 'llmsurgery/ant.py'),
|
|
16
|
+
'llmsurgery.ant._txts': ('ant.html#_txts', 'llmsurgery/ant.py'),
|
|
17
|
+
'llmsurgery.ant.append_sess': ('ant.html#append_sess', 'llmsurgery/ant.py'),
|
|
18
|
+
'llmsurgery.ant.canon': ('ant.html#canon', 'llmsurgery/ant.py'),
|
|
19
|
+
'llmsurgery.ant.conv_recs': ('ant.html#conv_recs', 'llmsurgery/ant.py'),
|
|
20
|
+
'llmsurgery.ant.cur_sess': ('ant.html#cur_sess', 'llmsurgery/ant.py'),
|
|
21
|
+
'llmsurgery.ant.dlg2msgs': ('ant.html#dlg2msgs', 'llmsurgery/ant.py'),
|
|
22
|
+
'llmsurgery.ant.dlg2sess': ('ant.html#dlg2sess', 'llmsurgery/ant.py'),
|
|
23
|
+
'llmsurgery.ant.fork_sess': ('ant.html#fork_sess', 'llmsurgery/ant.py'),
|
|
24
|
+
'llmsurgery.ant.load_sess': ('ant.html#load_sess', 'llmsurgery/ant.py'),
|
|
25
|
+
'llmsurgery.ant.mk_rec': ('ant.html#mk_rec', 'llmsurgery/ant.py'),
|
|
26
|
+
'llmsurgery.ant.mk_tr': ('ant.html#mk_tr', 'llmsurgery/ant.py'),
|
|
27
|
+
'llmsurgery.ant.mk_tu': ('ant.html#mk_tu', 'llmsurgery/ant.py'),
|
|
28
|
+
'llmsurgery.ant.msgs2dlg': ('ant.html#msgs2dlg', 'llmsurgery/ant.py'),
|
|
29
|
+
'llmsurgery.ant.msgs2recs': ('ant.html#msgs2recs', 'llmsurgery/ant.py'),
|
|
30
|
+
'llmsurgery.ant.rec_role': ('ant.html#rec_role', 'llmsurgery/ant.py'),
|
|
31
|
+
'llmsurgery.ant.rec_txt': ('ant.html#rec_txt', 'llmsurgery/ant.py'),
|
|
32
|
+
'llmsurgery.ant.recs2msgs': ('ant.html#recs2msgs', 'llmsurgery/ant.py'),
|
|
33
|
+
'llmsurgery.ant.reid_recs': ('ant.html#reid_recs', 'llmsurgery/ant.py'),
|
|
34
|
+
'llmsurgery.ant.save_sess': ('ant.html#save_sess', 'llmsurgery/ant.py'),
|
|
35
|
+
'llmsurgery.ant.sess2dlg': ('ant.html#sess2dlg', 'llmsurgery/ant.py'),
|
|
36
|
+
'llmsurgery.ant.sess_dir': ('ant.html#sess_dir', 'llmsurgery/ant.py'),
|
|
37
|
+
'llmsurgery.ant.sess_file': ('ant.html#sess_file', 'llmsurgery/ant.py'),
|
|
38
|
+
'llmsurgery.ant.sess_search': ('ant.html#sess_search', 'llmsurgery/ant.py'),
|
|
39
|
+
'llmsurgery.ant.sess_thread': ('ant.html#sess_thread', 'llmsurgery/ant.py'),
|
|
40
|
+
'llmsurgery.ant.show_recs': ('ant.html#show_recs', 'llmsurgery/ant.py'),
|
|
41
|
+
'llmsurgery.ant.stable_uuid': ('ant.html#stable_uuid', 'llmsurgery/ant.py'),
|
|
42
|
+
'llmsurgery.ant.strip_think': ('ant.html#strip_think', 'llmsurgery/ant.py'),
|
|
43
|
+
'llmsurgery.ant.tool_turn': ('ant.html#tool_turn', 'llmsurgery/ant.py'),
|
|
44
|
+
'llmsurgery.ant.trunc_tools': ('ant.html#trunc_tools', 'llmsurgery/ant.py')},
|
|
45
|
+
'llmsurgery.dialog': { 'llmsurgery.dialog.Attachment': ('dialog.html#attachment', 'llmsurgery/dialog.py'),
|
|
46
|
+
'llmsurgery.dialog.Attachment.__init__': ('dialog.html#attachment.__init__', 'llmsurgery/dialog.py'),
|
|
47
|
+
'llmsurgery.dialog.Dialog': ('dialog.html#dialog', 'llmsurgery/dialog.py'),
|
|
48
|
+
'llmsurgery.dialog.Dialog.__init__': ('dialog.html#dialog.__init__', 'llmsurgery/dialog.py'),
|
|
49
|
+
'llmsurgery.dialog.Dialog._repr_markdown_': ( 'dialog.html#dialog._repr_markdown_',
|
|
50
|
+
'llmsurgery/dialog.py'),
|
|
51
|
+
'llmsurgery.dialog.Dialog.find_msg': ('dialog.html#dialog.find_msg', 'llmsurgery/dialog.py'),
|
|
52
|
+
'llmsurgery.dialog.Dialog.mk_message': ('dialog.html#dialog.mk_message', 'llmsurgery/dialog.py'),
|
|
53
|
+
'llmsurgery.dialog.Dialog.mk_messages': ('dialog.html#dialog.mk_messages', 'llmsurgery/dialog.py'),
|
|
54
|
+
'llmsurgery.dialog.Dialog.remove_msgs': ('dialog.html#dialog.remove_msgs', 'llmsurgery/dialog.py'),
|
|
55
|
+
'llmsurgery.dialog.Dialog.todict': ('dialog.html#dialog.todict', 'llmsurgery/dialog.py'),
|
|
56
|
+
'llmsurgery.dialog.Message': ('dialog.html#message', 'llmsurgery/dialog.py'),
|
|
57
|
+
'llmsurgery.dialog.Message.__deepcopy__': ('dialog.html#message.__deepcopy__', 'llmsurgery/dialog.py'),
|
|
58
|
+
'llmsurgery.dialog.Message.__flds__': ('dialog.html#message.__flds__', 'llmsurgery/dialog.py'),
|
|
59
|
+
'llmsurgery.dialog.Message.__init__': ('dialog.html#message.__init__', 'llmsurgery/dialog.py'),
|
|
60
|
+
'llmsurgery.dialog.Message._neighbor': ('dialog.html#message._neighbor', 'llmsurgery/dialog.py'),
|
|
61
|
+
'llmsurgery.dialog.Message._repr_markdown_': ( 'dialog.html#message._repr_markdown_',
|
|
62
|
+
'llmsurgery/dialog.py'),
|
|
63
|
+
'llmsurgery.dialog.Message.ai_res': ('dialog.html#message.ai_res', 'llmsurgery/dialog.py'),
|
|
64
|
+
'llmsurgery.dialog.Message.clear_inp_cache': ( 'dialog.html#message.clear_inp_cache',
|
|
65
|
+
'llmsurgery/dialog.py'),
|
|
66
|
+
'llmsurgery.dialog.Message.clear_out_cache': ( 'dialog.html#message.clear_out_cache',
|
|
67
|
+
'llmsurgery/dialog.py'),
|
|
68
|
+
'llmsurgery.dialog.Message.content': ('dialog.html#message.content', 'llmsurgery/dialog.py'),
|
|
69
|
+
'llmsurgery.dialog.Message.dlg': ('dialog.html#message.dlg', 'llmsurgery/dialog.py'),
|
|
70
|
+
'llmsurgery.dialog.Message.flds': ('dialog.html#message.flds', 'llmsurgery/dialog.py'),
|
|
71
|
+
'llmsurgery.dialog.Message.has_error': ('dialog.html#message.has_error', 'llmsurgery/dialog.py'),
|
|
72
|
+
'llmsurgery.dialog.Message.insert_after': ('dialog.html#message.insert_after', 'llmsurgery/dialog.py'),
|
|
73
|
+
'llmsurgery.dialog.Message.mk_attachment': ('dialog.html#message.mk_attachment', 'llmsurgery/dialog.py'),
|
|
74
|
+
'llmsurgery.dialog.Message.msg_type': ('dialog.html#message.msg_type', 'llmsurgery/dialog.py'),
|
|
75
|
+
'llmsurgery.dialog.Message.next': ('dialog.html#message.next', 'llmsurgery/dialog.py'),
|
|
76
|
+
'llmsurgery.dialog.Message.output': ('dialog.html#message.output', 'llmsurgery/dialog.py'),
|
|
77
|
+
'llmsurgery.dialog.Message.previous': ('dialog.html#message.previous', 'llmsurgery/dialog.py'),
|
|
78
|
+
'llmsurgery.dialog.Message.rel_msg': ('dialog.html#message.rel_msg', 'llmsurgery/dialog.py'),
|
|
79
|
+
'llmsurgery.dialog.Message.remove_from_dlg': ( 'dialog.html#message.remove_from_dlg',
|
|
80
|
+
'llmsurgery/dialog.py'),
|
|
81
|
+
'llmsurgery.dialog.Message.summ': ('dialog.html#message.summ', 'llmsurgery/dialog.py'),
|
|
82
|
+
'llmsurgery.dialog.Message.update': ('dialog.html#message.update', 'llmsurgery/dialog.py'),
|
|
83
|
+
'llmsurgery.dialog.add_id_hash': ('dialog.html#add_id_hash', 'llmsurgery/dialog.py'),
|
|
84
|
+
'llmsurgery.dialog.code_output': ('dialog.html#code_output', 'llmsurgery/dialog.py'),
|
|
85
|
+
'llmsurgery.dialog.displayobj': ('dialog.html#displayobj', 'llmsurgery/dialog.py'),
|
|
86
|
+
'llmsurgery.dialog.get_msg': ('dialog.html#get_msg', 'llmsurgery/dialog.py'),
|
|
87
|
+
'llmsurgery.dialog.get_output_mds': ('dialog.html#get_output_mds', 'llmsurgery/dialog.py'),
|
|
88
|
+
'llmsurgery.dialog.mk_code_output': ('dialog.html#mk_code_output', 'llmsurgery/dialog.py'),
|
|
89
|
+
'llmsurgery.dialog.mk_displayobj': ('dialog.html#mk_displayobj', 'llmsurgery/dialog.py'),
|
|
90
|
+
'llmsurgery.dialog.mk_output': ('dialog.html#mk_output', 'llmsurgery/dialog.py'),
|
|
91
|
+
'llmsurgery.dialog.prompt_output': ('dialog.html#prompt_output', 'llmsurgery/dialog.py'),
|
|
92
|
+
'llmsurgery.dialog.ruuid4': ('dialog.html#ruuid4', 'llmsurgery/dialog.py')},
|
|
93
|
+
'llmsurgery.hist': { 'llmsurgery.hist.Message._xml_content': ('hist.html#message._xml_content', 'llmsurgery/hist.py'),
|
|
94
|
+
'llmsurgery.hist.Message.ai_output': ('hist.html#message.ai_output', 'llmsurgery/hist.py'),
|
|
95
|
+
'llmsurgery.hist.Message.local_time': ('hist.html#message.local_time', 'llmsurgery/hist.py'),
|
|
96
|
+
'llmsurgery.hist.Message.media_extra': ('hist.html#message.media_extra', 'llmsurgery/hist.py'),
|
|
97
|
+
'llmsurgery.hist.Message.prep_img': ('hist.html#message.prep_img', 'llmsurgery/hist.py'),
|
|
98
|
+
'llmsurgery.hist.Message.prompt_txt': ('hist.html#message.prompt_txt', 'llmsurgery/hist.py'),
|
|
99
|
+
'llmsurgery.hist.Message.render_out': ('hist.html#message.render_out', 'llmsurgery/hist.py'),
|
|
100
|
+
'llmsurgery.hist.Message.to_media': ('hist.html#message.to_media', 'llmsurgery/hist.py'),
|
|
101
|
+
'llmsurgery.hist.Message.to_parts': ('hist.html#message.to_parts', 'llmsurgery/hist.py'),
|
|
102
|
+
'llmsurgery.hist.Message.to_xml': ('hist.html#message.to_xml', 'llmsurgery/hist.py'),
|
|
103
|
+
'llmsurgery.hist.Message.todict': ('hist.html#message.todict', 'llmsurgery/hist.py'),
|
|
104
|
+
'llmsurgery.hist._MissError': ('hist.html#_misserror', 'llmsurgery/hist.py'),
|
|
105
|
+
'llmsurgery.hist._explode': ('hist.html#_explode', 'llmsurgery/hist.py'),
|
|
106
|
+
'llmsurgery.hist._img_id': ('hist.html#_img_id', 'llmsurgery/hist.py'),
|
|
107
|
+
'llmsurgery.hist._img_item': ('hist.html#_img_item', 'llmsurgery/hist.py'),
|
|
108
|
+
'llmsurgery.hist._img_output': ('hist.html#_img_output', 'llmsurgery/hist.py'),
|
|
109
|
+
'llmsurgery.hist._media_atts': ('hist.html#_media_atts', 'llmsurgery/hist.py'),
|
|
110
|
+
'llmsurgery.hist._media_unavailable': ('hist.html#_media_unavailable', 'llmsurgery/hist.py'),
|
|
111
|
+
'llmsurgery.hist._media_xml': ('hist.html#_media_xml', 'llmsurgery/hist.py'),
|
|
112
|
+
'llmsurgery.hist._mime_kind': ('hist.html#_mime_kind', 'llmsurgery/hist.py'),
|
|
113
|
+
'llmsurgery.hist._mime_supported': ('hist.html#_mime_supported', 'llmsurgery/hist.py'),
|
|
114
|
+
'llmsurgery.hist._seq_tools': ('hist.html#_seq_tools', 'llmsurgery/hist.py'),
|
|
115
|
+
'llmsurgery.hist.ai_fmt': ('hist.html#ai_fmt', 'llmsurgery/hist.py'),
|
|
116
|
+
'llmsurgery.hist.dlg2canon': ('hist.html#dlg2canon', 'llmsurgery/hist.py'),
|
|
117
|
+
'llmsurgery.hist.join_out': ('hist.html#join_out', 'llmsurgery/hist.py'),
|
|
118
|
+
'llmsurgery.hist.media_item': ('hist.html#media_item', 'llmsurgery/hist.py'),
|
|
119
|
+
'llmsurgery.hist.msgs2hist': ('hist.html#msgs2hist', 'llmsurgery/hist.py'),
|
|
120
|
+
'llmsurgery.hist.render_output_ai': ('hist.html#render_output_ai', 'llmsurgery/hist.py'),
|
|
121
|
+
'llmsurgery.hist.render_outputs_ai': ('hist.html#render_outputs_ai', 'llmsurgery/hist.py'),
|
|
122
|
+
'llmsurgery.hist.to_local_time': ('hist.html#to_local_time', 'llmsurgery/hist.py'),
|
|
123
|
+
'llmsurgery.hist.try_eval': ('hist.html#try_eval', 'llmsurgery/hist.py')},
|
|
124
|
+
'llmsurgery.ipynb': { 'llmsurgery.ipynb.Dialog.cell2msg': ('ipynb.html#dialog.cell2msg', 'llmsurgery/ipynb.py'),
|
|
125
|
+
'llmsurgery.ipynb.Dialog.from_cells': ('ipynb.html#dialog.from_cells', 'llmsurgery/ipynb.py'),
|
|
126
|
+
'llmsurgery.ipynb.Dialog.write': ('ipynb.html#dialog.write', 'llmsurgery/ipynb.py'),
|
|
127
|
+
'llmsurgery.ipynb.Message.cell_meta': ('ipynb.html#message.cell_meta', 'llmsurgery/ipynb.py'),
|
|
128
|
+
'llmsurgery.ipynb.Message.to_cell': ('ipynb.html#message.to_cell', 'llmsurgery/ipynb.py'),
|
|
129
|
+
'llmsurgery.ipynb._clean_out_meta': ('ipynb.html#_clean_out_meta', 'llmsurgery/ipynb.py'),
|
|
130
|
+
'llmsurgery.ipynb._output_from_cell': ('ipynb.html#_output_from_cell', 'llmsurgery/ipynb.py'),
|
|
131
|
+
'llmsurgery.ipynb.att2dict': ('ipynb.html#att2dict', 'llmsurgery/ipynb.py'),
|
|
132
|
+
'llmsurgery.ipynb.dict2att': ('ipynb.html#dict2att', 'llmsurgery/ipynb.py'),
|
|
133
|
+
'llmsurgery.ipynb.get_ipynb': ('ipynb.html#get_ipynb', 'llmsurgery/ipynb.py'),
|
|
134
|
+
'llmsurgery.ipynb.ipynb_cells': ('ipynb.html#ipynb_cells', 'llmsurgery/ipynb.py'),
|
|
135
|
+
'llmsurgery.ipynb.read_ipynb': ('ipynb.html#read_ipynb', 'llmsurgery/ipynb.py'),
|
|
136
|
+
'llmsurgery.ipynb.split_cell_src': ('ipynb.html#split_cell_src', 'llmsurgery/ipynb.py'),
|
|
137
|
+
'llmsurgery.ipynb.write_ipynb': ('ipynb.html#write_ipynb', 'llmsurgery/ipynb.py')},
|
|
138
|
+
'llmsurgery.oai': { 'llmsurgery.oai.CodexAppServer': ('oai.html#codexappserver', 'llmsurgery/oai.py'),
|
|
139
|
+
'llmsurgery.oai.CodexAppServer.__aenter__': ('oai.html#codexappserver.__aenter__', 'llmsurgery/oai.py'),
|
|
140
|
+
'llmsurgery.oai.CodexAppServer.__aexit__': ('oai.html#codexappserver.__aexit__', 'llmsurgery/oai.py'),
|
|
141
|
+
'llmsurgery.oai.CodexAppServer.__init__': ('oai.html#codexappserver.__init__', 'llmsurgery/oai.py'),
|
|
142
|
+
'llmsurgery.oai.CodexAppServer._send': ('oai.html#codexappserver._send', 'llmsurgery/oai.py'),
|
|
143
|
+
'llmsurgery.oai.CodexAppServer.close': ('oai.html#codexappserver.close', 'llmsurgery/oai.py'),
|
|
144
|
+
'llmsurgery.oai.CodexAppServer.create_thread': ( 'oai.html#codexappserver.create_thread',
|
|
145
|
+
'llmsurgery/oai.py'),
|
|
146
|
+
'llmsurgery.oai.CodexAppServer.fork_thread': ('oai.html#codexappserver.fork_thread', 'llmsurgery/oai.py'),
|
|
147
|
+
'llmsurgery.oai.CodexAppServer.inject_items': ('oai.html#codexappserver.inject_items', 'llmsurgery/oai.py'),
|
|
148
|
+
'llmsurgery.oai.CodexAppServer.notify': ('oai.html#codexappserver.notify', 'llmsurgery/oai.py'),
|
|
149
|
+
'llmsurgery.oai.CodexAppServer.request': ('oai.html#codexappserver.request', 'llmsurgery/oai.py'),
|
|
150
|
+
'llmsurgery.oai.CodexAppServer.start': ('oai.html#codexappserver.start', 'llmsurgery/oai.py'),
|
|
151
|
+
'llmsurgery.oai.CodexAppServer.start_thread': ('oai.html#codexappserver.start_thread', 'llmsurgery/oai.py'),
|
|
152
|
+
'llmsurgery.oai._item_txts': ('oai.html#_item_txts', 'llmsurgery/oai.py'),
|
|
153
|
+
'llmsurgery.oai._reid': ('oai.html#_reid', 'llmsurgery/oai.py'),
|
|
154
|
+
'llmsurgery.oai.codex_call': ('oai.html#codex_call', 'llmsurgery/oai.py'),
|
|
155
|
+
'llmsurgery.oai.codex_msg': ('oai.html#codex_msg', 'llmsurgery/oai.py'),
|
|
156
|
+
'llmsurgery.oai.codex_output': ('oai.html#codex_output', 'llmsurgery/oai.py'),
|
|
157
|
+
'llmsurgery.oai.curate_items': ('oai.html#curate_items', 'llmsurgery/oai.py'),
|
|
158
|
+
'llmsurgery.oai.dlg2items': ('oai.html#dlg2items', 'llmsurgery/oai.py'),
|
|
159
|
+
'llmsurgery.oai.dlg2thread': ('oai.html#dlg2thread', 'llmsurgery/oai.py'),
|
|
160
|
+
'llmsurgery.oai.item_txt': ('oai.html#item_txt', 'llmsurgery/oai.py'),
|
|
161
|
+
'llmsurgery.oai.load_rollout': ('oai.html#load_rollout', 'llmsurgery/oai.py'),
|
|
162
|
+
'llmsurgery.oai.reid_items': ('oai.html#reid_items', 'llmsurgery/oai.py'),
|
|
163
|
+
'llmsurgery.oai.response_items': ('oai.html#response_items', 'llmsurgery/oai.py'),
|
|
164
|
+
'llmsurgery.oai.rollout_file': ('oai.html#rollout_file', 'llmsurgery/oai.py'),
|
|
165
|
+
'llmsurgery.oai.strip_reasoning': ('oai.html#strip_reasoning', 'llmsurgery/oai.py'),
|
|
166
|
+
'llmsurgery.oai.tool_turn': ('oai.html#tool_turn', 'llmsurgery/oai.py')}}}
|
llmsurgery/ant.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Read, write, and build Claude Code session transcripts
|
|
2
|
+
|
|
3
|
+
Docs: https://AnswerDotAI.github.io/llmsurgery/ant.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_ant.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['SESSIONS', 'CC_VERSION', 'sess_dir', 'cur_sess', 'sess_file', 'canon', 'stable_uuid', 'mk_rec', 'save_sess',
|
|
9
|
+
'append_sess', 'msgs2recs', 'mk_tu', 'mk_tr', 'tool_turn', 'load_sess', 'sess_thread', 'rec_txt',
|
|
10
|
+
'conv_recs', 'rec_role', 'SessHits', 'sess_search', 'show_recs', 'strip_think', 'trunc_tools', 'reid_recs',
|
|
11
|
+
'fork_sess', 'dlg2msgs', 'dlg2sess', 'recs2msgs', 'msgs2dlg', 'sess2dlg']
|
|
12
|
+
|
|
13
|
+
# %% ../nbs/03_ant.ipynb #09dc6bf6
|
|
14
|
+
import base64, json, os, re, uuid
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from fastcore.utils import *
|
|
17
|
+
from fastllm.anthropic import denorm_msgs
|
|
18
|
+
from fastllm.chat import Msg, Part, PartType, mk_msg, hist2fmt, data_url
|
|
19
|
+
from .hist import dlg2canon
|
|
20
|
+
from .dialog import *
|
|
21
|
+
|
|
22
|
+
# %% ../nbs/03_ant.ipynb #66bb9972
|
|
23
|
+
SESSIONS = Path.home()/'.claude'/'projects'
|
|
24
|
+
|
|
25
|
+
def sess_dir(
|
|
26
|
+
cwd=None, # Project directory; the current directory if None
|
|
27
|
+
):
|
|
28
|
+
"The folder where Claude Code keeps session transcripts for the project at `cwd`"
|
|
29
|
+
return SESSIONS/re.sub(r'[^a-zA-Z0-9]', '-', str(Path(cwd or '.').resolve()))
|
|
30
|
+
|
|
31
|
+
# %% ../nbs/03_ant.ipynb #a86a78a2
|
|
32
|
+
def cur_sess(
|
|
33
|
+
cwd=None, # Project directory; the current directory if None
|
|
34
|
+
):
|
|
35
|
+
"The current conversation's session id: the most recent transcript for the project at `cwd`, else the advertised id"
|
|
36
|
+
try: return max(sess_dir(cwd).glob('*.jsonl'), key=lambda p: p.stat().st_mtime).stem
|
|
37
|
+
except ValueError: return os.environ.get('CLAUDE_CODE_SESSION_ID')
|
|
38
|
+
|
|
39
|
+
# %% ../nbs/03_ant.ipynb #d031edb9
|
|
40
|
+
def sess_file(
|
|
41
|
+
sid=None, # Session id; `cur_sess(cwd)` if None
|
|
42
|
+
cwd=None, # Project directory; the current directory, then all projects, if None
|
|
43
|
+
):
|
|
44
|
+
"Path to the transcript of session `sid` for the project at `cwd`"
|
|
45
|
+
sid = sid or cur_sess(cwd)
|
|
46
|
+
p = sess_dir(cwd)/f'{sid}.jsonl'
|
|
47
|
+
if cwd is not None or p.exists(): return p
|
|
48
|
+
return first(SESSIONS.glob(f'*/{sid}.jsonl')) or p
|
|
49
|
+
|
|
50
|
+
# %% ../nbs/03_ant.ipynb #5850201a
|
|
51
|
+
CC_VERSION = '2.1.206'
|
|
52
|
+
|
|
53
|
+
def canon(o):
|
|
54
|
+
"Canonical compact JSON for `o`, key-sorted, for stable hashing"
|
|
55
|
+
return json.dumps(o, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
|
|
56
|
+
|
|
57
|
+
def stable_uuid(s):
|
|
58
|
+
"A uuid deterministically derived from string `s`"
|
|
59
|
+
return str(uuid.uuid5(uuid.NAMESPACE_URL, s))
|
|
60
|
+
|
|
61
|
+
# %% ../nbs/03_ant.ipynb #aec44130
|
|
62
|
+
def _now(): return datetime.now(timezone.utc).strftime(r'%Y-%m-%dT%H:%M:%S.%f')[:-3]+'Z'
|
|
63
|
+
|
|
64
|
+
def _txts(o, skip=()):
|
|
65
|
+
if isinstance(o, str): yield o
|
|
66
|
+
elif isinstance(o, dict): yield from (t for k,v in o.items() if k not in skip for t in _txts(v, skip))
|
|
67
|
+
elif is_listy(o): yield from (t for x in o for t in _txts(x, skip))
|
|
68
|
+
|
|
69
|
+
def _est_toks(o):
|
|
70
|
+
"Rule-of-thumb token estimate for every string in `o`: words * 1.5"
|
|
71
|
+
return int(sum(len(s.split()) for s in _txts(o))*1.5)
|
|
72
|
+
|
|
73
|
+
# %% ../nbs/03_ant.ipynb #9da9f61e
|
|
74
|
+
def mk_rec(
|
|
75
|
+
role, # 'user' or 'assistant'
|
|
76
|
+
content, # A string, or a list of content blocks
|
|
77
|
+
cwd='.', # Project directory recorded in the envelope
|
|
78
|
+
uid=None, # Record uuid; random if None
|
|
79
|
+
ts=None, # ISO8601 timestamp; the current time if None
|
|
80
|
+
model='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
|
|
81
|
+
input_toks=0, # `input_tokens` recorded in assistant usage, e.g. an estimate of the context so far
|
|
82
|
+
**kwargs, # Extra or overriding envelope fields, e.g. `isCompactSummary=True`
|
|
83
|
+
):
|
|
84
|
+
"A transcript record for one conversation message, ready for `save_sess`"
|
|
85
|
+
uid = uid or str(uuid.uuid4())
|
|
86
|
+
if role=='user' and isinstance(content, list) and len(content)==1 and content[0].get('type')=='text': content = content[0]['text']
|
|
87
|
+
msg = dict(type='message', role=role, content=content)
|
|
88
|
+
r = dict(type=role, uuid=uid, parentUuid=None, sessionId=None, timestamp=ts or _now(), cwd=str(Path(cwd).resolve()),
|
|
89
|
+
version=CC_VERSION, gitBranch='HEAD', isSidechain=False, userType='external', permissionMode='default', message=msg)
|
|
90
|
+
if role=='assistant':
|
|
91
|
+
tu = isinstance(content, list) and any(isinstance(b, dict) and b.get('type')=='tool_use' for b in content)
|
|
92
|
+
r['requestId'] = 'req_'+stable_uuid(f'{uid}:req').replace('-', '')[:24]
|
|
93
|
+
usage = dict(input_tokens=input_toks, output_tokens=_est_toks(content), cache_creation_input_tokens=0, cache_read_input_tokens=0)
|
|
94
|
+
msg.update(id='msg_'+stable_uuid(f'{uid}:msg').replace('-', '')[:24],
|
|
95
|
+
stop_reason='tool_use' if tu else 'end_turn', stop_sequence=None, stop_details=None, usage=usage)
|
|
96
|
+
if model: msg['model'] = model
|
|
97
|
+
return dict(r, **kwargs)
|
|
98
|
+
|
|
99
|
+
# %% ../nbs/03_ant.ipynb #ba9f2c9f
|
|
100
|
+
def save_sess(
|
|
101
|
+
recs, # Records in conversation order, e.g. from `mk_rec`
|
|
102
|
+
sid=None, # Session id; a fresh uuid if None
|
|
103
|
+
cwd=None, # Project directory; the current directory if None
|
|
104
|
+
ts=None, # If given, stamp every record's timestamp: True for the current time, or an ISO8601 string
|
|
105
|
+
):
|
|
106
|
+
"Chain `recs`, write them as session `sid` for the project at `cwd`, and return `sid`"
|
|
107
|
+
sid,prev = sid or str(uuid.uuid4()),None
|
|
108
|
+
for r in recs:
|
|
109
|
+
r['sessionId'],r['parentUuid'],prev = sid,prev,r['uuid']
|
|
110
|
+
if ts: r['timestamp'] = _now() if ts is True else ts
|
|
111
|
+
if 'session_id' in r: r['session_id'] = sid
|
|
112
|
+
f = sess_file(sid, cwd or '.')
|
|
113
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
f.write_text(''.join(json.dumps(r)+'\n' for r in recs))
|
|
115
|
+
return sid
|
|
116
|
+
|
|
117
|
+
# %% ../nbs/03_ant.ipynb #c161a097
|
|
118
|
+
def append_sess(
|
|
119
|
+
recs, # Records to append, e.g. a munged template round
|
|
120
|
+
sid=None, # Session to append to; `cur_sess()` if None
|
|
121
|
+
cwd=None, # Project directory; the current directory if None
|
|
122
|
+
ts=None, # If given, stamp each appended record's timestamp: True for the current time, or an ISO8601 string
|
|
123
|
+
):
|
|
124
|
+
"Chain `recs` onto the tail of session `sid` and append them to its transcript, returning `sid`"
|
|
125
|
+
sid = sid or cur_sess(cwd)
|
|
126
|
+
prev = last(r['uuid'] for r in load_sess(sid, cwd) if 'uuid' in r)
|
|
127
|
+
for r in recs:
|
|
128
|
+
r['sessionId'],r['parentUuid'],prev = sid,prev,r['uuid']
|
|
129
|
+
if ts: r['timestamp'] = _now() if ts is True else ts
|
|
130
|
+
if 'session_id' in r: r['session_id'] = sid
|
|
131
|
+
with sess_file(sid, cwd).open('a') as f: f.writelines(json.dumps(r)+'\n' for r in recs)
|
|
132
|
+
return sid
|
|
133
|
+
|
|
134
|
+
# %% ../nbs/03_ant.ipynb #53b019df
|
|
135
|
+
def msgs2recs(
|
|
136
|
+
msgs, # Anthropic-style messages: dicts with `role` and `content`
|
|
137
|
+
key='', # Salt: the same messages and key give the same ids
|
|
138
|
+
cwd='.', # Project directory recorded in the envelopes
|
|
139
|
+
ts='2026-01-01T00:00:00.000Z', # Timestamp for every record
|
|
140
|
+
model='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
|
|
141
|
+
**kwargs, # Extra envelope fields for every record, e.g. `entrypoint`
|
|
142
|
+
):
|
|
143
|
+
"Deterministic transcript records for `msgs`, one record per message"
|
|
144
|
+
out,tot = [],0
|
|
145
|
+
for i,m in enumerate(msgs):
|
|
146
|
+
out.append(mk_rec(m['role'], m['content'], cwd=cwd, uid=stable_uuid(f'{key}:{i}:{canon(m)}'), ts=ts, model=model, input_toks=tot, **kwargs))
|
|
147
|
+
tot += _est_toks(m['content'])
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
# %% ../nbs/03_ant.ipynb #fb2e37dc
|
|
151
|
+
def mk_tu(
|
|
152
|
+
name, # Tool name, as the transcript records it
|
|
153
|
+
input=None, # Tool arguments
|
|
154
|
+
tid=None, # tool_use id; random if None
|
|
155
|
+
):
|
|
156
|
+
"A `tool_use` content block"
|
|
157
|
+
return dict(type='tool_use', id=tid or 'toolu_'+uuid.uuid4().hex[:24], name=name, input=input or {})
|
|
158
|
+
|
|
159
|
+
def mk_tr(
|
|
160
|
+
tu, # The `tool_use` block being answered
|
|
161
|
+
content, # The tool's output
|
|
162
|
+
):
|
|
163
|
+
"The `tool_result` content block answering `tu`"
|
|
164
|
+
return dict(type='tool_result', tool_use_id=tu['id'], content=content)
|
|
165
|
+
|
|
166
|
+
def tool_turn(
|
|
167
|
+
prompt, # The user request
|
|
168
|
+
name, # Tool name
|
|
169
|
+
input, # Tool arguments
|
|
170
|
+
output, # Tool result
|
|
171
|
+
answer, # The assistant's closing text
|
|
172
|
+
**kwargs, # Passed to each `mk_rec`, e.g. `cwd`
|
|
173
|
+
):
|
|
174
|
+
"A complete synthetic tool-use turn, as four records ready for `save_sess`"
|
|
175
|
+
tu = mk_tu(name, input)
|
|
176
|
+
return [mk_rec('user', prompt, **kwargs), mk_rec('assistant', [tu], **kwargs),
|
|
177
|
+
mk_rec('user', [mk_tr(tu, output)], **kwargs), mk_rec('assistant', [dict(type='text', text=answer)], **kwargs)]
|
|
178
|
+
|
|
179
|
+
# %% ../nbs/03_ant.ipynb #e3ef0c32
|
|
180
|
+
def load_sess(
|
|
181
|
+
sid=None, # Session id; the current session if None
|
|
182
|
+
cwd=None, # Project directory; the current directory if None
|
|
183
|
+
):
|
|
184
|
+
"The records of session `sid`, as an `L` of attribute-access dicts"
|
|
185
|
+
return L(dict2obj(json.loads(l)) for l in sess_file(sid, cwd).read_text().splitlines())
|
|
186
|
+
|
|
187
|
+
# %% ../nbs/03_ant.ipynb #a5f38b3d
|
|
188
|
+
def sess_thread(
|
|
189
|
+
recs, # Session records, e.g. from `load_sess`
|
|
190
|
+
):
|
|
191
|
+
"The records on the active conversation chain, walking `parentUuid` back from the last record"
|
|
192
|
+
byid = {r.uuid:r for r in recs if 'uuid' in r}
|
|
193
|
+
cur,res = recs.filter(lambda r: 'uuid' in r)[-1],[]
|
|
194
|
+
while cur is not None:
|
|
195
|
+
res.append(cur)
|
|
196
|
+
cur = byid.get(cur.get('parentUuid'))
|
|
197
|
+
return L(reversed(res))
|
|
198
|
+
|
|
199
|
+
# %% ../nbs/03_ant.ipynb #5bfa0829
|
|
200
|
+
def rec_txt(
|
|
201
|
+
r, # A session record
|
|
202
|
+
):
|
|
203
|
+
"Every readable string in `r`'s message content, joined, for finding records by text"
|
|
204
|
+
return '\n'.join(_txts(obj2dict(r).get('message', {}).get('content', ''), skip=('type','id','tool_use_id','signature')))
|
|
205
|
+
|
|
206
|
+
# %% ../nbs/03_ant.ipynb #f9d47bbc
|
|
207
|
+
def conv_recs(
|
|
208
|
+
recs, # Session records, e.g. from `load_sess`
|
|
209
|
+
):
|
|
210
|
+
"Just the records carrying conversation messages, dropping Claude Code's bookkeeping"
|
|
211
|
+
return L(r for r in recs if r.get('type') in ('user','assistant') and 'message' in r)
|
|
212
|
+
|
|
213
|
+
def rec_role(
|
|
214
|
+
r, # A session record
|
|
215
|
+
):
|
|
216
|
+
"The conversational role of `r`: a user record carrying tool results counts as `tool`"
|
|
217
|
+
r = obj2dict(r)
|
|
218
|
+
c = r['message']['content']
|
|
219
|
+
if r['type']=='user' and isinstance(c, list) and any(b.get('type')=='tool_result' for b in c): return 'tool'
|
|
220
|
+
return r['type']
|
|
221
|
+
|
|
222
|
+
# %% ../nbs/03_ant.ipynb #defb1a03
|
|
223
|
+
def _preview(t, m, maxlen):
|
|
224
|
+
h = maxlen//2
|
|
225
|
+
s,e = max(0, m.start()-h), min(len(t), m.end()+h)
|
|
226
|
+
return ('…' if s else '') + t[s:e].replace('\n',' ') + ('…' if e<len(t) else '')
|
|
227
|
+
|
|
228
|
+
class SessHits(list):
|
|
229
|
+
"Search hits with a match-centered preview per line"
|
|
230
|
+
def __repr__(self): return '\n'.join(f"{h.i:5} {h.role:9} {h.ts[:16]} {h.preview}" for h in self)
|
|
231
|
+
|
|
232
|
+
def sess_search(
|
|
233
|
+
pat, # Regex to find in conversation text
|
|
234
|
+
sid=None, # Session id; the current session if None
|
|
235
|
+
cwd=None, # Project directory; the current directory if None
|
|
236
|
+
maxlen=160, # Preview characters shown around a hit's first match
|
|
237
|
+
):
|
|
238
|
+
"Search every conversation record of a session, returning `SessHits` with the records on `.recs`"
|
|
239
|
+
recs = conv_recs(load_sess(sid, cwd))
|
|
240
|
+
res = SessHits()
|
|
241
|
+
for i,r in enumerate(recs):
|
|
242
|
+
if m := re.search(pat, t:=rec_txt(r)): res.append(AttrDict(i=i, role=rec_role(r), ts=r['timestamp'], preview=_preview(t, m, maxlen), rec=r))
|
|
243
|
+
res.recs = recs
|
|
244
|
+
return res
|
|
245
|
+
|
|
246
|
+
# %% ../nbs/03_ant.ipynb #58f5cf47
|
|
247
|
+
def show_recs(
|
|
248
|
+
recs, # Session records, e.g. a slice of `SessHits.recs`
|
|
249
|
+
mx=500, # Characters of text shown per record
|
|
250
|
+
):
|
|
251
|
+
"A readable transcript of the conversation records in `recs`"
|
|
252
|
+
def _s(r):
|
|
253
|
+
t = rec_txt(r)
|
|
254
|
+
if len(t)>mx: t = t[:mx]+f'…[+{len(t)-mx} chars]'
|
|
255
|
+
return f"--- {rec_role(r)} {r['timestamp'][:19]} ---\n{t}"
|
|
256
|
+
return PrettyString('\n'.join(_s(r) for r in conv_recs(recs)))
|
|
257
|
+
|
|
258
|
+
# %% ../nbs/03_ant.ipynb #9e265e88
|
|
259
|
+
def strip_think(
|
|
260
|
+
recs, # Session records
|
|
261
|
+
):
|
|
262
|
+
"Drop records whose message content is only `thinking` blocks; resume does not need them"
|
|
263
|
+
def _keep(r):
|
|
264
|
+
c = r.get('message', {}).get('content', '')
|
|
265
|
+
return isinstance(c, str) or not all(b.get('type')=='thinking' for b in c)
|
|
266
|
+
return L(recs).filter(_keep)
|
|
267
|
+
|
|
268
|
+
# %% ../nbs/03_ant.ipynb #a56fc348
|
|
269
|
+
def _trunc_deep(o, mx):
|
|
270
|
+
if isinstance(o, str): return o if len(o)<=mx else o[:mx]+f'…[+{len(o)-mx} chars]'
|
|
271
|
+
if isinstance(o, dict): return {k:_trunc_deep(v, mx) for k,v in o.items()}
|
|
272
|
+
if is_listy(o): return [_trunc_deep(x, mx) for x in o]
|
|
273
|
+
return o
|
|
274
|
+
|
|
275
|
+
def trunc_tools(
|
|
276
|
+
recs, # Session records
|
|
277
|
+
mx=2000, # Maximum characters per string in tool inputs and results
|
|
278
|
+
):
|
|
279
|
+
"Copies of `recs` with strings in tool_use inputs and tool_result content truncated to `mx` characters"
|
|
280
|
+
recs = [obj2dict(r) for r in recs]
|
|
281
|
+
for r in recs:
|
|
282
|
+
c = r.get('message', {}).get('content', '')
|
|
283
|
+
if isinstance(c, str): continue
|
|
284
|
+
for b in c:
|
|
285
|
+
if b.get('type')=='tool_use': b['input'] = _trunc_deep(b.get('input', {}), mx)
|
|
286
|
+
elif b.get('type')=='tool_result' and 'content' in b: b['content'] = _trunc_deep(b['content'], mx)
|
|
287
|
+
return L(recs)
|
|
288
|
+
|
|
289
|
+
# %% ../nbs/03_ant.ipynb #7ffe1bb4
|
|
290
|
+
def reid_recs(
|
|
291
|
+
recs, # Records in conversation order
|
|
292
|
+
key='', # Salt: the same records and key give the same ids
|
|
293
|
+
ts=None, # If given, set every record's timestamp to this
|
|
294
|
+
):
|
|
295
|
+
"Deterministically re-derive record uuids, tool_use ids, and API metadata, so one capture gives one file"
|
|
296
|
+
recs,ids = [obj2dict(r) for r in recs],{}
|
|
297
|
+
for i,r in enumerate(recs):
|
|
298
|
+
c = r.get('message', {}).get('content', '')
|
|
299
|
+
if not isinstance(c, str):
|
|
300
|
+
for j,b in enumerate(c):
|
|
301
|
+
if b.get('type')=='tool_use':
|
|
302
|
+
nid = 'toolu_'+stable_uuid(f'{key}:{i}:{j}').replace('-', '')[:24]
|
|
303
|
+
ids[b['id']],b['id'] = nid,nid
|
|
304
|
+
elif b.get('type')=='tool_result' and b.get('tool_use_id') in ids: b['tool_use_id'] = ids[b['tool_use_id']]
|
|
305
|
+
uid = stable_uuid(f'{key}:{i}')
|
|
306
|
+
if 'uuid' in r: ids[r['uuid']] = uid
|
|
307
|
+
r['uuid'] = uid
|
|
308
|
+
if ts: r['timestamp'] = ts
|
|
309
|
+
if r.get('type')=='assistant':
|
|
310
|
+
r['requestId'] = 'req_'+stable_uuid(f'{uid}:req').replace('-', '')[:24]
|
|
311
|
+
r['message']['id'] = 'msg_'+stable_uuid(f'{uid}:msg').replace('-', '')[:24]
|
|
312
|
+
for r in recs:
|
|
313
|
+
for k,v in r.items():
|
|
314
|
+
if isinstance(v, str) and v in ids: r[k] = ids[v]
|
|
315
|
+
return L(recs)
|
|
316
|
+
|
|
317
|
+
# %% ../nbs/03_ant.ipynb #6db09ecc
|
|
318
|
+
def fork_sess(
|
|
319
|
+
sid=None, # Session id to fork; `cur_sess()` if None
|
|
320
|
+
cwd=None, # Project directory; passed to `sess_file` via `load_sess`
|
|
321
|
+
mx=None, # If given, truncate tool input/output strings to `mx` characters
|
|
322
|
+
think=True, # Keep thinking records?
|
|
323
|
+
key=None, # If given, record and session ids re-derive deterministically from this salt
|
|
324
|
+
):
|
|
325
|
+
"Write a munged copy of session `sid` under a fresh id, returning the new id to resume"
|
|
326
|
+
recs = sess_thread(load_sess(sid, cwd))
|
|
327
|
+
if not think: recs = strip_think(recs)
|
|
328
|
+
if mx: recs = trunc_tools(recs, mx)
|
|
329
|
+
if key: recs = reid_recs(recs, key)
|
|
330
|
+
return save_sess(list(recs), stable_uuid(key) if key else None, cwd)
|
|
331
|
+
|
|
332
|
+
# %% ../nbs/03_ant.ipynb #7b8d57fa
|
|
333
|
+
def dlg2msgs(
|
|
334
|
+
dlg, # A `Dialog`, ending with a prompt
|
|
335
|
+
aim_info=None, # Model capability dict for media handling; images enabled if None
|
|
336
|
+
):
|
|
337
|
+
"Anthropic-style messages for `dlg`, with each reply's tool calls recovered as real blocks"
|
|
338
|
+
return denorm_msgs(dlg2canon(dlg, aim_info))
|
|
339
|
+
|
|
340
|
+
def dlg2sess(
|
|
341
|
+
dlg, # The dialog to convert
|
|
342
|
+
cwd=None, # Project directory for the session; the current directory if None
|
|
343
|
+
key='dlg2sess', # Salt for deterministic record ids
|
|
344
|
+
aim_info=None, # Model capability dict; images enabled if None
|
|
345
|
+
):
|
|
346
|
+
"Write `dlg` as a Claude Code session for the project at `cwd`, returning the session id to resume"
|
|
347
|
+
recs = msgs2recs(dlg2msgs(dlg, aim_info), key=key, cwd=str(Path(cwd or '.').resolve()))
|
|
348
|
+
return save_sess(recs, stable_uuid(f'{key}:{dlg.name}'), cwd)
|
|
349
|
+
|
|
350
|
+
# %% ../nbs/03_ant.ipynb #16af5ac6
|
|
351
|
+
def _tr_txt(b):
|
|
352
|
+
c = b.get('content', '')
|
|
353
|
+
if isinstance(c, str): return c
|
|
354
|
+
if any(x.get('type') not in ('text','tool_reference') for x in c): raise ValueError('unsupported tool_result block')
|
|
355
|
+
return '\n'.join(x['text'] if x['type']=='text' else f"<tool_reference tool=\"{x['tool_name']}\"/>" for x in c)
|
|
356
|
+
|
|
357
|
+
def _norm_user(content):
|
|
358
|
+
if isinstance(content, str): return mk_msg(content)
|
|
359
|
+
items = []
|
|
360
|
+
for b in content:
|
|
361
|
+
if b['type']=='text': items.append(b['text'])
|
|
362
|
+
elif b['type']=='image': items.append(base64.b64decode(b['source']['data']))
|
|
363
|
+
else: raise ValueError(f"unsupported user block: {b['type']}")
|
|
364
|
+
return mk_msg(items)
|
|
365
|
+
|
|
366
|
+
def recs2msgs(
|
|
367
|
+
recs, # Session records, e.g. from `load_sess`
|
|
368
|
+
):
|
|
369
|
+
"Canonical fastllm messages for the conversation records in `recs`"
|
|
370
|
+
msgs,names = [],{}
|
|
371
|
+
for r in recs:
|
|
372
|
+
r = obj2dict(r)
|
|
373
|
+
if r.get('type') not in ('user','assistant'): continue
|
|
374
|
+
c = r['message']['content']
|
|
375
|
+
if isinstance(c, str) and r['type']=='assistant': c = [dict(type='text', text=c)]
|
|
376
|
+
if r['type']=='assistant':
|
|
377
|
+
parts = []
|
|
378
|
+
for b in c:
|
|
379
|
+
if b['type']=='text': parts.append(Part(type=PartType.text, text=b['text']))
|
|
380
|
+
elif b['type']=='thinking': parts.append(Part(type=PartType.thinking, text=b.get('thinking','')))
|
|
381
|
+
elif b['type']=='tool_use':
|
|
382
|
+
names[b['id']] = b['name']
|
|
383
|
+
parts.append(Part(type=PartType.tool_use, data=dict(id=b['id'], name=b['name'], arguments=b.get('input', {}))))
|
|
384
|
+
else: raise ValueError(f"unsupported assistant block: {b['type']}")
|
|
385
|
+
msgs.append(Msg(role='assistant', content=parts))
|
|
386
|
+
elif rec_role(r)=='tool':
|
|
387
|
+
if not all(b.get('type')=='tool_result' for b in c): raise ValueError('record mixes tool_result with other blocks')
|
|
388
|
+
msgs.append(Msg(role='tool', content=[Part(type=PartType.tool_result, text=_tr_txt(b),
|
|
389
|
+
data=dict(id=b['tool_use_id'], name=names.get(b['tool_use_id']))) for b in c]))
|
|
390
|
+
else: msgs.append(_norm_user(c))
|
|
391
|
+
return msgs
|
|
392
|
+
|
|
393
|
+
# %% ../nbs/03_ant.ipynb #ac13833a
|
|
394
|
+
def msgs2dlg(
|
|
395
|
+
msgs, # Canonical messages, e.g. from `recs2msgs`
|
|
396
|
+
name, # Dialog name
|
|
397
|
+
cls=Dialog, # Dialog class to create
|
|
398
|
+
mx=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
|
|
399
|
+
):
|
|
400
|
+
"A dialog for `msgs`: one prompt per user turn, replies rendered in the format `fmt2hist` parses"
|
|
401
|
+
dlg,turns = cls(name),[]
|
|
402
|
+
for m in msgs:
|
|
403
|
+
if m.role=='user': turns.append((m,[]))
|
|
404
|
+
elif turns: turns[-1][1].append(m)
|
|
405
|
+
else: raise ValueError('msgs must start with a user message')
|
|
406
|
+
for u,rs in turns:
|
|
407
|
+
segs,atts = [],[]
|
|
408
|
+
for p in u.content:
|
|
409
|
+
if p.type==PartType.text: segs.append(p.text)
|
|
410
|
+
elif p.type==PartType.input_image:
|
|
411
|
+
mime,data = data_url(p.text)
|
|
412
|
+
atts.append(Attachment(base64.b64decode(data), mime))
|
|
413
|
+
segs.append(f'')
|
|
414
|
+
else: raise ValueError(f'unsupported user part: {p.type}')
|
|
415
|
+
dlg.mk_message('\n\n'.join(segs), msg_type=sprompt, output=hist2fmt(rs, mx=mx), attachments=atts)
|
|
416
|
+
return dlg
|
|
417
|
+
|
|
418
|
+
# %% ../nbs/03_ant.ipynb #abc4a08a
|
|
419
|
+
def sess2dlg(
|
|
420
|
+
sid=None, # Session id; `cur_sess()` if None
|
|
421
|
+
cwd=None, # Project directory; passed to `sess_file` via `load_sess`
|
|
422
|
+
name=None, # Dialog name; the session id if None
|
|
423
|
+
mx=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
|
|
424
|
+
):
|
|
425
|
+
"The conversation of session `sid` as a dialog, one prompt per user turn"
|
|
426
|
+
recs = strip_think(sess_thread(load_sess(sid, cwd)))
|
|
427
|
+
return msgs2dlg(recs2msgs(recs), name or sid or cur_sess(), mx=mx)
|