from dialoghelper import *dialoghelper
from fastcore import toolsHelpers
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
set_var
def set_var(
var:str, val
):
Set var to val after finding it in all frames of the call stack
find_var
def find_var(
var:str
):
Search for var in all frames of the call stack
a = 1
find_var('a')1
set_var('a', 42)
a42
find_msg_id
def find_msg_id(
):
*Get the message id by searching the call stack for __msg_id.*
find_dname
def find_dname(
dname:NoneType=None
):
*Get the dialog name by searching the call stack for __dialog_id, 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
def xgeta(
url, kwargs:VAR_KEYWORD
):
xposta
def xposta(
url, kwargs:VAR_KEYWORD
):
call_endpa
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
):
find_msg_id()'_9cbd170d'
curr_dialog
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
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()28
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
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
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
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
def pop_data_a(
idx, timeout:int=15
):
fire_event
def fire_event(
evt:str, data:NoneType=None
):
fire_event_a
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
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
allow
def allow(
c:VAR_POSITIONAL
):
RunPython
def RunPython(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Initialize self. See help(type(self)) for accurate signature.
def f(): warnings.warn('a warning')
allow('f')
await python('print("asdf"); f(); 1+1'){'stdout': 'asdf\n', 'warnings': 'UserWarning: a warning', 'result': 2}
await python('[]'){'result': []}
safe_type
def safe_type(
o:object
):
Same as type(o)
# print(MarkdownRenderer(add_msg)._repr_markdown_())docs
def docs(
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.
await python('''
a = {"b":1}
list(a.items())
'''){'result': [('b', 1)]}
await python('Path().exists()'){'result': True}
await python("os.path.join('/foo', 'bar', 'baz.py')"){'result': '/foo/bar/baz.py'}
await python('a_=3')
a_3
await python('''aa_='33' ''')
await python('''len(aa_) '''){'result': 2}
await python('''
dict(a=safe_type(1))
'''){'result': {'a': int}}
await python("""
async def agen():
for x in [1,2]: yield x
res = []
async for x in agen(): res.append(x)
res
"""){'result': [1, 2]}
await python('''
import asyncio
async def fetch(n): return n * 10
print(string.ascii_letters)
await asyncio.gather(fetch(1), fetch(2), fetch(3))
'''){'stdout': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\n',
'result': [10, 20, 30]}
View/edit dialog
read_msg
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
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 read_msgid. 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': '_5a8db1de', 'is_exported': 0, 'content': 'def f(): warnings.warn(\'a warning\')\nallow(\'f\')\nawait python(\'print("asdf"); f(); 1+1\')', 'output': '{'stdout': 'asdf\\n', 'warnings': 'UserWarning: a warning', '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': '<output result="pending" reason="incomplete"/>', 'msg_type': 'code'}, {'id': '_6e354677', 'is_exported': 0, 'content': '1+1', 'output': '<output result="pending" reason="incomplete"/>', 'msg_type': 'code'}]
hl_md(await find_msgs(r'1\+1', include_meta=False, as_xml=True))<msgs><code id="_5a8db1de"><source>def f(): warnings.warn('a warning')
allow('f')
await python('print("asdf"); f(); 1+1')<out>{'stdout': 'asdf\n', 'warnings': 'UserWarning: a warning', 'result': 2}</out></code><code id="_8ce548d6"><source>1+1<out>2</out></code><code id="_44cb1b2a"><source>_id = await _add_msg_unsafe('1+1', run=True, msg_type='code')<out><output result="pending" reason="incomplete"/></out></code><code id="_6e354677"><source>1+1<out><output result="pending" reason="incomplete"/></out></code></msgs>view_dlg
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 read_msg multiple times.
hl_md((await view_dlg(nums=True))[:500])<msgs><code id="_955b9784"> 1 │ #| default_exp core</code><code id="_a982e24d"> 1 │ from dialoghelper import *</code><note id="_0aafe008"> 1 │ # dialoghelper</note><code id="_468aa264" export> 1 │ import json,importlib,linecache,re,inspect,uuid,ast,time,warnings,collections,time,asyncio,urllib.parse,dataclasses,shlex
2 │
3 │ from typing import Dict
4 │ from tempfile import TemporaryDirectory
5 │ from ipykernel_helper import *
6 │ from dataclasses import add_msg
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.
print(__msg_id)
_id = await add_msg('testing')
print(__msg_id)_d28405ce
_d28405ce
read_msgid
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.
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)# dh_settings['dname'] = 'tmp'
# _id = await add_msg('testing', placement='at_end')
# print(_id)
# del(dh_settings['dname'])del_msg
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
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)'_b4c4220c'
await del_msg(_id){'status': 'success'}_edit_id = await add_msg('This message should be found.\n\nThis is a multiline message.')
_edit_id'_928096c0'
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
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.
1+12
codeid = (await read_msg())['id']await run_msg(codeid)'{"status":"queued"}'
copy_msg
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
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'}
tgt = (await read_msg())['id']await paste_msg(tgt){'success': 'complete'}
newmsg = await read_msg(1, id=tgt)
newmsg['content']'1+1'
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
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
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
test header
hdid = (await read_msg())['id']test note
header end
await toggle_header(hdid){'success': 'complete'}
Dlg conveniences
url2note
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://www.example.org')await del_msg(_id){'status': 'success'}create_dialog
def create_dialog(
name:str, # Name/path of the new dialog (relative to current dialog's folder, or absolute if starts with '/')
):
Create a new dialog
await create_dialog('test_dialog'){'success': '"aai-ws/dialoghelper/nbs/test_dialog" is now running'}rm_dialog
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
await rm_dialog('test_dialog'){'success': 'deleted "/Users/jhoward/aai-ws/dialoghelper/nbs/test_dialog"'}run_code_interactive
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.
dialog_link
def dialog_link(
path:str, # Path to dialog (e.g. '/aai-ws/dialoghelper/nbs/00_core')
):
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.
Text Edit
msg_insert_line
def msg_insert_line(
text:str, # The text to edit
insert_line:int, # The 1-based line number after which to insert the text (0: before 1st line, 1: after 1st line, 2: after 2nd, etc.)
new_str:str, # The text to insert
):
Insert text at specific line num in message. {besure} {dname}
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')
await msg_insert_line(_edit_id, 5, 'This should go to the last line'){'success': 'Inserted text at line _928096c0 content'}
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
msg_str_replace
def msg_str_replace(
text:str, # The text to edit
old_str:str, # Text to find and replace
new_str:str, # Text to replace with
):
Replace first occurrence of old_str with new_str in a message. {dname}
await msg_str_replace(_edit_id, 'This should go to the first line', 'This should go to the 1st line'){'success': 'Replaced text in message _928096c0 content'}
import asyncio
from fastcore.meta import splice_sig
def orig(text, x:int, y:str): ...
async def wrapper(id:str, *args, **kw):
await asyncio.sleep(0)
return f"got {id} {args} {kw}"
w = splice_sig(wrapper, orig, 'text')
print(type(w), inspect.iscoroutinefunction(w))
print(inspect.signature(w))
await w('myid', 42, 'hello')<class 'function'> True
(id: str, x: int, y: str)
"got myid (42, 'hello') {}"
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
msg_strs_replace
def msg_strs_replace(
text:str, # The text to edit
old_strs:list, # List of strings to find and replace
new_strs:list, # List of replacement strings (must match length of old_strs)
):
Replace multiple strings simultaneously in a message. {dname}
await msg_strs_replace(_edit_id, ['This is a multiline message.', 'This should go to the last line'], ['5th line', 'last line']){'success': 'Replaced all strings in message _928096c0 content'}
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
msg_replace_lines
def msg_replace_lines(
text:str, # The text to edit
start_line:int, # Starting line number to replace (1-based indexing)
end_line:int=None, # Ending line number to replace (1-based, inclusive, negative counts from end, None for single line)
new_content:str='', # New content to replace the specified lines
):
Replace line range in msg with new content. {besure} {dname}
await msg_replace_lines(_edit_id, 2, 4,'line 2\nline 3\nline 4\n'){'success': 'Replaced lines in message _928096c0 content'}
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
msg_del_lines
def msg_del_lines(
text:str, # The text to edit
start_line:int, # Starting line number to delete (1-based indexing)
end_line:int=None, # Ending line number to delete (1-based, inclusive, negative counts from end, None for single line)
):
Delete line range from a message. {besure} {dname}
await msg_del_lines(_edit_id, 2, 4){'success': 'Deleted lines in message _928096c0 content'}
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
await del_msg(_edit_id){'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)',
{'A': {'text': 'url',
'range': {'byteOffset': {'start': 6370, 'end': 6373},
'start': {'line': 135, 'column': 30},
'end': {'line': 135, 'column': 33}}},
'B': {'text': 'data',
'range': {'byteOffset': {'start': 6380, 'end': 6384},
'start': {'line': 135, 'column': 40},
'end': {'line': 135, 'column': 44}}}},
'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
Context
ctx_folder
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
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
def ctx_symfile(
sym
):
Add note with filepath and contents for a symbol’s source file
# ctx_symfile(TemporaryDirectory)ctx_symfolder
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
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
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
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 use if doing anything non-trivial, or 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')
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.