from dialoghelper import *dialoghelper.utils
from fastcore import tools
from fastcore.test import *Helpers
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
ast-grep
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 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: endEscaping in shell: Use \$VAR or single quotes when using --inline-rules from command line
_ast_id = await add_msg("print('hello')\nprint('world')\nlog('keep')", msg_type='code')print(await msg_ast_replace(_ast_id, 'print($A)', 'logger.info($A)'))@@ -1,3 +1,3 @@
-print('hello')
-print('world')
+logger.info('hello')
+logger.info('world')
log('keep')
print((await read_msg(n=0, id=_ast_id, nums=True))['content']) 1 │ logger.info('hello')
2 │ logger.info('world')
3 │ log('keep')
await del_msg(_ast_id){'status': 'success'}
Context
ctx_folder
async def ctx_folder(
path:Path='.', # Path to collect
types:str | list='py,doc', # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
out:bool=False, # Include notebook cell outputs?
raw:bool=True, # Add raw message, or note?
exts:str | list=None, # list or comma-separated str of exts to include (overrides `types`)
prefix:bool=False, # Include Anthropic's suggested prose intro?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
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')def start_share(): fire_event(‘shareScreen’)
def _capture_screen(timeout=15):
def capture_screen(timeout=15): “Capture the screen as a PIL image.”
def capture_tool(timeout:int=15): “Capture the screen. Re-call this function to get the most recent screenshot, as needed. Use default timeout where possible”
[TRUNCATED: output size 24344 exceeded max size 600 bytes]
ctx_repo
async def ctx_repo(
owner:str, # GitHub repo owner
repo:str, # GitHub repo name
types:str | list='py,doc', # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
exts:str | list=None, # list or comma-separated str of exts to include (overrides `types`)
out:bool=False, # Include notebook cell outputs?
raw:bool=True, # Add raw message, or note?
ref:str=None, # Git ref (branch/tag/sha) (get from URL not provided); defaults to repo's default branch
folder:str=None, # Only include files under this path (get from URL not provided)
show_filters:bool=True, # Include filter info in title?
token:str=None, # GitHub token (uses GITHUB_TOKEN env var if None)
prefix:bool=False, # Include Anthropic's suggested prose intro?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
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
ctx_symfile
async def ctx_symfile(
sym
):
Add note with filepath and contents for a symbol’s source file
# ctx_symfile(TemporaryDirectory)ctx_symfolder
async def ctx_symfolder(
sym, # Symbol to get folder context from
types:str | list='py', # List or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg
skip_file_re:str='^_mod', # Skip files matching regex
prefix:bool=False, # Include Anthropic's suggested prose intro?
out:bool=True, # Include notebook cell outputs?
include_base:bool=True, # Include full path in src?
title:str=None, # Optional title attr for Documents element
max_size:int=100000, # Skip files larger than this (bytes)
max_total:int=10000000, # Max total output size in bytes
readme_first:bool=False, # Prioritize README files at start of context?
files_only:bool=False, # Return dict of {filename: size} instead of context?
sigs_only:bool=False, # Return signatures instead of full text? (where supported by `codesigs` lib)
ids:bool=True, # Include cell ids in notebooks?
recursive:bool=True, # search subfolders
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)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
gistid = 'jph00/e7cfd4ded593e8ef6217e78a0131960c'
gist = load_gist(gistid)
gist.html_url'https://gist.github.com/jph00/e7cfd4ded593e8ef6217e78a0131960c'
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
):
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)print(mk_toollist([hi]))- &`hi`: Say hi to `who`
import_gist
def import_gist(
gist_id:str, # user/id or just id of gist to import as a module
mod_name:str=None, # module name to create (taken from gist filename if not passed)
add_global:bool=True, # add module to caller's globals?
import_wildcard:bool=False, # import all exported symbols to caller's globals
create_msg:bool=False, # Add a message that lists usable tools
):
Import gist directly from string without saving to disk
import_gist(gistid)
importtest.testfoo'testbar'
import_gist.__doc__'Import gist directly from string without saving to disk'
import_gist(gistid, import_wildcard=True)
importtest.testfoo'testbar'
hi("Sarah")'Hello Sarah'
importtest.__all__['hi', 'whoami']
update_gist
def update_gist(
gist_id:str, content:str
):
Update the first file in a gist with new content
read_pr
def read_pr(
pr_number:int | 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 newinputfunction defined here) - Post to
"/input_reply_"with a value foruser_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:
InputForm
def InputForm(
c:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Create an input() with a Form with needed hx_post and id
input
def input(
prompt:str='', args:VAR_POSITIONAL
):
Solveit customised input to handle fasttag prompts
InputBtn
def InputBtn(
txt, value:NoneType=None, btncls:tuple=(), kw:VAR_KEYWORD
):
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()