'<msgs><code id="_955b9784">#| default_exp core</code><code id="_a982e24d">from dialoghelper import *</code><note id="_0aafe008"># dialoghelper</note><code id="_e881cda4" export>import json,importlib,linecache,re,inspect,uuid,ast,time\nfrom typing import Dict\nfrom tempfile import TemporaryDirectory\nfrom ipykernel_helper import *\nfrom dataclasses import dataclass\nfrom os.path import normpath\nfrom fastcore.xml import to_xml\nfrom fastcore.meta import splice_sig\n\nfrom fastcore.utils import *\nfrom fastcore.meta import delegates\nfrom ghapi.all import *\nfrom fastcore.xtras import asdict\nfrom inspect import currentframe,Parameter,signature\nfrom httpx import AsyncClient, get as xget, post as xpost\nfrom IPython.display import display,Markdown,HTML\nfrom monsterui.all import franken_class_map,apply_classes\nfrom fasthtml.common import Safe,Script,Div\nfrom toolslm.xml import *\nfrom lisette.core import ToolResponse</code><code id="_e54b45ad" export>dname_doc = """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.**"""</code><code id="_0eac27d1">from fastcore import tools</code><note id="_b2e59262">## Helpers</note><code id="_cc9f963f" export>md_cls_d = {\n **{f\'h{i}\': f\'uk-h{i}\' for i in range(1,5)},\n \'a\': "uk-link",\n \'blockquote\': "uk-blockquote",\n \'hr\':\'uk-divider-icon\',\n \'table\':\'uk-table uk-table-sm uk-table-middle uk-table-divider border [&_tr]:divide-x w-[80%] mx-auto\',\n \'ol\': \'uk-list uk-list-decimal space-y-0\', \n \'ul\': \'uk-list uk-list-bullet space-y-0\',\n \'p\': \'leading-tight\',\n \'li\': \'leading-tight\',\n \'pre\': \'\', \'pre code\': \'\',\n \'code\': \'tracking-tight\'\n}\n\ndef add_styles(s:str, cls_map:dict=None):\n "Add solveit styles to `s`"\n return Safe(apply_classes(s, class_map=cls_map or md_cls_d))</code><code id="_b9451ad8">import mistletoe\nfrom fasthtml.common import show</code><code id="_f13fdf03">s = mistletoe.markdown("### hi\\n\\n- first\\n- *second*")\ns</code><code id="_94c3f587">show(s)</code><code id="_f1aab712">show(add_styles(s))</code><note id="_710b1308">## Run python</note><code id="_bf3872ca" export>from fastcore.imports import __llmtools__\nfrom RestrictedPython import compile_restricted,utility_builtins, safe_builtins,limited_builtins</code><code id="_f5fdf7df" export>__llmtools__.add(\'read_url\')\n\nall_builtins = safe_builtins | utility_builtins | limited_builtins</code><code id="_1a7f1131" export>def _safe_getattr(obj, name):\n val = getattr(obj, name)\n if callable(val):\n keys = [f"{cls.__name__}.{name}" for cls in type(obj).__mro__]\n if not any(k in __llmtools__ for k in keys): raise AttributeError(f"Cannot access callable: {name}")\n return val</code><code id="_c56b1f7e" export>def _run_python(code:str):\n tools = {k: globals()[k] for k in __llmtools__ if k in globals()}\n tools |= {k:v for k,v in globals().items() if not callable(v) and not k.startswith(\'_\')}\n def unpack(a,*args): return list(a)\n rg = dict(__builtins__=all_builtins, _getattr_=_safe_getattr,\n _getitem_=lambda o,k: o[k], _getiter_=iter,\n _unpack_sequence_=unpack, _iter_unpack_sequence_=unpack,\n enumerate=enumerate, sorted=sorted, reversed=reversed, **tools)\n loc = {}\n def run(src, is_exec=True):\n f,mode = (exec,\'exec\') if is_exec else (eval,\'eval\')\n try: return f(compile_restricted(src, \'<tool>\', mode), rg, loc)\n except SyntaxError as e: return f\'SyntaxError: {e}\'\n tree = ast.parse(code)\n if tree.body and isinstance(tree.body[-1], ast.Expr):\n last = tree.body.pop()\n if tree.body: run(ast.unparse(ast.Module(tree.body, [])))\n return run(ast.unparse(ast.Expression(last.value)), False)\n run(code)</code><code id="_67bd9bc7" export>class RunPython:\n @property\n def __doc__(self):\n tools = \', \'.join(sorted(__llmtools__))\n return f"""Execute restricted Python with access to LLM tools, returning last expression.\n All non-callable globals and non-callable attrs are usable.\n In addition most builtins are available, plus these symbols: {tools}"""\n def __call__(self,\n code:str # Python code to execute, can be multiple lines, include functions, etc\n ): # The result of the last expression, if any\n return _run_python(code)</code><code id="_ade322da" export>run_python = RunPython()\n__llmtools__.add(\'run_python\')</code><note id="_25fb70ef">## Basics</note><code id="_f80e3a63" export>def _find_frame_dict(var:str):\n "Find the dict (globals or locals) containing var"\n frame = currentframe().f_back.f_back\n while frame:\n if var in frame.f_globals: return frame.f_globals\n frame = frame.f_back\n raise ValueError(f"Could not find {var} in any scope")\n\ndef find_var(var:str):\n "Search for var in all frames of the call stack"\n return _find_frame_dict(var)[var]\n \ndef set_var(var:str, val):\n "Set var to val after finding it in all frames of the call stack"\n _find_frame_dict(var)[var] = val</code><code id="_bb0489a1">a = 1\nfind_var(\'a\')</code><code id="_cec6f088">set_var(\'a\', 42)\na</code><code id="_eb1636a6" export>dh_settings = {\'port\':5001}</code><code id="_8c809b6d"># dh_settings = {\'port\':6001}</code><code id="_a01ad161" export>def find_dname(dname=None):\n "Get the dialog name by searching the call stack for __dialog_id, and resolving `dname` if supplied."\n if dname:\n dname = dname.removesuffix(\'.ipynb\')\n if dname.startswith(\'/\'): return dname\n curr = dh_settings.get(\'dname\', find_var(\'__dialog_name\'))\n if not dname: return \'/\'+curr\n p = Path(curr).parent\n res = normpath((p/dname))\n assert \'../\' not in res, "Path traversal not permitted"\n return \'/\'+res\n\ndef find_msg_id():\n "Get the message id by searching the call stack for __msg_id."\n return find_var(\'__msg_id\')\n\ndef _diff_dialog(pred, dname, err="`id` parameter must be provided when target dialog is different", id=None):\n "Raise ValueError if targeting a different dialog, `pred` is True, and no `id` provided"\n if not pred or id: return\n if dname or (\'dname\' in dh_settings): raise ValueError(err)</code><code id="_1930a9ce">find_dname()</code><code id="_bfecc90c">find_dname(\'index\')</code><code id="_294995f3">find_dname(\'../index\')</code><code id="_78e6e1a7">find_dname(\'/foo/bar\')</code><code id="_5d3a6e32" export>async def xposta(url, **kwargs):\n async with AsyncClient() as c: return await c.post(url, **kwargs)\nasync def xgeta(url, **kwargs):\n async with AsyncClient() as c: return await c.get (url, **kwargs)\n\nasync def call_endpa(path, dname=\'\', json=False, raiseex=False, id=None, **data):\n dname = find_dname(dname).strip(\'/\')\n data[\'dlg_name\'] = dname\n if id: data[\'id_\'] = id\n headers = {\'Accept\': \'application/json\'} if json else {}\n res = await xposta(f\'http://localhost:{dh_settings["port"]}/{path}\', data=data, headers=headers)\n if raiseex: res.raise_for_status()\n try: return dict2obj(res.json()) if json else res.text\n except Exception as e: return res.text</code><code id="_9cbd170d">find_msg_id()</code><code id="_a9cb5512" export>@llmtool\nasync def curr_dialog(\n with_messages:bool=False, # Include messages as well?\n dname:str=\'\' # Dialog to get info for; defaults to current dialog\n):\n "Get the current dialog info."\n res = await call_endpa(\'curr_dialog_\', dname, json=True, with_messages=with_messages)\n if res: return {\'name\': res[\'name\'], \'mode\': res[\'mode\']}</code><code id="_8810450f" export>@llmtool\nasync def msg_idx(\n id:str=None, # Message id to find (defaults to current message)\n dname:str=\'\' # Dialog to get message index from; defaults to current dialog\n):\n "Get absolute index of message in dialog."\n _diff_dialog(True, dname, id=id)\n if not id: id = find_msg_id()\n return (await call_endpa(\'msg_idx_\', dname, json=True, id=id))[\'idx\']\n</code><code id="_751dd86e">await msg_idx()</code><code id="_5335c78c" export>async def add_html(\n content:str, # The HTML to send to the client (generally should include hx-swap-oob)\n dname:str=\'\' # Dialog to get info for; defaults to current dialog\n):\n "Send HTML to the browser to be swapped into the DOM"\n await call_endpa(\'add_html_\', dname, content=to_xml(content))\n return {\'success\': \'Content added to DOM\'}\n</code><code id="_a8e79a9a">from fasthtml.common import *</code><code id="_3bc464f8">await add_html(Div(P(\'Hi\'), hx_swap_oob=\'beforeend:#dialog-container\'))</code><code id="_4b43e4e9" export>async def add_scr(scr, oob=\'innerHTML:#ephemeral\'):\n "Swap a script element to the end of the ephemeral element"\n if isinstance(scr,str): scr = Script(scr)\n await add_html(Div(scr, hx_swap_oob=oob))\n</code><code id="_0823ff14" export>async def iife(code: str) -> str:\n "Wrap javascript code string in an IIFE and execute it via `add_html`"\n await add_scr(f\'\'\'\n(async () => {{\n{code}\n}})();\n\'\'\')\n</code><code id="_99a07c05" export>async def pop_data(idx, timeout=15):\n return dict2obj(await call_endpa(\'pop_data_blocking_\', data_id=idx, timeout=timeout, json=True))\n</code><code id="_bc87c15d" export>async def fire_event(evt:str, data=None):\n params = f"\'{evt}\'"\n if data is not None: params += f", {json.dumps(data)}"\n await add_html(Script(f"htmx.trigger(document.body, {params});", id=\'js-event\', hx_swap_oob=\'true\'))\n</code><code id="_b1bb1088" export>async def event_get(evt:str, timeout=15, data=None):\n "Call `fire_event` and then `pop_data` to get a response"\n idx = uuid.uuid4()\n if not data: data = {}\n data[\'idx\'] = str(idx)\n await fire_event(evt, data=data)\n return await pop_data(idx, timeout)\n</code><code id="_e2d1a5be" export>def trigger_now(evt, data=None, ttl=5000):\n "Synchronously trigger a browser event, safe against replay"\n ts = time.time_ns() // 1_000_000\n params = f"\'{evt}\'"\n guard = f\'window["_trig_{ts}"]\'\n if data is not None: params += f", {json.dumps(data)}"\n display(HTML(f\'\'\'<script>\nif (Date.now() - {ts} < {ttl} && !{guard}) {{\n {guard}=1;\n htmx.trigger(document.body, {params});\n}}\n</script>\'\'\'))\n</code><code id="_80334098" export>def display_response(display:str, result:str=None):\n "Return a special response where `display` is added as markdown/HTML to the prompt output, and `result` is returned to the LLM"\n if result is None: result = f"The following has been added to the user\'s markdown/HTML dialog response:\\n{display}"\n return ToolResponse({\'_display\': display, \'result\': result})</code><note id="_a90b1e4a">## View/edit dialog</note><code id="_f819e9bd" export>def _maybe_xml(res, as_xml, key=None):\n if as_xml: return res\n res = loads(res)\n if \'error\' in res: return res\n if key: res = res[key]\n return dict2obj(res)</code><code id="_558c6aa7" export>@llmtool(dname=dname_doc)\nasync def read_msg(\n n:int=-1, # Message index (if relative, +ve is downwards)\n relative:bool=True, # Is `n` relative to current message (True) or absolute (False)?\n id:str=None, # Message id to find (defaults to current message)\n view_range:list[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF\n nums:bool=False, # Whether to show line numbers\n dname:str=\'\' # Dialog to get info for; defaults to current dialog\n ):\n """Get the message indexed in the current dialog.\n 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.\n - To get the exact message use `n=0` and `relative=True` together with `id`.\n - To get a relative message use `n` (relative position index).\n - To get the nth message use `n` with `relative=False`, e.g `n=0` first message, `n=-1` last message.\n {dname}"""\n _diff_dialog(relative, dname, "`id` parameter must be provided, or use `relative=False` with `n`, when target dialog is different", id=id)\n if relative and not id: id = find_msg_id()\n data = dict(n=n, relative=relative, id=id)\n if view_range: data[\'view_range\'] = view_range # None gets converted to \'\' so we avoid passing it to use the p.default\n if nums: data[\'nums\'] = nums\n return await call_endpa(\'read_msg_\', dname, json=True, **data)\n</code><code id="_6a4aa03b" export>@llmtool(dname=dname_doc)\nasync def find_msgs(\n re_pattern:str=\'\', # Optional regex to search for (re.DOTALL+re.MULTILINE is used)\n msg_type:str=None, # optional limit by message type (\'code\', \'note\', or \'prompt\')\n use_case:bool=False, # Use case-sensitive matching?\n use_regex:bool=True, # Use regex matching?\n only_err:bool=False, # Only return messages that have errors?\n only_exp:bool=False, # Only return messages that are exported?\n only_chg:bool=False, # Only return messages that have changed vs git HEAD?\n ids:str=\'\', # Optionally filter by comma-separated list of message ids\n limit:int=None, # Optionally limit number of returned items\n include_output:bool=True, # Include output in returned dict?\n include_meta:bool=True, # Include all additional message metadata\n as_xml:bool=False, # Use concise unescaped XML output format\n nums:bool=False, # Show line numbers?\n trunc_out:bool=False, # Middle-out truncate code output to 100 characters?\n trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?\n headers_only:bool=False, # Only return note messages that are headers (first line only); cannot be used together with `header_section`\n 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)\n dname:str=\'\' # Dialog to get info for; defaults to current dialog\n)->list[dict]: # Messages in requested dialog that contain the given information\n """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`.\n {dname}\n 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`.\n 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.)\n To refer to a found message from code or tools, use its `id` field."""\n res = await call_endpa(\'find_msgs_\', dname, json=False, re_pattern=re_pattern, msg_type=msg_type, limit=limit, ids=ids,\n use_case=use_case, use_regex=use_regex, only_err=only_err, only_exp=only_exp, only_chg=only_chg,\n include_output=include_output, include_meta=include_meta, as_xml=as_xml, nums=nums, trunc_out=trunc_out, trunc_in=trunc_in,\n headers_only=headers_only, header_section=header_section)\n return _maybe_xml(res, as_xml=as_xml, key=\'msgs\')\n</code><code id="_aa444dff"># NB: must have a dialogue open including a message with this text in its content\ntxt = \'tools\'\nfound = await find_msgs(txt)</code><code id="_8ce548d6">1+1</code><code id="_df4be3ff">r = await find_msgs(r\'1\\+1\', include_meta=False, include_output=True)\nr</code><code id="_bd06bf55">hl_md(await find_msgs(r\'1\\+1\', include_meta=False, as_xml=True))</code><code id="_9ff2a38e" export>@llmtool\nasync def view_dlg(\n dname:str=\'\', # Dialog to get info for; defaults to current dialog\n msg_type:str=None, # optional limit by message type (\'code\', \'note\', or \'prompt\')\n nums:bool=False, # Whether to show line numbers\n include_output:bool=False, # Include output in returned dict?\n trunc_out:bool=True, # Middle-out truncate code output to 100 characters (only applies if `include_output`)?\n trunc_in:bool=False, # Middle-out truncate cell content to 80 characters?\n):\n "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."\n return await find_msgs(msg_type=msg_type, dname=dname, as_xml=True, nums=nums,\n include_meta=False, include_output=include_output, trunc_out=trunc_out, trunc_in=trunc_in)\n</code><code id="_b5cb1f3b">hl_md((await view_dlg(nums=True))[:500])</code><code id="_fdc5a465" export>Placements = str_enum(\'Placements\', \'add_after\', \'add_before\', \'at_start\', \'at_end\')</code><code id="_7c4c7f5e" export>@llmtool(dname=dname_doc)\nasync def read_msgid(\n id:str, # Message id to find\n view_range:list[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF\n nums:bool=False, # Whether to show line numbers\n dname:str=\'\' # Dialog to get message from; defaults to current dialog\n ):\n """Get message `id`. Message IDs can be view directly in LLM chat history/context, or found in `find_msgs` results.\n {dname}"""\n return await read_msg(0, id=id, view_range=view_range, nums=nums, dname=dname)\n</code><code id="_3ad14786" export>@llmtool(dname=dname_doc)\nasync def add_msg(\n content:str, # Content of the message (i.e the message prompt, code, or note text)\n placement:str=\'add_after\', # Can be \'at_start\' or \'at_end\', and for default dname can also be \'add_after\' or \'add_before\'\n id:str=None, # id of message that placement is relative to (if None, uses current message; note: each add_msg updates "current" to the newly created message)\n msg_type: str=\'note\', # Message type, can be \'code\', \'note\', or \'prompt\'\n output:str=\'\', # Prompt/code output; Code outputs must be .ipynb-compatible JSON array\n time_run: str | None = \'\', # When was message executed\n is_exported: int | None = 0, # Export message to a module?\n skipped: int | None = 0, # Hide message from prompt?\n i_collapsed: int | None = 0, # Collapse input?\n o_collapsed: int | None = 0, # Collapse output?\n heading_collapsed: int | None = 0, # Collapse heading section?\n pinned: int | None = 0, # Pin to context?\n dname:str=\'\' # Dialog to get info for; defaults to current dialog. If passed, provide `id` or use `placement=\'at_start\'`/`\'at_end\'`\n)->str: # Message ID of newly created message\n """Add/update a message to the queue to show after code execution completes.\n **NB**: when creating multiple messages in a row, after the 1st message set `id` to the result of the last `add_msg` call,\n otherwise messages will appear in the dialog in REVERSE order.\n {dname}"""\n _diff_dialog(placement not in (\'at_start\',\'at_end\'), dname,\n "`id` or `placement=\'at_end\'`/`placement=\'at_start\'` must be provided when target dialog is different", id=id)\n if placement not in (\'at_start\',\'at_end\') and not id: id = find_msg_id()\n return await call_endpa(\n \'add_relative_\', dname, content=content, placement=placement, id=id, msg_type=msg_type, output=output,\n time_run=time_run, is_exported=is_exported, skipped=skipped, pinned=pinned,\n i_collapsed=i_collapsed, o_collapsed=o_collapsed, heading_collapsed=heading_collapsed)\n</code><code id="_9c544573">print(__msg_id)\n_id = await add_msg(\'testing\')\nprint(__msg_id)</code><code id="_ebf0d896">print((await read_msg()).content)</code><note id="_b85e21c7">`read_msg` (and all endpoints that return json) wrap responses in `dict2obj`, so you can use either dict or object syntax.</note><code id="_679f80cf">bmsg = await add_msg(\'at bottom\', placement=\'at_end\')</code><code id="_a67fc2f2">assert(await msg_idx(bmsg)>await msg_idx(_id)+10)</code><code id="_376b6e07"># dh_settings[\'dname\'] = \'tmp\'\n# _id = await add_msg(\'testing\', placement=\'at_end\')\n# print(_id)\n# del(dh_settings[\'dname\'])</code><code id="_f1ee1903" export>@llmtool\nasync def del_msg(\n id:str=None, # id of message to delete\n dname:str=\'\', # Dialog to get info for; defaults to current dialog\n log_changed:bool=False # Add a note showing the deleted content?\n):\n "Delete a message from the dialog. DO NOT USE THIS unless you have been explicitly instructed to delete messages."\n if log_changed: msg = await read_msgid(id, dname=dname)\n res = await call_endpa(\'rm_msg_\', dname, raiseex=True, msid=id, json=True)\n if log_changed: await add_msg(f"> Deleted #{id}\\n\\n```\\n{msg.content}\\n```")\n return res\n</code><code id="_9c6c959b">await del_msg(bmsg)\nawait del_msg(_id)</code><code id="_a9614fcc" export>@delegates(add_msg)\nasync def _add_msg_unsafe(\n content:str, # Content of the message (i.e the message prompt, code, or note text)\n placement:str=\'add_after\', # Can be \'at_start\' or \'at_end\', and for default dname can also be \'add_after\' or \'add_before\'\n id:str=None, # id of message that placement is relative to (if None, uses current message)\n run:bool=False, # For prompts, send it to the AI; for code, execute it (*DANGEROUS -- be careful of what you run!)\n dname:str=\'\', # Dialog to get info for; defaults to current dialog (`run` only has a effect if dialog is currently running)\n **kwargs\n)->str: # Message ID of newly created message\n """Add/update a message to the queue to show after code execution completes, and optionally run it.\n **NB**: when creating multiple messages in a row, after the 1st message set `id` to the result of the last `add_msg` call,\n otherwise messages will appear in the dialog in REVERSE order.\n *WARNING*--This can execute arbitrary code, so check carefully what you run!--*WARNING"""\n _diff_dialog(placement not in (\'at_start\',\'at_end\'), dname,\n "`id` or `placement=\'at_end\'`/`placement=\'at_start\'` must be provided when target dialog is different", id=id) \n if placement not in (\'at_start\',\'at_end\') and not id: id = find_msg_id()\n res = await call_endpa( \'add_relative_\', dname, content=content, placement=placement, id=id, run=run, **kwargs)\n return res\n</code><code id="_44cb1b2a">_id = await _add_msg_unsafe(\'1+1\', run=True, msg_type=\'code\')</code><code id="_8f1e0ee6">await del_msg(_id)</code><code id="_da3525d7">_id = await _add_msg_unsafe(\'Hi\', run=True, msg_type=\'prompt\')</code><code id="_4b0077ef">await del_msg(_id)</code><code id="_023dcb74" export>def _umsg(\n content:str|None=None, # Content of the message (i.e the message prompt, code, or note text)\n msg_type: str|None = None, # Message type, can be \'code\', \'note\', or \'prompt\'\n output:str|None = None, # Prompt/code output; Code outputs must be .ipynb-compatible JSON array\n time_run: str | None = None, # When was message executed\n is_exported: int | None = None, # Export message to a module?\n skipped: int | None = None, # Hide message from prompt?\n i_collapsed: int | None = None, # Collapse input?\n o_collapsed: int | None = None, # Collapse output?\n heading_collapsed: int | None = None, # Collapse heading section?\n pinned: int | None = None # Pin to context?\n): ...</code><code id="_38875a12" export>@llmtool(dname=dname_doc)\n@delegates(_umsg)\nasync def update_msg(\n id:str=None, # id of message to update (if None, uses current message)\n msg:Optional[Dict]=None, # Dictionary of field keys/values to update\n dname:str=\'\', # Dialog to get info for; defaults to current dialog\n log_changed:bool=False, # Add a note showing the diff?\n **kwargs):\n """Update an existing message. Provide either `msg` OR field key/values to update.\n - Use `content` param to update contents.\n - Only include parameters to update--missing ones will be left unchanged.\n {dname}"""\n if msg: kwargs |= msg.get(\'msg\', msg)\n if not id: id = kwargs.pop(\'id\', None)\n if not id: raise TypeError("update_msg needs either a dict message with and id, or `id=`")\n res = await call_endpa(\'update_msg_\', dname, id=id, log_changed=log_changed, **kwargs)\n if log_changed:\n r = json.loads(res) if isinstance(res, str) else res\n diff = r.get(\'diff\', \'\')\n note = f"> Updated #{id}\\n\\n```diff\\n{diff}\\n```" if diff else f"> Updated #{id}\\n\\nNo changes."\n await add_msg(note)\n res = r.get(\'id\', res)\n return res\n</code><code id="_4df69d72">_id = await add_msg(\'testing\')</code><code id="_05b509a7">_id = await update_msg(_id, content=\'toasting\')</code><code id="_8eaf1438">_id = await update_msg(_id, skipped=1)</code><code id="_f6d1d6e1">msg = await read_msgid(_id)\nmsg[\'content\'] = \'toasted\'\nawait update_msg(msg=msg)</code><code id="_ae95747a">await del_msg(_id)</code><code id="_ab9d75ec">_edit_id = await add_msg(\'This message should be found.\\n\\nThis is a multiline message.\')\n_edit_id</code><code id="_22ecf206">print((await read_msg())[\'content\'])</code><code id="_6d53e2dd">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_eda5f04b">print((await read_msg(n=0, id=_edit_id, nums=True, view_range=[2,3]))[\'content\'])</code><code id="_316bd7a0" export>async def run_msg(\n ids:str=None, # Comma-separated ids of message(s) to execute\n dname:str=\'\' # Running dialog to get info for; defaults to current dialog. (Note dialog *must* be running for this function)\n):\n "Adds a message to the run queue. Use read_msg to see the output once it runs."\n return await call_endpa(\'add_runq_\', dname, ids=ids, api=True)\n</code><code id="_6e354677">1+1</code><code id="_6e1c440e">codeid = (await read_msg())[\'id\']</code><code id="_7084f544">await run_msg(codeid)</code><code id="_73025e57" export>@llmtool\nasync def copy_msg(\n ids:str=None, # Comma-separated ids of message(s) to copy\n cut:bool=False, # Cut message(s)? (If not, copies)\n dname:str=\'\' # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)\n):\n "Add `ids` to clipboard."\n id,*_ = ids.split(\',\')\n res = await call_endpa(\'msg_clipboard_\', dname, ids=ids, id=id, cmd=\'cut\' if cut else \'copy\')\n return {\'success\':\'complete\'}\n</code><code id="_80def27e" export>@llmtool\nasync def paste_msg(\n id:str=None, # Message id to paste next to\n after:bool=True, # Paste after id? (If not, pastes before)\n dname:str=\'\' # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)\n):\n "Paste clipboard msg(s) after/before the current selected msg (id)."\n res = await call_endpa(\'msg_paste_\', dname, id=id, after=after)\n return {\'success\':\'complete\'}\n</code><code id="_bc91a8d8">await copy_msg(codeid)</code><code id="_cb519067">tgt = (await read_msg())[\'id\']</code><code id="_784f644c">await paste_msg(tgt)</code><code id="_084f2a60">newmsg = await read_msg(1, id=tgt)\nnewmsg[\'content\']</code><code id="_527dc036">await del_msg(newmsg[\'id\'])</code><code id="_13e9163a" export>mermaid_url = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"\n\ndef enable_mermaid():\n return Script("""\nif (window.mermaid) mermaid.run()\nelse {\n import(\'%s\').then(m => {\n window.mermaid = m.default;\n window.mermaid.run();\n htmx.onLoad(elt => {\n if (elt.matches(\'div.mermaid, pre.mermaid\') || htmx.findAll(elt, \'div.mermaid, pre.mermaid\')) window.mermaid.run();\n });\n });\n}""" % mermaid_url, type="module")</code><code id="_f2a4de16">enable_mermaid()</code><code id="_f3d1e424" export>def mermaid(code, cls="mermaid", **kwargs):\n "A mermaid diagram"\n return Div(code, cls=cls, **kwargs)</code><code id="_b90326a2">mermaid(\'graph LR; A[Start] --> B[Process]; B --> C[End];\')</code><note id="_167cba59">You can also add to a note:\n\n ```mermaid\n graph LR\n A[Start] --> B[Process]\n B --> C[End]\n ```\n\nThis renders as: \n\n```mermaid\ngraph LR\nA[Start] --> B[Process]\nB --> C[End]\n```</note><note id="_e7821a9b">You can also add to a note:\n\n ```mermaid\n graph LR\n A[Start] --> B[Process]\n B --> C[End]\n ```\n\nThis renders as: \n\n```\ngraph LR\nA[Start] --> B[Process]\nB --> C[End]\n```</note><code id="_b220e29b" export>@llmtool\nasync def toggle_header(\n id:str, # id of markdown header note message to toggle collapsed state\n dname:str=\'\' # Running dialog to copy messages from; defaults to current dialog. (Note dialog *must* be running for this function)\n):\n "Toggle collapsed header state for `id`"\n res = await call_endpa(\'toggle_header_collapse_\', dname, id=id)\n return {\'success\':\'complete\'}\n</code><note id="_ccfcb371">#### test header</note><code id="_9eaf3f71">hdid = (await read_msg())[\'id\']</code><note id="_e59c6be8">test note</note><note id="_9bba132a">#### header end</note><code id="_c66d782b">await toggle_header(hdid)</code><note id="_96bf4afb">### Dlg conveniences</note><code id="_1827e124" export>async def url2note(\n url:str, # URL to read\n extract_section:bool=True, # If url has an anchor, return only that section\n selector:str=None, # Select section(s) using BeautifulSoup.select (overrides extract_section)\n ai_img:bool=True, # Make images visible to the AI\n split_re:str=\'\' # Regex to split content into multiple notes, set to \'\' for single note\n):\n "Read URL as markdown, and add note(s) below current message with the result"\n res = read_url(url, as_md=True, extract_section=extract_section, selector=selector, ai_img=ai_img)\n if split_re: return [await add_msg(s) for s in re.split(split_re, res, flags=re.MULTILINE) if s.strip()]\n return await add_msg(res)\n</code><code id="_b90ff705">_id = await url2note(\'https://www.example.org\')</code><code id="_fca3268c">await del_msg(_id)</code><code id="_f26259cf" export>@llmtool\nasync def create_dialog(\n name:str, # Name/path of the new dialog (relative to current dialog\'s folder, or absolute if starts with \'/\')\n):\n "Create a new dialog"\n name = find_dname(name).lstrip(\'/\')\n return await call_endpa(\'create_dialog_\', name=name, api=True, json=True)\n</code><code id="_1dd50b03">await create_dialog(\'test_dialog\')</code><code id="_e393f14b" export>async def rm_dialog(\n name:str, # Name/path of the dialog to delete (relative to current dialog\'s folder, or absolute if starts with \'/\')\n):\n "Delete a dialog (or folder) and associated records"\n name = find_dname(name).lstrip(\'/\')\n return await call_endpa(\'rm_dialog_\', name=name, sess=\'{}\', api=True, json=True)\n</code><code id="_de14cbc3">await rm_dialog(\'test_dialog\')</code><code id="_5617305b" export>@llmtool\nasync def run_code_interactive(\n code:str # Code to have user run\n):\n """Insert code into user\'s dialog and request for the user to run it. Use other tools where possible, \n but if they can not find needed information, *ALWAYS* use this instead of guessing or giving up.\n IMPORTANT: This tool is TERMINAL - after calling it, you MUST stop all tool usage \n and wait for user response. Never call additional tools after this one."""\n await add_msg(\'# Please run this:\\n\'+code, msg_type=\'code\')\n return {\'success\': "CRITICAL: Message added to user dialog. STOP IMMEDIATELY. Do NOT call any more tools. Wait for user to run code and respond."}\n</code><code id="_1a614e33" export>def dialog_link(\n path:str # Path to dialog (e.g. \'/aai-ws/dialoghelper/nbs/00_core\')\n):\n """Return an IPython HTML link to open a dialog in Solveit.\n 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."""\n from urllib.parse import urlencode\n from IPython.display import HTML\n path = path.removeprefix(\'/\')\n url = f"/dialog_?{urlencode({\'name\': path})}"\n return HTML(f\'<a href="{url}" target="_blank">{path}</a>\')</code><note id="_90fb625a">## Text Edit</note><code id="_5250fa23" export>def _msg_edit(success_tpl):\n def decorator(fn):\n async def wrapper(id:str, *args, update_output:bool=False, dname:str=\'\', log_changed:bool=False, **kw):\n msg = await read_msg(n=0, id=id, dname=dname)\n field = \'output\' if update_output else \'content\'\n text = msg.get(field, \'\')\n if not text: return {\'error\': f"Message has no {field}"}\n try: new_text = fn(text, *args, **kw)\n except ValueError as e: return {\'error\': str(e)}\n await update_msg(id=id, **{field: new_text}, dname=dname, log_changed=log_changed)\n return {\'success\': success_tpl.format(id=id, field=field)}\n return splice_sig(wrapper, fn, \'text\')\n return decorator\n</code><code id="_ceb1ad3b" export>besure_doc = "Be sure you\'ve called `read_msg(…, nums=True)` to ensure you know the line nums."\n\n@llmtool(dname=dname_doc, besure=besure_doc)\n@_msg_edit(\'Inserted text at line {id} {field}\')\ndef msg_insert_line(text, insert_line:int, new_str:str):\n "Insert text at specific line num in message. {besure}\\n{dname}"\n lines = text.splitlines()\n if not (0 <= insert_line <= len(lines)): raise ValueError(f\'Invalid line {insert_line}. Valid range: 0-{len(lines)}\')\n lines.insert(insert_line, new_str)\n return \'\\n\'.join(lines)</code><code id="_10a14307">await msg_insert_line(_edit_id, 0, \'This should go to the first line\')\nawait msg_insert_line(_edit_id, 3, \'This should go to the 4th line\')\nawait msg_insert_line(_edit_id, 5, \'This should go to the last line\')\n</code><code id="_53264093">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_8568202a" export>@llmtool(dname=dname_doc)\n@_msg_edit(\'Replaced text in message {id} {field}\')\ndef msg_str_replace(text, old_str:str, new_str:str):\n "Replace first occurrence of old_str with new_str in a message.\\n{dname}"\n count = text.count(old_str)\n if count == 0: raise ValueError(f"Text not found: {repr(old_str)}")\n if count > 1: raise ValueError(f"Multiple matches ({count}): {repr(old_str)}")\n return text.replace(old_str, new_str, 1)</code><code id="_7fb271dd">await msg_str_replace(_edit_id, \'This should go to the first line\', \'This should go to the 1st line\')</code><code id="_fe526375">import asyncio\nfrom fastcore.meta import splice_sig\n\ndef orig(text, x:int, y:str): ...\n\nasync def wrapper(id:str, *args, **kw):\n await asyncio.sleep(0)\n return f"got {id} {args} {kw}"\n\nw = splice_sig(wrapper, orig, \'text\')\nprint(type(w), inspect.iscoroutinefunction(w))\nprint(inspect.signature(w))\nawait w(\'myid\', 42, \'hello\')</code><code id="_fab1351f">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_983ce14a" export>@llmtool(dname=dname_doc)\n@_msg_edit(\'Replaced all strings in message {id} {field}\')\ndef msg_strs_replace(text, old_strs:list[str], new_strs:list[str]):\n "Replace multiple strings simultaneously in a message.\\n{dname}"\n if not isinstance(old_strs, list): raise ValueError(f"`old_strs` should be a list[str] but got {type(old_strs)}")\n if not isinstance(new_strs, list): raise ValueError(f"`new_strs` should be a list[str] but got {type(new_strs)}")\n if len(old_strs) != len(new_strs): raise ValueError(f"Length mismatch: {len(old_strs)} old_strs vs {len(new_strs)} new_strs")\n for idx,(old_str,new_str) in enumerate(zip(old_strs, new_strs)):\n count = text.count(old_str)\n if count == 0: raise ValueError(f"Text not found at index {idx}: {repr(old_str)}")\n if count > 1: raise ValueError(f"Multiple matches ({count}) at index {idx}: {repr(old_str)}")\n text = text.replace(old_str, new_str, 1)\n return text</code><code id="_baed4dec">await msg_strs_replace(_edit_id, [\'This is a multiline message.\', \'This should go to the last line\'], [\'5th line\', \'last line\'])</code><code id="_8d1ff40a">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_7b11e714" export>def _norm_lines(n:int, start:int, end:int=None):\n "Normalize and validate line range. Returns (start, end) or raises ValueError."\n if end is None: end = start\n if end < 0: end = n + end + 1\n if not (1 <= start <= n): raise ValueError(f\'Invalid start line {start}. Valid range: 1-{n}\')\n if not (start <= end <= n): raise ValueError(f\'Invalid end line {end}. Valid range: {start}-{n}\')\n return start, end</code><code id="_1002423f" export>@llmtool(dname=dname_doc, besure=besure_doc)\n@_msg_edit(\'Replaced lines in message {id} {field}\')\ndef msg_replace_lines(text, start_line:int, end_line:int=None, new_content:str=\'\'):\n "Replace line range in msg with new content. {besure}\\n{dname}"\n lines = text.splitlines(keepends=True)\n s,e = _norm_lines(len(lines), start_line, end_line)\n if lines and new_content and not new_content.endswith(\'\\n\'): new_content += \'\\n\'\n lines[s-1:e] = [new_content] if new_content else []\n return \'\'.join(lines)</code><code id="_a087b570">await msg_replace_lines(_edit_id, 2, 4,\'line 2\\nline 3\\nline 4\\n\')</code><code id="_86e28340">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_cbd87701" export>@llmtool(dname=dname_doc, besure=besure_doc)\n@_msg_edit(\'Deleted lines in message {id} {field}\')\ndef msg_del_lines(text, start_line:int, end_line:int=None):\n "Delete line range from a message. {besure}\\n{dname}"\n lines = text.splitlines(keepends=True)\n s,e = _norm_lines(len(lines), start_line, end_line)\n del lines[s-1:e]\n return \'\'.join(lines)</code><code id="_fd38ca34">await msg_del_lines(_edit_id, 2, 4)</code><code id="_1c2dcb37">print((await read_msg(n=0, id=_edit_id, nums=True))[\'content\'])</code><code id="_26889752">await del_msg(_edit_id)</code><code id="_5cafdc5a" export>@llmtool\ndef dialoghelper_explain_dialog_editing(\n)->str: # Detailed documention on dialoghelper dialog editing\n "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"\n return """# dialoghelper dialog editing functionality\n\nThis guide consolidates understanding of how dialoghelper tools work together. Individual tool schemas are already in context—this adds architectural insight and usage patterns.\n\n## Core Concepts\n\n- **Dialog addressing**: All functions accepting `dname` resolve paths relative to current dialog (no leading `/`) or absolute from Solveit\'s runtime data path (with leading `/`). The `.ipynb` extension is never included.\n- **Message addressing**: Messages have stable `id` strings (e.g., `_a9cb5512`). The current executing message\'s id is in `__msg_id`. Tools use `id` for targeting; `find_msg_id()` retrieves current.\n- **Implicit state**: After `add_msg`/`update_msg`, `__msg_id` is updated to the new/modified message. This enables chaining: successive `add_msg` calls create messages in sequence.\n\n## Tool Workflow Patterns\n\n### Reading dialog state\n- `view_dlg` — fastest way to see entire dialog structure with line numbers for editing\n- `find_msgs` — search with regex, filter by type/errors/changes\n- `read_msg` — navigate relative to current message\n- `read_msgid` — direct access when you have the id\n\n**Key insight**: Messages above the current prompt are already in LLM context—their content and outputs are always up-to-date. Do NOT use read tools just to review content you can already see. Use read tools only for: (1) getting line numbers immediately before editing, (2) accessing messages below current prompt (if you\'re sure the user wants you to "look ahead"), (3) accessing other dialogs.\n\n### Modifying dialogs\n- `add_msg` — placement can be `add_after`/`add_before` (relative to current) or `at_start`/`at_end` (absolute)\n - **NB** When not passing a message id, it defaults to the *current* message. So if you call it multiple times with no message id, the messages will be added in REVERSE! Instead, get the return value of `add_msg` after each call, and use that for the next call\n- `update_msg` — partial updates; only pass fields to change\n- `del_msg` — use sparingly, only when explicitly requested\n`copy_msg` → `paste_msg` — for moving/duplicating messages within running dialogs.\n\n## Non-decorated Functions Worth Knowing\n\nThere are additional functions available that can be added to fenced blocks, or the user may add as tools; they are not included in schemas by default.\n\n**Browser integration:**\n- `add_html(content)` — inject HTML with `hx-swap-oob` into live browser DOM\n- `iife(code)` — execute JavaScript immediately in browser\n- `fire_event(evt, data)` / `event_get(evt)` — trigger/await browser events\n\n**Content helpers:**\n- `url2note(url, ...)` — fetch URL as markdown, add as note message\n- `mermaid(code)` / `enable_mermaid()` — render mermaid diagrams\n- `add_styles(s)` — apply solveit\'s MonsterUI styling to HTML\n\n**Dangerous (not exposed by default):**\n- `_add_msg_unsafe(content, run=True, ...)` — add AND execute message (code or prompt)\n- `run_msg(ids)` — queue messages for execution\n- `rm_dialog(name)` — delete entire dialog\n\n## Important Patterns\n\n### Key Principles\n\n1. **Always re-read before editing.** Past tool call results in chat history are TRUNCATED. Never rely on line numbers from earlier in the conversation—call `read_msgid(id, nums=True)` immediately before any edit operation.\n2. **Work backwards.** When making multiple edits to a message, start from the end and work towards the beginning. This prevents line number shifts from invalidating your planned edits.\n3. **Don\'t guess when tools fail.** If a tool call returns an error, STOP and ask for clarification. Do not retry with guessed parameters.\n4. **Verify after complex edits.** After significant changes, re-read the affected region to confirm the edit worked as expected before proceeding.\n\n### Typical Workflow\n\n```\n1. read_msgid(id, nums=True) # Get current state with line numbers\n2. Identify lines to change\n3. msg_replace_lines(...) or msg_str_replace(...) # Make edit\n4. If more edits needed: re-read, then repeat from step 2\n```\n\n### Tool Selection\n\n- **`msg_replace_lines`**: Best for replacing/inserting contiguous blocks. Use `view_range` on read to focus on the area.\n- **`msg_str_replace`**: Best for targeted single small string replacements when you know the exact text.\n- **`msg_strs_replace`**: Best for multiple small independent replacements in one call.\n- **`msg_insert_line`**: Best for adding new content without replacing existing lines.\n- **`msg_del_lines`**: Best for removing content.\n\n**Rough rule of thumb:** Prefer `msg_replace_lines` over `msg_str(s)_replace` unless there\'s >1 match to change or it\'s just a word or two. Use the insert/delete functions for inserting/deleting; don\'t use `msg_str(s)_replace` for that.\n\n### Common Mistakes to Avoid\n\n- Using line numbers from a truncated earlier result\n- Making multiple edits without re-reading between them\n- Guessing line numbers when a view_range was truncated\n- Always call `read_msgid(id, nums=True)` first to get accurate line numbers\n- String-based tools (`msg_str_replace`, `msg_strs_replace`) fail if the search string appears zero or multiple times—use exact unique substrings."""</code><note id="_d56b9839">## ast-grep</note><code id="_9adf1cbb" export>def ast_py(code:str):\n "Get an SgRoot root node for python `code`"\n from ast_grep_py import SgRoot\n return SgRoot(code, "python").root()</code><code id="_faeb0863">node = ast_py("print(\'hello world\')")\nstmt = node.find(pattern="print($A)")\nres = stmt.get_match(\'A\')\nres.text(),res.range()</code><code id="_22c2316b" export>@llmtool\ndef ast_grep(\n pattern:str, # ast-grep pattern to search, e.g "post($A, data=$B, $$$)"\n path:str=".", # path to recursively search for files\n lang:str="python" # language to search/scan\n): # json format from calling `ast-grep --json=compact\n """Use `ast-grep` to find code patterns by AST structure (not text).\n \n Pattern syntax:\n - $VAR captures single nodes, $$$ captures multiple\n - Match structure directly: `def $FUNC($$$)` finds any function; `class $CLASS` finds classes regardless of inheritance\n - DON\'T include `:` - it\'s concrete syntax, not AST structure\n - Whitespace/formatting ignored - matches structural equivalence\n \n Examples: `import $MODULE` (find imports); `$OBJ.$METHOD($$$)` (find method calls); `await $EXPR` (find await expressions)\n \n 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"""\n import json, subprocess\n cmd = f"ast-grep --pattern \'{pattern}\' --lang {lang} --json=compact"\n if path != ".": cmd = f"cd {path} && {cmd}"\n res = subprocess.run(cmd, shell=True, capture_output=True, text=True)\n return json.loads(res.stdout) if res.stdout else res.stderr</code><note id="_adba2bf4">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.\n\nThe 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.\n\nIn 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.</note><code id="_f7227c1e">res = ast_grep(r"xpost($A, data=$B, $$$)", \'..\')\n[(o[\'text\'],o[\'metaVariables\'][\'single\'],o[\'file\']) for o in res]</code><note id="_67b07d1f">**Basic Patterns:**\n- Match code structure directly: `console.log($ARG)` \n- Metavariables capture parts: `$VAR` (single), `$$$` (multiple)\n- Patterns match AST structure, not text - whitespace/formatting doesn\'t matter\n\n**The Colon Issue:**\n- **Don\'t include `:` in patterns** - it\'s part of Python\'s concrete syntax, not the AST structure\n- ✅ `def $FUNC($$$)` - matches function definitions\n- ❌ `def $FUNC($$$):` - too specific, looking for the colon token itself\n\n**When to use `kind` vs `pattern`:**\n- `pattern`: Simple direct matches (`await $EXPR`)\n- `kind`: Structural node types (`kind: function_declaration`)\n\n**Critical rule for relational searches:**\nAlways add `stopBy: end` to `has`/`inside` rules to search the entire subtree:\n```yaml\nhas:\n pattern: await $EXPR\n stopBy: end\n```\n\n**Escaping in shell:**\nUse `\\$VAR` or single quotes when using `--inline-rules` from command line</note><note id="_e7207b36">## Context</note><code id="_5fd28219" export>@delegates(folder2ctx)\nasync def ctx_folder(\n path:Path=\'.\', # Path to collect\n 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\n out=False, # Include notebook cell outputs?\n raw=True, # Add raw message, or note?\n exts:str|list=None, # list or comma-separated str of exts to include (overrides `types`)\n **kwargs\n):\n "Convert folder to XML context and place in a new message"\n if exts: types=None\n res = folder2ctx(path, types=types, out=out, exts=exts, **kwargs)\n if not raw: res = f\'```\\n{res}\\n```\'\n return await add_msg(res, msg_type=\'raw\' if raw else \'note\')\n</code><code id="_2300e115"># ctx_folder(\'..\', max_total=600, sigs_only=True, exts=\'py\')</code><raw id="_b378ec14"><documents><document index="4"><src>\n../dialoghelper/capture.py\n</src><document-content>\ndef setup_share():\n "Setup screen sharing"\n\ndef start_share(): fire_event(\'shareScreen\')\n\ndef _capture_screen(timeout=15):\n\ndef capture_screen(timeout=15):\n "Capture the screen as a PIL image."\n\ndef capture_tool(timeout:int=15):\n "Capture the screen. Re-call this function to get the most recent screenshot, as needed. Use default timeout where possible"\n</document-content></document><document index="5"><src>\n../dialoghelper/core.py\n</src><\n\n[TRUNCATED: output size 24344 exceeded max size 600 bytes]</raw><code id="_2dc4dc40" export>@delegates(repo2ctx)\nasync def ctx_repo(\n owner:str, # GitHub repo owner\n repo:str, # GitHub repo name\n 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\n exts:str|list=None, # list or comma-separated str of exts to include (overrides `types`)\n out=False, # Include notebook cell outputs?\n raw=True, # Add raw message, or note?\n **kwargs\n):\n "Convert GitHub repo to XML context and place in a new message"\n res = repo2ctx(owner, repo, out=out, types=types, exts=exts, **kwargs)\n if exts: types=None\n if not raw: res = f\'```\\n{res}\\n```\'\n return await add_msg(res, msg_type=\'raw\' if raw else \'note\')\n</code><code id="_5bd8915b" export>async def ctx_symfile(sym):\n "Add note with filepath and contents for a symbol\'s source file"\n return await add_msg(sym2file(sym), msg_type=\'note\');\n</code><code id="_6180d2a3"># ctx_symfile(TemporaryDirectory)</code><code id="_bbc9bd0c" export>@delegates(sym2folderctx)\nasync def ctx_symfolder(\n sym, # Symbol to get folder context from\n **kwargs):\n "Add raw message with folder context for a symbol\'s source file location"\n return await add_msg(sym2folderctx(sym, **kwargs), msg_type=\'raw\');\n</code><code id="_7a308942"># ctx_symfolder(folder2ctx)</code><code id="_9d8d83b5" export>@delegates(sym2folderctx)\nasync def ctx_sympkg(\n sym, # Symbol to get folder context from\n **kwargs):\n "Add raw message with repo context for a symbol\'s root package"\n return await add_msg(sym2pkgctx(sym, **kwargs), msg_type=\'raw\');\n</code><code id="_11c8b385"># ctx_sympkg(folder2ctx)</code><note id="_db2c0e1c">## Gists</note><code id="_eb4c6bf4" export>def load_gist(gist_id:str):\n "Retrieve a gist"\n api = GhApi()\n if \'/\' in gist_id: *_,user,gist_id = gist_id.split(\'/\')\n else: user = None\n return api.gists.get(gist_id, user=user)</code><code id="_3ab7586f">gistid = \'jph00/e7cfd4ded593e8ef6217e78a0131960c\'\ngist = load_gist(gistid)\ngist.html_url</code><code id="_3b151f8f" export>def gist_file(gist_id:str):\n "Get the first file from a gist"\n gist = load_gist(gist_id)\n return first(gist.files.values())</code><code id="_ea33969d">gfile = gist_file(gistid)\nprint(gfile.content[:100]+"…")</code><code id="_1ccfc95a" export>def import_string(\n code:str, # Code to import as a module\n name:str # Name of module to create\n):\n with TemporaryDirectory() as tmpdir:\n path = Path(tmpdir) / f"{name}.py"\n path.write_text(code)\n # linecache.cache storage allows inspect.getsource() after tmpdir lifetime ends\n linecache.cache[str(path)] = (len(code), None, code.splitlines(keepends=True), str(path))\n spec = importlib.util.spec_from_file_location(name, path)\n module = importlib.util.module_from_spec(spec)\n sys.modules[name] = module\n spec.loader.exec_module(module)\n return module</code><code id="_9ec853f7">def hi(who:str):\n "Say hi to `who`"\n return f"Hello {who}"\n\ndef hi2(who):\n "Say hi to `who`"\n return f"Hello {who}"\n\ndef hi3(who:str):\n return f"Hello {who}"\n\nbye = "bye"</code><code id="_728affe1">assert is_usable_tool(hi)\nassert not is_usable_tool(hi2)\nassert not is_usable_tool(hi3)\nassert not is_usable_tool(bye)</code><code id="_27730708" export>def mk_toollist(syms):\n return "\\n".join(f"- &`{sym.__name__}`: {sym.__doc__}" for sym in syms if is_usable_tool(sym))</code><code id="_a952258c">print(mk_toollist([hi]))</code><code id="_84868ea2" export>def import_gist(\n gist_id:str, # user/id or just id of gist to import as a module\n mod_name:str=None, # module name to create (taken from gist filename if not passed)\n add_global:bool=True, # add module to caller\'s globals?\n import_wildcard:bool=False, # import all exported symbols to caller\'s globals\n create_msg:bool=False # Add a message that lists usable tools\n):\n "Import gist directly from string without saving to disk"\n fil = gist_file(gist_id)\n mod_name = mod_name or Path(fil[\'filename\']).stem\n module = import_string(fil[\'content\'], mod_name)\n glbs = currentframe().f_back.f_globals\n if add_global: glbs[mod_name] = module\n syms = getattr(module, \'__all__\', None)\n if syms is None: syms = [o for o in dir(module) if not o.startswith(\'_\')]\n syms = [getattr(module, nm) for nm in syms]\n if import_wildcard:\n for sym in syms: glbs[sym.__name__] = sym\n if create_msg:\n pref = getattr(module, \'__doc__\', "Tools added to dialog:")\n asyncio.ensure_future(add_msg(f"{pref}\\n\\n{mk_toollist(syms)}"))\n return module</code><code id="_aefee238">import_gist(gistid)\nimporttest.testfoo</code><code id="_4a4f7f89">import_gist.__doc__</code><code id="_e80a7944">import_gist(gistid, import_wildcard=True)\nimporttest.testfoo</code><code id="_573abb48">hi("Sarah")</code><code id="_392dc090">importtest.__all__</code><code id="_f790682a" export>def update_gist(gist_id:str, content:str):\n "Update the first file in a gist with new content"\n api = GhApi()\n if \'/\' in gist_id: *_,user,gist_id = gist_id.split(\'/\')\n gist = api.gists.get(gist_id)\n fname = first(gist.files.keys())\n res = api.gists.update(gist_id, files={fname: {\'content\': content}})\n return res[\'html_url\']</code><note id="_5aa7e033">## export -</note><code id="_379099ac">#| hide\nfrom nbdev import nbdev_export\nnbdev_export()</code></msgs>'