dialoghelper.utils

from dialoghelper import *
from fastcore import tools
from fastcore.test import *

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

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 used 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, timeout=timeout)',
  {'A': {'text': 'url',
    'range': {'byteOffset': {'start': 5029, 'end': 5032},
     'start': {'line': 111, 'column': 30},
     'end': {'line': 111, 'column': 33}}},
   'B': {'text': 'data',
    'range': {'byteOffset': {'start': 5039, 'end': 5043},
     'start': {'line': 111, 'column': 40},
     'end': {'line': 111, '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

_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


source

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
    maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
    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


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
    maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
    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


async def ctx_symfile(
    sym
):

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

# ctx_symfile(TemporaryDirectory)

source

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
    maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
    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


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
    maxdepth:int=None, # max depth to descend (1=just immediate contents; None=unlimited)
    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


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

Call self as a function.

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

Call self as a function.

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


source

read_pr


def read_pr(
    pr_number:int | str, # Issue/PR number, or GitHub issue/PR URL
    owner:str='answerdotai', # Owner
    repo:str=None, # 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)

Input

input can take a string prompt as normal. OR, you can supply custom UI. For it to work, you must:

  • Wrap in <solveit-input> tags (done automatically by the new input function defined here)
  • Post to "/input_reply_" with a value for user_input.
  • For access keys (shortcuts for buttons), include an accesskey="y" in the button, and make sure it is in a form/div with id 'input-request-form'.

The show_prompt demo here (using the custom InputBtn) demonstrates this to give a custom yes/no prompt:


source

InputForm


def InputForm(
    c:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

Create an input() with a Form with needed hx_post and id


source

input


def input(
    prompt:str='', args:VAR_POSITIONAL
):

Solveit customised input to handle fasttag prompts


source

InputBtn


def InputBtn(
    txt, value:NoneType=None, btncls:tuple=(), kw:VAR_KEYWORD
):

Call self as a function.

def show_prompt():
    return InputForm(
        Div('Ship this change now?'),
        Div(cls='flex gap-2')(
            InputBtn('Yes', btncls='primary', accesskey="y"),
            InputBtn('No', btncls='default', accesskey="n"))
    )
# show_prompt()