dialoghelper

from dialoghelper import *
from fastcore import tools

Helpers


source

add_styles


def add_styles(
    s:str, cls_map:dict=None
):

Add solveit styles to s

import mistletoe
from fasthtml.common import show
s = 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


source

set_var


def set_var(
    var:str, val
):

Set var to val after finding it in all frames of the call stack


source

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)
a
42

source

find_msg_id


def find_msg_id(
    
):

*Get the message id by searching the call stack for __msg_id.*


source

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'

source

xgeta


def xgeta(
    url, kwargs:VAR_KEYWORD
):

source

xposta


def xposta(
    url, kwargs:VAR_KEYWORD
):

source

call_endpa


def call_endpa(
    path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, data:VAR_KEYWORD
):

source

call_endp


def call_endp(
    path, dname:str='', json:bool=False, raiseex:bool=False, id:NoneType=None, data:VAR_KEYWORD
):
find_msg_id()
'_9cbd170d'

source

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.


source

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

source

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


source

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'}

source

add_scr


def add_scr(
    scr, oob:str='innerHTML:#ephemeral'
):

Swap a script element to the end of the ephemeral element


source

add_scr_a


def add_scr_a(
    scr, oob:str='innerHTML:#ephemeral'
):

Swap a script element to the end of the ephemeral element


source

iife


def iife(
    code:str
):

Wrap javascript code string in an IIFE and execute it via add_html


source

iife_a


def iife_a(
    code:str
):

Wrap javascript code string in an IIFE and execute it via add_html


source

pop_data


def pop_data(
    idx, timeout:int=15
):

source

pop_data_a


def pop_data_a(
    idx, timeout:int=15
):

source

fire_event


def fire_event(
    evt:str, data:NoneType=None
):

source

fire_event_a


def fire_event_a(
    evt:str, data:NoneType=None
):

source

event_get


def event_get(
    evt:str, timeout:int=15, data:NoneType=None
):

Call fire_event and then pop_data to get a response


source

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


source

trigger_now


def trigger_now(
    evt, data:NoneType=None, ttl:int=5000
):

Synchronously trigger a browser event, safe against replay


source

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


source

allow


def allow(
    c:VAR_POSITIONAL
):

source

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': []}

source

safe_type


def safe_type(
    o:object
):

Same as type(o)

# print(MarkdownRenderer(add_msg)._repr_markdown_())

source

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


source

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.


source

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+1
2
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': '{&#x27;stdout&#x27;: &#x27;asdf\\n&#x27;, &#x27;warnings&#x27;: &#x27;UserWarning: a warning&#x27;, &#x27;result&#x27;: 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>{&#x27;stdout&#x27;: &#x27;asdf\n&#x27;, &#x27;warnings&#x27;: &#x27;UserWarning: a warning&#x27;, &#x27;result&#x27;: 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>

source

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 

source

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

source

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'])

source

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'}

source

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.

source

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+1
2
codeid = (await read_msg())['id']
await run_msg(codeid)
'{"status":"queued"}'

source

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.


source

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'}

source

enable_mermaid


def enable_mermaid(
    
):
enable_mermaid()

source

mermaid


def mermaid(
    code, cls:str='mermaid', kwargs:VAR_KEYWORD
):

A mermaid diagram

mermaid('graph LR; A[Start] --> B[Process]; B --> C[End];')
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]

source

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


source

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


source

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'}

source

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'}

source

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"'}

source

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.


source

Text Edit


source

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

source

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

source

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

source

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

source

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


source

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)))

source

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: end

Escaping in shell: Use \$VAR or single quotes when using --inline-rules from command line

Context


source

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')

../dialoghelper/capture.py def setup_share(): “Setup screen sharing”

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” ../dialoghelper/core.py <

[TRUNCATED: output size 24344 exceeded max size 600 bytes]


source

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


source

ctx_symfile


def ctx_symfile(
    sym
):

Add note with filepath and contents for a symbol’s source file

# ctx_symfile(TemporaryDirectory)

source

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)

source

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


source

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'

source

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=…

source

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)

source

mk_toollist


def mk_toollist(
    syms
):
print(mk_toollist([hi]))
- &`hi`: Say hi to `who`

source

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']

source

update_gist


def update_gist(
    gist_id:str, content:str
):

Update the first file in a gist with new content

Help


source

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


source

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.


source