from dialoghelper import *dialoghelper
from fastcore import tools
from fastcore.test import *Helpers
add_styles
def add_styles(
s:str, cls_map:dict=None
):
Add solveit styles to s
import mistletoe
from fasthtml.common import shows = mistletoe.markdown("### hi\n\n- first\n- *second*")
s'<h3>hi</h3>\n<ul>\n<li>first</li>\n<li><em>second</em></li>\n</ul>\n'
show(s)hi
- first
- second
show(add_styles(s))hi
- first
- second
Basics
a = 1
find_var('a')1
find_dname
def find_dname(
dname:NoneType=None
):
*Get the dialog name by searching the call stack for __dialog_name, and resolving dname if supplied.*
find_dname()'/aai-ws/dialoghelper/nbs/00_core'
find_dname('index')'/aai-ws/dialoghelper/nbs/index'
find_dname('../index')'/aai-ws/dialoghelper/index'
find_dname('/foo/bar')'/foo/bar'
xgeta
async def xgeta(
url, kwargs:VAR_KEYWORD
):
xposta
async def xposta(
url, kwargs:VAR_KEYWORD
):
call_endpa
async def call_endpa(
path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, data:VAR_KEYWORD
):
call_endp
def call_endp(
path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, data:VAR_KEYWORD
):
curr_dialog
async def curr_dialog(
with_messages:bool=False, # Include messages as well?
dname:str='', # Dialog to get info for; defaults to current dialog
):
Get the current dialog info.
msg_idx
async def msg_idx(
id:str=None, # Message id to find (defaults to current message)
dname:str='', # Dialog to get message index from; defaults to current dialog
):
Get absolute index of message in dialog.
await msg_idx()27
add_html
def add_html(
content:str, # The HTML to send to the client (generally should include hx-swap-oob)
dname:str='', # Dialog to get info for; defaults to current dialog
):
Send HTML to the browser to be swapped into the DOM
add_html_a
async def add_html_a(
content:str, # The HTML to send to the client (generally should include hx-swap-oob)
dname:str='', # Dialog to get info for; defaults to current dialog
):
Send HTML to the browser to be swapped into the DOM
from fasthtml.common import *add_html(Div(P('Hi'), hx_swap_oob='beforeend:#dialog-container')){'success': 'Content added to DOM'}
add_scr
def add_scr(
scr, oob:str='innerHTML:#ephemeral'
):
Swap a script element to the end of the ephemeral element
add_scr_a
async def add_scr_a(
scr, oob:str='innerHTML:#ephemeral'
):
Swap a script element to the end of the ephemeral element
iife
def iife(
code:str
):
Wrap javascript code string in an IIFE and execute it via add_html
iife_a
async def iife_a(
code:str
):
Wrap javascript code string in an IIFE and execute it via add_html
pop_data
def pop_data(
idx, timeout:int=15
):
pop_data_a
async def pop_data_a(
idx, timeout:int=15
):
fire_event
def fire_event(
evt:str, data:NoneType=None
):
fire_event_a
async def fire_event_a(
evt:str, data:NoneType=None
):
event_get
def event_get(
evt:str, timeout:int=15, data:NoneType=None
):
Call fire_event and then pop_data to get a response
event_get_a
async def event_get_a(
evt:str, timeout:int=15, data:NoneType=None
):
Call fire_event and then pop_data to get a response
trigger_now
def trigger_now(
evt, data:NoneType=None, ttl:int=5000
):
Synchronously trigger a browser event, safe against replay
display_response
def display_response(
display:str, result:str=None
):
Return a special response where display is added as markdown/HTML to the prompt output, and result is returned to the LLM
Run python
set_pyrun
def set_pyrun(
rp:RunPython
):
Replace the default RunPython used by msg_pyrun
await pyrun('[]')[]
await pyrun('Path().exists()')True
def f(): warnings.warn('a warning')
allow('f')
await pyrun('print("asdf"); f(); 1+1'){'stdout': 'asdf\n',
'stderr': "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_80279/3774884187.py:1: UserWarning: a warning\n def f(): warnings.warn('a warning')\n",
'result': 2}
doc
def doc(
sym
)->str:
Get documentation (signature, docstring, + docments if they exist) for sym. NB: This is not an llm tool, so must be run with python(). sym must be available in the namespace.
View/edit dialog
read_msg
async def read_msg(
n:int=-1, # Message index (if relative, +ve is downwards)
relative:bool=True, # Is `n` relative to current message (True) or absolute (False)?
id:str=None, # Message id to find (defaults to current message)
view_range:list=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF
nums:bool=False, # Whether to show line numbers
dname:str='', # Dialog to get info for; defaults to current dialog
):
Get the message indexed in the current dialog. NB: Messages in the current dialog above the current message are already visible; use this only when you need line numbers for editing operations, or for messages not in the current dialog or below the current message. - To get the exact message use n=0 and relative=True together with id. - To get a relative message use n (relative position index). - To get the nth message use n with relative=False, e.g n=0 first message, n=-1 last message. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree.
find_msgs
async def find_msgs(
re_pattern:str='', # Optional regex to search for (re.DOTALL+re.MULTILINE is used)
msg_type:str=None, # optional limit by message type ('code', 'note', or 'prompt')
use_case:bool=False, # Use case-sensitive matching?
use_regex:bool=True, # Use regex matching?
only_err:bool=False, # Only return messages that have errors?
only_exp:bool=False, # Only return messages that are exported?
only_chg:bool=False, # Only return messages that have changed vs git HEAD?
ids:str='', # Optionally filter by comma-separated list of message ids
limit:int=None, # Optionally limit number of returned items
include_output:bool=True, # Include output in returned dict?
include_meta:bool=True, # Include all additional message metadata
as_xml:bool=False, # Use concise unescaped XML output format
nums:bool=False, # Show line numbers?
trunc_out:bool=False, # Middle-out truncate code output to 100 characters?
trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?
headers_only:bool=False, # Only return note messages that are headers (first line only); cannot be used together with `header_section`
header_section:str=None, # Find section starting with this header; returns it plus all children (i.e until next header of equal or more significant level)
dname:str='', # Dialog to get info for; defaults to current dialog
)->list: # Messages in requested dialog that contain the given information
Often it is more efficient to call view_dlg to see the whole dialog at once, so you can use it all from then on, instead of using find_msgs. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree. Message ids are identical to those in LLM chat history, so do NOT call this to view a specific message if it’s in the chat history–instead use view_msg. Do NOT use find_msgs to view message content in the current dialog above the current prompt – these are already provided in LLM context, so just read the content there directly. (NB: LLM context only includes messages above the current prompt, whereas find_msgs can access all messages.) To refer to a found message from code or tools, use its id field.
# NB: must have a dialogue open including a message with this text in its content
txt = 'tools'
found = await find_msgs(txt)1+12
r = await find_msgs(r'1\+1', include_meta=False, include_output=True)
r[{'id': '_9f0b2705', 'is_exported': 0, 'content': 'def f(): warnings.warn(\'a warning\')\nallow(\'f\')\nawait pyrun(\'print("asdf"); f(); 1+1\')', 'output': {'stdout': 'asdf\n', 'stderr': "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_80279/3774884187.py:1: UserWarning: a warning\n def f(): warnings.warn('a warning')\n", 'result': 2}, 'msg_type': 'code'}, {'id': '_8ce548d6', 'is_exported': 0, 'content': '1+1', 'output': 2, 'msg_type': 'code'}, {'id': '_44cb1b2a', 'is_exported': 0, 'content': "_id = await _add_msg_unsafe('1+1', run=True, msg_type='code')", 'output': '', 'msg_type': 'code'}]
hl_md(await find_msgs(r'1\+1', include_meta=False, as_xml=True))<msgs><code id="_9f0b2705"><source>def f(): warnings.warn('a warning')
allow('f')
await pyrun('print("asdf"); f(); 1+1')<out>{'stdout': 'asdf\n', 'stderr': "/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_80279/3774884187.py:1: UserWarning: a warning\n def f(): warnings.warn('a warning')\n", 'result': 2}</out></code><code id="_8ce548d6"><source>1+1<out>2</out></code><code id="_44cb1b2a">_id = await _add_msg_unsafe('1+1', run=True, msg_type='code')</code></msgs>view_dlg
async def view_dlg(
dname:str='', # Dialog to get info for; defaults to current dialog
msg_type:str=None, # optional limit by message type ('code', 'note', or 'prompt')
nums:bool=False, # Whether to show line numbers
include_output:bool=False, # Include output in returned dict?
trunc_out:bool=True, # Middle-out truncate code output to 100 characters (only applies if `include_output`)?
trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?
):
Concise XML view of all messages (optionally filtered by type), not including metadata. Often it is more efficient to call this to see the whole dialog at once (including line numbers if needed), instead of running find_msgs or view_msg multiple times.
hl_md((await view_dlg(nums=True))[:500])add_msg
async def add_msg(
content:str, # Content of the message (i.e the message prompt, code, or note text)
placement:str='add_after', # Can be 'at_start' or 'at_end', and for default dname can also be 'add_after' or 'add_before'
id:str=None, # id of message that placement is relative to (if None, uses current message)
dname:str='', # Dialog to get info for; defaults to current dialog (`run` only has a effect if dialog is currently running)
msg_type:str='note', # Message type, can be 'code', 'note', or 'prompt'
output:str='', # Prompt/code output; Code outputs must be .ipynb-compatible JSON array
time_run:str | None='', # When was message executed
is_exported:int | None=0, # Export message to a module?
skipped:int | None=0, # Hide message from prompt?
i_collapsed:int | None=0, # Collapse input?
o_collapsed:int | None=0, # Collapse output?
heading_collapsed:int | None=0, # Collapse heading section?
pinned:int | None=0, # Pin to context?
)->str: # Message ID of newly created message
Add/update a message to the queue to show after code execution completes. NB: when creating multiple messages in a row, after the 1st message set id to the result of the last add_msg call, otherwise messages will appear in the dialog in REVERSE order. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree.
add_prompt
async def add_prompt(
content:str, # Prompt to run
dname:str=None, # Dialog to run prompt in; defaults to current dialog
msg_id:str=None, # Message id to place prompt after (if None, places at end)
wait:bool=True, # Wait for and return response?
poll:float=0.5, # Frequency of polling to check for completion
placement:str='', # Location to place message, defaults to 'at_end' if no msg_id
id:str=None, # id of message that placement is relative to (if None, uses current message)
output:str='', # Prompt/code output; Code outputs must be .ipynb-compatible JSON array
time_run:str | None='', # When was message executed
is_exported:int | None=0, # Export message to a module?
skipped:int | None=0, # Hide message from prompt?
i_collapsed:int | None=0, # Collapse input?
o_collapsed:int | None=0, # Collapse output?
heading_collapsed:int | None=0, # Collapse heading section?
pinned:int | None=0, # Pin to context?
): # Message ID of newly created message
Run a prompt and, if wait, wait for and return the response text
_id = await add_msg('testing')read_msgid
async def read_msgid(
id:str, # Message id to find
view_range:list=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF
nums:bool=False, # Whether to show line numbers
dname:str='', # Dialog to get message from; defaults to current dialog
add_to_dlg:bool=False, # Whether to add message content to current dialog (as a raw message)
):
Get message id. Message IDs can be view directly in LLM chat history/context, or found in find_msgs results. Use add_to_dlg if the LLM or human may need to refer to the message content again later. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree.
r = await read_msg(-2)
print((await read_msg(-2)).content)testing
read_msg (and all endpoints that return json) wrap responses in dict2obj, so you can use either dict or object syntax.
bmsg = await add_msg('at bottom', placement='at_end')assert(await msg_idx(bmsg)>await msg_idx(_id)+10)view_msg
async def view_msg(
id:str, # Message id to view
dname:str='', # Dialog to get message from; defaults to current dialog
nums:bool=True, # Whether to show line numbers
view_range:list=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF. Rarely needed--read whole message in nearly all cases instead
add_to_dlg:bool=False, # Whether to add message content to current dialog (as a raw message)
):
Views the content* of message id. Same as read_msgid(...)['content'], defaulting to nums=True. Use add_to_dlg if the LLM or human may need to refer to the message content again later. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not* include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree.
print((await view_msg(r.id))) 1 │ testing
# dh_settings['dname'] = 'tmp'
# _id = await add_msg('testing', placement='at_end')
# print(_id)
# del(dh_settings['dname'])del_msg
async def del_msg(
id:str=None, # id of message to delete
dname:str='', # Dialog to get info for; defaults to current dialog
log_changed:bool=False, # Add a note showing the deleted content?
):
Delete a message from the dialog. DO NOT USE THIS unless you have been explicitly instructed to delete messages.
await del_msg(bmsg)
await del_msg(_id){'status': 'success'}
_id = await _add_msg_unsafe('1+1', run=True, msg_type='code')await del_msg(_id){'status': 'success'}
_id = await _add_msg_unsafe('Hi', run=True, msg_type='prompt')await del_msg(_id){'status': 'success'}
update_msg
async def update_msg(
id:str=None, # id of message to update (if None, uses current message)
msg:Optional=None, # Dictionary of field keys/values to update
dname:str='', # Dialog to get info for; defaults to current dialog
log_changed:bool=False, # Add a note showing the diff?
content:str | None=None, # Content of the message (i.e the message prompt, code, or note text)
msg_type:str | None=None, # Message type, can be 'code', 'note', or 'prompt'
output:str | None=None, # Prompt/code output; Code outputs must be .ipynb-compatible JSON array
time_run:str | None=None, # When was message executed
is_exported:int | None=None, # Export message to a module?
skipped:int | None=None, # Hide message from prompt?
i_collapsed:int | None=None, # Collapse input?
o_collapsed:int | None=None, # Collapse output?
heading_collapsed:int | None=None, # Collapse heading section?
pinned:int | None=None, # Pin to context?
):
Update an existing message. Provide either msg OR field key/values to update. - Use content param to update contents. - Only include parameters to update–missing ones will be left unchanged. If dname is None, the current dialog is used. If it is an open dialog, it will be updated interactively with real-time updates to the browser. If it is a closed dialog, it will be updated on disk. Dialog names must be paths relative to solveit root (if starting with /, e.g. /myproject/dlg) or relative to the current dialog’s folder (if not starting with /), and should not include the .ipynb extension. Use absolute paths when targeting dialogs outside the current dialog’s folder tree.
_id = await add_msg('testing')_id = await update_msg(_id, content='toasting')_id = await update_msg(_id, skipped=1)msg = await read_msgid(_id)
msg['content'] = 'toasted'
await update_msg(msg=msg)'_87274a32'
await del_msg(_id){'status': 'success'}
_edit_id = await add_msg('This message should be found.\n\nThis is a multiline message.')
_edit_id'_0e06a95a'
print((await read_msg())['content'])This message should be found.
This is a multiline message.
print((await read_msg(n=0, id=_edit_id, nums=True))['content']) 1 │ This message should be found.
2 │
3 │ This is a multiline message.
print((await read_msg(n=0, id=_edit_id, nums=True, view_range=[2,3]))['content']) 2 │
3 │ This is a multiline message.
run_msg
async def run_msg(
ids:str=None, # Comma-separated ids of message(s) to execute
dname:str='', # Running dialog to get info for; defaults to current dialog. (Note dialog *must* be running for this function)
):
Adds a message to the run queue. Use read_msg to see the output once it runs.
codeid = (await read_msg())['id']await run_msg(codeid){'status': 'queued'}
copy_msg
async def copy_msg(
ids:str=None, # Comma-separated ids of message(s) to copy
cut:bool=False, # Cut message(s)? (If not, copies)
dname:str='', # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)
):
Add ids to clipboard.
paste_msg
async def paste_msg(
id:str=None, # Message id to paste next to
after:bool=True, # Paste after id? (If not, pastes before)
dname:str='', # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)
):
Paste clipboard msg(s) after/before the current selected msg (id).
await copy_msg(codeid){'success': 'complete'}
test_eq((await copy_msg('_fake', dname='/dlg/nonexistent'))['error'], 'Dialog /dlg/nonexistent may not be running, or message not found')tgt = (await read_msg())['id']await paste_msg(tgt){'success': 'complete'}
test_eq((await paste_msg('_fake', dname='/dlg/nonexistent'))['error'], 'Dialog /dlg/nonexistent may not be running, or message not found')newmsg = await read_msg(1, id=tgt)
newmsg['content']'async def run_msg(\n ids:str=None, # Comma-separated ids of message(s) to execute\n dname:str=\'\' # Running dialog to get info for; defaults to current dialog. (Note dialog *must* be running for this function)\n):\n "Adds a message to the run queue. Use read_msg to see the output once it runs."\n return await call_endpa(\'add_runq_\', dname, ids=ids, json=True)'
await del_msg(newmsg['id']){'status': 'success'}
enable_mermaid
def enable_mermaid(
):
enable_mermaid()mermaid
def mermaid(
code, cls:str='mermaid', kwargs:VAR_KEYWORD
):
A mermaid diagram
mermaid('graph LR; A[Start] --> B[Process]; B --> C[End];')You can also add to a note:
```mermaid
graph LR
A[Start] --> B[Process]
B --> C[End]
```
This renders as:
graph LR
A[Start] --> B[Process]
B --> C[End]
You can also add to a note:
```mermaid
graph LR
A[Start] --> B[Process]
B --> C[End]
```
This renders as:
graph LR
A[Start] --> B[Process]
B --> C[End]
toggle_header
async def toggle_header(
id:str, # id of markdown header note message to toggle collapsed state
dname:str='', # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)
):
Toggle collapsed header state for id
toggle_bookmark
async def toggle_bookmark(
id:str, # id of message to toggle bookmark on
n:int, # Bookmark number (1-9)
dname:str='', # Dialog to set bookmark in; defaults to current dialog
):
Toggle numbered bookmark (1-9) on a message, clearing it from any other message when setting
toggle_comment
async def toggle_comment(
id:str, # id of code message (or comma-separated ids) to toggle comments on
dname:str='', # Dialog to toggle comments in; defaults to current dialog. (Note dialog *must* be running for this function)
):
Toggle line comments on code message(s). If any lines are uncommented, comments all; otherwise uncomments all.
await toggle_comment(codeid) # comment
await toggle_comment(codeid) # uncomment{'success': 'complete'}
test_eq((await toggle_comment('_fake', dname='/dlg/nonexistent'))['error'], 'Dialog /dlg/nonexistent may not be running, or message not found')test header
header end
hdid = (await read_msg())['id']await toggle_header(hdid){'success': 'complete'}
test_eq((await toggle_header('_fake', dname='/dlg/nonexistent'))['error'], 'Dialog /dlg/nonexistent may not be running, or message not found')Dlg conveniences
url2note
async def url2note(
url:str, # URL to read
extract_section:bool=True, # If url has an anchor, return only that section
selector:str=None, # Select section(s) using BeautifulSoup.select (overrides extract_section)
ai_img:bool=True, # Make images visible to the AI
split_re:str='', # Regex to split content into multiple notes, set to '' for single note
):
Read URL as markdown, and add note(s) below current message with the result
_id = await url2note('https://docs.python.org')await del_msg(_id){'status': 'success'}
create_or_run_dialog
async def create_or_run_dialog(
name:str, # Name/path of the dialog (relative to current dialog's folder, or absolute if starts with '/')
):
Create a new dialog, or set an existing one running
await create_or_run_dialog('test_dialog'){'success': '"aai-ws/dialoghelper/nbs/test_dialog" is now running'}
stop_dialog
async def stop_dialog(
name:str, # Name/path of the dialog (relative to current dialog's folder, or absolute if starts with '/')
):
Stop a running dialog kernel
await stop_dialog('test_dialog'){'success': 'dialog stopped'}
rm_dialog
async def rm_dialog(
name:str, # Name/path of the dialog to delete (relative to current dialog's folder, or absolute if starts with '/')
):
Delete a dialog (or folder) and associated records, stopping the kernel if running
await rm_dialog('test_dialog'){'success': 'deleted "/Users/jhoward/aai-ws/dialoghelper/nbs/test_dialog"'}
run_code_interactive
async def run_code_interactive(
code:str, # Code to have user run
):
Insert code into user’s dialog and request for the user to run it. Use other tools where possible, but if they can not find needed information, ALWAYS use this instead of guessing or giving up. IMPORTANT: This tool is TERMINAL - after calling it, you MUST stop all tool usage and wait for user response. Never call additional tools after this one.
Text Edit
await msg_insert_line(_edit_id, 0, 'This should go to the first line')
await msg_insert_line(_edit_id, 3, 'This should go to the 4th line')
print(await msg_insert_line(_edit_id, 5, 'This should go to the last line'))@@ -5 +5,2 @@
This is a multiline message.
+This should go to the last line
print((await read_msg(n=0, id=_edit_id, nums=True))['content']) 1 │ This should go to the first line
2 │ This message should be found.
3 │
4 │ This should go to the 4th line
5 │ This is a multiline message.
6 │ This should go to the last line
print(await msg_str_replace(_edit_id, 'This should go to the first line', 'This should go to the 1st line'))@@ -1,2 +1,2 @@
-This should go to the first line
+This should go to the 1st line
This message should be found.
print((await read_msg(n=0, id=_edit_id, nums=True))['content']) 1 │ This should go to the 1st line
2 │ This message should be found.
3 │
4 │ This should go to the 4th line
5 │ This is a multiline message.
6 │ This should go to the last line
print(await msg_strs_replace(_edit_id, ['This is a multiline message.', 'This should go to the last line'], ['5th line', 'last line']))@@ -4,3 +4,3 @@
This should go to the 4th line
-This is a multiline message.
-This should go to the last line
+5th line
+last line
print((await read_msg(n=0, id=_edit_id, nums=True))['content']) 1 │ This should go to the 1st line
2 │ This message should be found.
3 │
4 │ This should go to the 4th line
5 │ 5th line
6 │ last line
print(await msg_replace_lines(_edit_id, 2, 4,'line 2\nline 3\nline 4\n'))@@ -1,5 +1,5 @@
This should go to the 1st line
-This message should be found.
-
-This should go to the 4th line
+line 2
+line 3
+line 4
5th line
print((await read_msg(n=0, id=_edit_id, nums=True))['content']) 1 │ This should go to the 1st line
2 │ line 2
3 │ line 3
4 │ line 4
5 │ 5th line
6 │ last line
print(await msg_del_lines(_edit_id, 2, 4))@@ -1,5 +1,2 @@
This should go to the 1st line
-line 2
-line 3
-line 4
5th line
print((await read_msg(n=0, id=_edit_id, nums=True)).content) 1 │ This should go to the 1st line
2 │ 5th line
3 │ last line
print(await msg_pyrun(_edit_id, r"'\n'.join(sorted(text.splitlines()))"))@@ -1,3 +1,3 @@
+5th line
This should go to the 1st line
-5th line
last line
print((await read_msg(n=0, id=_edit_id, nums=True)).content) 1 │ 5th line
2 │ This should go to the 1st line
3 │ last line
await del_msg(_edit_id){'status': 'success'}
with TemporaryDirectory() as tmp:
p = f'{tmp}/test.txt'
Path(p).write_text('alpha\nbeta\ngamma\ndelta\n')
print(await file_insert_line(p, 0, 'first'))
print(await file_str_replace(p, 'beta', 'BETA'))
print(await file_strs_replace(p, ['gamma', 'delta'], ['GAMMA', 'DELTA']))
print(await file_replace_lines(p, 2, 3, 'two\nthree\n'))
print(await file_del_lines(p, 1))
print(await file_pyrun(p, r"'\n'.join(reversed(text.splitlines()))"))
print('---')
print(Path(p).read_text())@@ -1 +1,2 @@
+first
alpha
@@ -2,3 +2,3 @@
alpha
-beta
+BETA
gamma
@@ -3,3 +3,3 @@
BETA
-gamma
-delta
+GAMMA
+DELTA
@@ -1,4 +1,4 @@
first
-alpha
-BETA
+two
+three
GAMMA
@@ -1,2 +1 @@
-first
two
@@ -1,4 +1,4 @@
+DELTA
+GAMMA
+three
two
-three
-GAMMA
-DELTA
---
DELTA
GAMMA
three
two
id1 = await add_msg('hello world')
id2 = await add_msg('hello there', id=id1)results = await msg_str_replace([id1, id2], 'hello', 'hi')
print(results)[('_5d7693c2', '@@ -1 +1 @@\n-hello world\n+hi world'), ('_887f10e3', '@@ -1 +1 @@\n-hello there\n+hi there')]
await del_msg(id1)
await del_msg(id2){'status': 'success'}
ast-grep
ast_py
def ast_py(
code:str
):
Get an SgRoot root node for python code
node = ast_py("print('hello world')")
stmt = node.find(pattern="print($A)")
res = stmt.get_match('A')
res.text(),res.range()("'hello world'",
Range(start=Pos(line=0, col=6, index=6), end=Pos(line=0, col=19, index=19)))
ast_grep
def ast_grep(
pattern:str, # ast-grep pattern to search, e.g "post($A, data=$B, $$$)"
path:str='.', # path to recursively search for files
lang:str='python', # language to search/scan
):
Use ast-grep to find code patterns by AST structure (not text).
Pattern syntax: - $VAR captures single nodes, \[$ captures multiple
- Match structure directly: `def $FUNC(\]$)finds any function;class $CLASSfinds classes regardless of inheritance - DON'T include:` - it’s concrete syntax, not AST structure - Whitespace/formatting ignored - matches structural equivalence
Examples: import $MODULE (find imports); $OBJ.$METHOD($$$) (find method calls); await $EXPR (find await expressions)
Useful for: Refactoring—find all uses of deprecated APIs or changed signatures; Security review—locate SQL queries, file operations, eval calls; Code exploration—understand how libraries are used across codebase; Pattern analysis—find async functions, error handlers, decorators; Better than regex—handles multi-line code, nested structures, respects syntax
The ast_grep function calls the ast-grep CLI, which is a tool for searching code based on its structure rather than just text patterns. Unlike regular expressions that match character sequences, ast-grep understands the syntax of programming languages and lets you search for code patterns in a way that respects the language’s grammar. This means you can find function calls, variable assignments, or other code constructs even when they’re formatted differently or have varying amounts of whitespace.
The key advantage is using metavariables (like $A, $B, $$$) as placeholders in your search patterns. When you search for xpost($A, data=$B, $$$), you’re asking to find all calls to xpost where the first argument can be anything (captured as $A), there’s a keyword argument data with any value (captured as $B), and there may be additional arguments after that (the $$$ matches zero or more remaining arguments). This is much more reliable than trying to write a regex that handles all the variations of how that function might be called.
In the example below, we search for calls to xpost in the parent directory and extract both the matched code and the specific values of our metavariables, showing us exactly where and how this function is being used in the codebase.
res = ast_grep(r"xpost($A, data=$B, $$$)", '..')
[(o['text'],o['metaVariables']['single'],o['file']) for o in res][('xpost(url, data=data, headers=headers)',
{'B': {'text': 'data',
'range': {'byteOffset': {'start': 5567, 'end': 5571},
'start': {'line': 112, 'column': 40},
'end': {'line': 112, 'column': 44}}},
'A': {'text': 'url',
'range': {'byteOffset': {'start': 5557, 'end': 5560},
'start': {'line': 112, 'column': 30},
'end': {'line': 112, 'column': 33}}}},
'dialoghelper/core.py')]
Basic Patterns: - Match code structure directly: console.log($ARG) - Metavariables capture parts: $VAR (single), $$$ (multiple) - Patterns match AST structure, not text - whitespace/formatting doesn’t matter
The Colon Issue: - Don’t include : in patterns - it’s part of Python’s concrete syntax, not the AST structure - ✅ def $FUNC($$$) - matches function definitions - ❌ def $FUNC($$$): - too specific, looking for the colon token itself
When to use kind vs pattern: - pattern: Simple direct matches (await $EXPR) - kind: Structural node types (kind: function_declaration)
Critical rule for relational searches: Always add stopBy: end to has/inside rules to search the entire subtree:
has:
pattern: await $EXPR
stopBy: endEscaping in shell: Use \$VAR or single quotes when using --inline-rules from command line
_ast_id = await add_msg("print('hello')\nprint('world')\nlog('keep')", msg_type='code')print(await msg_ast_replace(_ast_id, 'print($A)', 'logger.info($A)'))@@ -1,3 +1,3 @@
-print('hello')
-print('world')
+logger.info('hello')
+logger.info('world')
log('keep')
print((await read_msg(n=0, id=_ast_id, nums=True))['content']) 1 │ logger.info('hello')
2 │ logger.info('world')
3 │ log('keep')
await del_msg(_ast_id){'status': 'success'}
Context
ctx_folder
async def ctx_folder(
path:Path='.', # Path to collect
types:str | list='py,doc', # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
out:bool=False, # Include notebook cell outputs?
raw:bool=True, # Add raw message, or note?
exts:str | list=None, # list or comma-separated str of exts to include (overrides `types`)
prefix:bool=False, # Include Anthropic's suggested prose intro?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
symlinks:bool=True, # follow symlinks?
file_glob:str=None, # Only include files matching glob
file_re:str=None, # Only include files matching regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
skip_file_re:str=None, # Skip files matching regex
skip_folder_re:str=None, # Skip folders matching regex,
ret_folders:bool=False, # return folders, not just files
sort:bool=True, # sort files by name within each folder
):
Convert folder to XML context and place in a new message
# ctx_folder('..', max_total=600, sigs_only=True, exts='py')def start_share(): fire_event(‘shareScreen’)
def _capture_screen(timeout=15):
def capture_screen(timeout=15): “Capture the screen as a PIL image.”
def capture_tool(timeout:int=15): “Capture the screen. Re-call this function to get the most recent screenshot, as needed. Use default timeout where possible”
[TRUNCATED: output size 24344 exceeded max size 600 bytes]
ctx_repo
async def ctx_repo(
owner:str, # GitHub repo owner
repo:str, # GitHub repo name
types:str | list='py,doc', # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
exts:str | list=None, # list or comma-separated str of exts to include (overrides `types`)
out:bool=False, # Include notebook cell outputs?
raw:bool=True, # Add raw message, or note?
ref:str=None, # Git ref (branch/tag/sha) (get from URL not provided); defaults to repo's default branch
folder:str=None, # Only include files under this path (get from URL not provided)
show_filters:bool=True, # Include filter info in title?
token:str=None, # GitHub token (uses GITHUB_TOKEN env var if None)
prefix:bool=False, # Include Anthropic's suggested prose intro?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
symlinks:bool=True, # follow symlinks?
file_glob:str=None, # Only include files matching glob
file_re:str=None, # Only include files matching regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
skip_file_re:str=None, # Skip files matching regex
skip_folder_re:str=None, # Skip folders matching regex,
ret_folders:bool=False, # return folders, not just files
sort:bool=True, # sort files by name within each folder
): # XML for LM context, or dict of file sizes
Convert GitHub repo to XML context and place in a new message
ctx_symfile
async def ctx_symfile(
sym
):
Add note with filepath and contents for a symbol’s source file
# ctx_symfile(TemporaryDirectory)ctx_symfolder
async def ctx_symfolder(
sym, # Symbol to get folder context from
types:str | list='py', # List or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
skip_file_re:str='^_mod', # Skip files matching regex
prefix:bool=False, # Include Anthropic's suggested prose intro?
out:bool=True, # Include notebook cell outputs?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
symlinks:bool=True, # follow symlinks?
file_glob:str=None, # Only include files matching glob
file_re:str=None, # Only include files matching regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
skip_folder_re:str=None, # Skip folders matching regex,
ret_folders:bool=False, # return folders, not just files
sort:bool=True, # sort files by name within each folder
exts:str | list=None, # list or comma-separated str of exts to include
):
Add raw message with folder context for a symbol’s source file location
# ctx_symfolder(folder2ctx)ctx_sympkg
async def ctx_sympkg(
sym, # Symbol to get folder context from
types:str | list='py', # List or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
skip_file_re:str='^_mod', # Skip files matching regex
prefix:bool=False, # Include Anthropic's suggested prose intro?
out:bool=True, # Include notebook cell outputs?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
symlinks:bool=True, # follow symlinks?
file_glob:str=None, # Only include files matching glob
file_re:str=None, # Only include files matching regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
skip_folder_re:str=None, # Skip folders matching regex,
ret_folders:bool=False, # return folders, not just files
sort:bool=True, # sort files by name within each folder
exts:str | list=None, # list or comma-separated str of exts to include
):
Add raw message with repo context for a symbol’s root package
# ctx_sympkg(folder2ctx)Gists and github
load_gist
def load_gist(
gist_id:str
):
Retrieve a gist
gistid = 'jph00/e7cfd4ded593e8ef6217e78a0131960c'
gist = load_gist(gistid)
gist.html_url'https://gist.github.com/jph00/e7cfd4ded593e8ef6217e78a0131960c'
gist_file
def gist_file(
gist_id:str
):
Get the first file from a gist
gfile = gist_file(gistid)
print(gfile.content[:100]+"…")"This is a test module which makes some simple tools available."
__all__ = ["hi","whoami"]
testfoo=…
import_string
def import_string(
code:str, # Code to import as a module
name:str, # Name of module to create
):
def hi(who:str):
"Say hi to `who`"
return f"Hello {who}"
def hi2(who):
"Say hi to `who`"
return f"Hello {who}"
def hi3(who:str):
return f"Hello {who}"
bye = "bye"assert is_usable_tool(hi)
assert not is_usable_tool(hi2)
assert not is_usable_tool(hi3)
assert not is_usable_tool(bye)mk_toollist
def mk_toollist(
syms
):
print(mk_toollist([hi]))- &`hi`: Say hi to `who`
import_gist
def import_gist(
gist_id:str, # user/id or just id of gist to import as a module
mod_name:str=None, # module name to create (taken from gist filename if not passed)
add_global:bool=True, # add module to caller's globals?
import_wildcard:bool=False, # import all exported symbols to caller's globals
create_msg:bool=False, # Add a message that lists usable tools
):
Import gist directly from string without saving to disk
import_gist(gistid)
importtest.testfoo'testbar'
import_gist.__doc__'Import gist directly from string without saving to disk'
import_gist(gistid, import_wildcard=True)
importtest.testfoo'testbar'
hi("Sarah")'Hello Sarah'
importtest.__all__['hi', 'whoami']
update_gist
def update_gist(
gist_id:str, content:str
):
Update the first file in a gist with new content
read_pr
def read_pr(
pr_number:int, # Issue or PR number
owner:str='answerdotai', # Owner
repo:str='solveit', # Repo
folder:str='', # For diffs, limit to only to files in `folder`
replies:bool=False, # Include replies
):
Fetch a GitHub PR or issue’s title, body, optionally replies, and diff (if PR)
Help
dialoghelper_explain_dialog_editing
def dialoghelper_explain_dialog_editing(
)->str: # Detailed documention on dialoghelper dialog editing
Call this to get a detailed explanation of how dialog editing is done in dialoghelper. ALWAYS call this first, if dialog editing has not previously occured in this session
solveit_docs
def solveit_docs(
):
Full reference documentation for Solveit - use this to answer questions about how to use Solveit. NB: The whole docs fit in LLM context, so read the whole thing, don’t search/filter it. Always re-run rather than relying on truncated history or assumptions.
dialog_link
def dialog_link(
path:str='', # Path to dialog (e.g. '/aai-ws/dialoghelper/nbs/00_core'), defaults to current dialog
msg_id:str=None, # Optional message id to scroll to
):
Return an IPython HTML link to open a dialog in Solveit. After calling this tool, output the resulting HTML anchor tag exactly as returned—do not wrap in a fenced code block or convert to markdown link format.
dialog_link(msg_id='_a7d82acd')dialog_link(path='/CRAFT')dialog_link(path='/CRAFT', msg_id='_ce727fd8')spawn_agent
async def spawn_agent(
prompt:str
):
Spawn a subagent to complete a task defined by prompt. Must be run as a tool - not from Python. The subagent’s context and tools is defined by the parent prompt’s history
Input
input
def input(
prompt:str='', args:VAR_POSITIONAL
):
Solveit customised input to handle fasttag prompts
InputBtn
def InputBtn(
txt, value:NoneType=None, btncls:tuple=(), kw:VAR_KEYWORD
):
def show_prompt():
return input(
Div('Ship this change now?'),
Div(cls='flex gap-2')(
InputBtn('Yes', btncls='primary', accesskey="y"),
InputBtn('No', btncls='default', accesskey="n")
))# show_prompt()