# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['Placements', 'empty', 'find_var', 'call_endp', 'find_dname', 'find_msg_id', 'curr_dialog', 'find_msgs', 'msg_idx',
           'add_html', 'add_msg', 'del_msg', 'update_msg', 'url2note', 'ast_py', 'ast_grep', 'read_msg', 'run_msg',
           'msg_insert_line', 'msg_str_replace', 'msg_strs_replace', 'msg_replace_lines', 'load_gist', 'gist_file',
           'import_string', 'is_usable_tool', 'mk_toollist', 'import_gist', 'tool_info', 'fc_tool_info', 'asdict']

# %% ../nbs/00_core.ipynb
import json, importlib, linecache
from typing import Dict
from tempfile import TemporaryDirectory
from ipykernel_helper import *
from dataclasses import dataclass
from fastcore.xml import to_xml

from fastcore.utils import *
from fastcore.meta import delegates
from ghapi.all import *
from fastlite import *
from fastcore.xtras import asdict
from inspect import currentframe,Parameter,signature
from httpx import get as xget, post as xpost
from .core import __all__ as _all
from IPython.display import display,Markdown

# %% ../nbs/00_core.ipynb
_all_ = ["asdict"]

# %% ../nbs/00_core.ipynb
def find_var(var:str):
    "Search for var in all frames of the call stack"
    frame = currentframe()
    while frame:
        dv = frame.f_globals.get(var, frame.f_locals.get(var, None))
        if dv: return dv
        frame = frame.f_back
    raise ValueError(f"Could not find {var} in any scope")

# %% ../nbs/00_core.ipynb
def call_endp(path, dname='', json=False, raiseex=False, **data):
    if not dname: dname = find_dname()
    data['dlg_name'] = dname
    res = xpost(f'http://localhost:5001/{path}', data=data)
    if raiseex: res.raise_for_status()
    try: return res.json() if json else res.text
    except Exception as e: return str(e)

# %% ../nbs/00_core.ipynb
def find_dname():
    "Get the message id by searching the call stack for __dialog_id."
    return find_var('__dialog_name')

# %% ../nbs/00_core.ipynb
def find_msg_id():
    "Get the message id by searching the call stack for __dialog_id."
    return find_var('__msg_id')

# %% ../nbs/00_core.ipynb
def curr_dialog(
    with_messages:bool=False,  # Include messages as well?
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Get the current dialog info."
    res = call_endp('curr_dialog_', dname, json=True, with_messages=with_messages)
    if res: return {'name': res['name'], 'mode': res['mode']}

# %% ../nbs/00_core.ipynb
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')
    limit:int=None, # Optionally limit number of returned items
    include_output:bool=True, # Include output in returned dict?
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Find `list[dict]` of messages in current specific dialog that contain the given information. To refer to a message found later, use its `id` field."
    res = call_endp('find_msgs_', dname, json=True, re_pattern=re_pattern, msg_type=msg_type, limit=limit)['msgs']
    if not include_output:
        for o in res: o.pop('output', None)
    return res

# %% ../nbs/00_core.ipynb
def msg_idx(
    msgid=None,  # Message id to find (defaults to current message)
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Get absolute index of message in dialog."
    if not msgid: msgid = find_msg_id()
    return call_endp('msg_idx_', dname, json=True, msgid=msgid)['msgid']

# %% ../nbs/00_core.ipynb
def add_html(
    content:str, # The HTML to send to the client (generally should include hx-swap-oob)
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Send HTML to the browser to be swapped into the DOM"
    call_endp('add_html_', dname, content=to_xml(content))

# %% ../nbs/00_core.ipynb
Placements = str_enum('Placements', 'add_after', 'add_before', 'at_start', 'at_end')

# %% ../nbs/00_core.ipynb
def add_msg(
    content:str, # Content of the message (i.e the message prompt, code, or note text)
    placement:str='add_after', # Can be 'add_after', 'add_before', 'at_start', 'at_end'
    msgid:str=None, # id of message that placement is relative to (if None, uses current message)
    msg_type: str='note', # Message type, can be 'code', 'note', or 'prompt'
    output:str='', # For prompts/code, initial output
    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?
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Add/update a message to the queue to show after code execution completes."
    if placement not in ('at_start','at_end') and not msgid: msgid = find_msg_id()
    return call_endp(
        'add_relative_', dname, content=content, placement=placement, msgid=msgid, msg_type=msg_type, output=output,
        time_run=time_run, is_exported=is_exported, skipped=skipped, pinned=pinned,
        i_collapsed=i_collapsed, o_collapsed=o_collapsed, heading_collapsed=heading_collapsed)

# %% ../nbs/00_core.ipynb
def del_msg(
    msgid:str=None, # id of message to delete
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Delete a message from the dialog."
    call_endp('rm_msg_', dname, raiseex=True, msid=msgid)

# %% ../nbs/00_core.ipynb
@delegates(add_msg)
def _add_msg_unsafe(
    content:str, # Content of the message (i.e the message prompt, code, or note text)
    placement:str='add_after', # Can be 'add_after', 'add_before', 'at_start', 'at_end'
    msgid:str=None, # id of message that placement is relative to (if None, uses current message)
    run:bool=False, # For prompts, send it to the AI; for code, execute it (*DANGEROUS -- be careful of what you run!)
    dname:str='', # Running dialog to get info for; defaults to current dialog
    **kwargs
):
    """Add/update a message to the queue to show after code execution completes, and optionally run it. Be sure to pass a `sid` (stable id) not a `mid` (which is used only for sorting, and can change).
    *WARNING*--This can execute arbitrary code, so check carefully what you run!--*WARNING"""
    if placement not in ('at_start','at_end') and not msgid: msgid = find_msg_id()
    return call_endp(
        'add_relative_', dname, content=content, placement=placement, msgid=msgid, run=run, **kwargs)

# %% ../nbs/00_core.ipynb
def _umsg(
    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, # For prompts/code, the output
    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?
): ...

# %% ../nbs/00_core.ipynb
@delegates(_umsg)
def update_msg(
    msgid:str=None, # id of message to update (if None, uses current message)
    msg:Optional[Dict]=None, # Dictionary of field keys/values to update
    dname:str='', # Running dialog to get info for; defaults to current dialog
    **kwargs):
    """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 not msgid and not msg: raise TypeError("update_msg needs either a dict message or `msgid=`")
    return call_endp('add_relative_', dname, placement='update', msgid=msgid, **kwargs)

# %% ../nbs/00_core.ipynb
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)
):
    "Read URL as markdown, and add a note below current message with the result"
    res = read_url(url, as_md=True, extract_section=extract_section, selector=selector)
    return add_msg(res)

# %% ../nbs/00_core.ipynb
def ast_py(code:str):
    "Get an SgRoot root node for python `code`"
    from ast_grep_py import SgRoot
    return SgRoot(code, "python").root()

# %% ../nbs/00_core.ipynb
def ast_grep(
    pattern:str, # ast-grep pattern to search
    path=".", # path to recursively search for files
    lang="python" # language to search/scan
):
    "Use the `ast-grep` command to find `pattern` in `path`"
    import json, subprocess
    cmd = f"ast-grep --pattern '{pattern}' --lang {lang} --json=compact"
    if path != ".": cmd = f"cd {path} && {cmd}"
    res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return json.loads(res.stdout) if res.stdout else res.stderr

# %% ../nbs/00_core.ipynb
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)?
    msgid:str=None,  # Message id to find (defaults to current message)
    view_range:list[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF
    nums:bool=False, # Whether to show line numbers
    dname:str='' # Running dialog to get info for; defaults to current dialog
    ):
    "Get the `Message` object indexed in the current dialog."
    if not msgid: msgid = find_msg_id()
    data = dict(n=n, relative=relative, msgid=msgid)
    if view_range: data['view_range'] = view_range # None gets converted to '' so we avoid passing it to use the p.default
    if nums: data['nums'] = nums
    return call_endp('read_msg_', dname, json=True, **data)

# %% ../nbs/00_core.ipynb
def run_msg(
    msgid:str=None, # id of message to execute
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Adds a message to the run queue. Use read_msg to see the output once it runs."
    return call_endp('add_runq_', dname, msgid=msgid, api=True)

# %% ../nbs/00_core.ipynb
def msg_insert_line(
    msgid:str,  # Message id to edit
    insert_line: int, # The line number after which to insert the text (0 for beginning of file)
    new_str: str, # The text to insert
    dname:str='' # Running dialog to get info for; defaults to current dialog
    ):
    "Insert text at a specific line number in a message"
    return call_endp('msg_insert_line_', dname, json=True, msgid=msgid, insert_line=insert_line, new_str=new_str)

# %% ../nbs/00_core.ipynb
def msg_str_replace(
    msgid:str,  # Message id to edit
    old_str: str, # Text to find and replace
    new_str: str, # Text to replace with
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Replace first occurrence of old_str with new_str in a message"
    return call_endp('msg_str_replace_', dname, json=True, msgid=msgid, old_str=old_str, new_str=new_str)

# %% ../nbs/00_core.ipynb
def msg_strs_replace(
    msgid:str,  # Message id to edit
    old_strs:list[str], # List of strings to find and replace
    new_strs:list[str], # List of replacement strings (must match length of old_strs)
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Replace multiple strings simultaneously in a message"
    return call_endp('msg_strs_replace_', dname, json=True, msgid=msgid, old_strs=old_strs, new_strs=new_strs)

# %% ../nbs/00_core.ipynb
def msg_replace_lines(
    msgid:str,  # Message id to edit
    start_line:int, # Starting line number to replace (1-based indexing)
    end_line:int, # Ending line number to replace (1-based indexing, inclusive)
    new_content:str, # New content to replace the specified lines
    dname:str='' # Running dialog to get info for; defaults to current dialog
):
    "Replace a range of lines with new content in a message"
    return call_endp('msg_replace_lines_', dname, json=True, msgid=msgid, start_line=start_line, end_line=end_line, new_content=new_content)

# %% ../nbs/00_core.ipynb
def load_gist(gist_id:str):
    "Retrieve a gist"
    api = GhApi()
    if '/' in gist_id: *_,user,gist_id = gist_id.split('/')
    else: user = None
    return api.gists.get(gist_id, user=user)

# %% ../nbs/00_core.ipynb
def gist_file(gist_id:str):
    "Get the first file from a gist"
    gist = load_gist(gist_id)
    return first(gist.files.values())

# %% ../nbs/00_core.ipynb
def import_string(
    code:str, # Code to import as a module
    name:str  # Name of module to create
):
    with TemporaryDirectory() as tmpdir:
        path = Path(tmpdir) / f"{name}.py"
        path.write_text(code)
        # linecache.cache storage allows inspect.getsource() after tmpdir lifetime ends
        linecache.cache[str(path)] = (len(code), None, code.splitlines(keepends=True), str(path))
        spec = importlib.util.spec_from_file_location(name, path)
        module = importlib.util.module_from_spec(spec)
        sys.modules[name] = module
        spec.loader.exec_module(module)
        return module

# %% ../nbs/00_core.ipynb
empty = Parameter.empty

def is_usable_tool(func:callable):
    "True if the function has a docstring and all parameters have types, meaning that it can be used as an LLM tool."    
    if not func.__doc__ or not callable(func): return False
    return all(p.annotation != empty for p in signature(func).parameters.values())

# %% ../nbs/00_core.ipynb
def mk_toollist(syms):
    return "\n".join(f"- &`{sym.__name__}`: {sym.__doc__}" for sym in syms if is_usable_tool(sym))

# %% ../nbs/00_core.ipynb
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"
    fil = gist_file(gist_id)
    mod_name = mod_name or Path(fil['filename']).stem
    module = import_string(fil['content'], mod_name)
    glbs = currentframe().f_back.f_globals
    if add_global: glbs[mod_name] = module
    syms = getattr(module, '__all__', None)
    if syms is None: syms = [o for o in dir(module) if not o.startswith('_')]
    syms = [getattr(module, nm) for nm in syms]
    if import_wildcard:
        for sym in syms: glbs[sym.__name__] = sym
    if create_msg:
        pref = getattr(module, '__doc__', "Tools added to dialog:")
        add_msg(f"{pref}\n\n{mk_toollist(syms)}")
    return module

# %% ../nbs/00_core.ipynb
def tool_info():
    cts='''Tools available from `dialoghelper`:

- &`curr_dialog`: Get the current dialog info.
- &`msg_idx`: Get absolute index of message in dialog.
- &`add_html`: Send HTML to the browser to be swapped into the DOM using hx-swap-oob.
- &`find_msg_id`: Get the current message id.
- &`find_msgs`: Find messages in current specific dialog that contain the given information.
  - (solveit can often get this id directly from its context, and will not need to use this if the required information is already available to it.)
- &`read_msg`: Get the message indexed in the current dialog.
  - To get the exact message use `n=0` and `relative=True` together with `msgid`.
  - 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.
- &`del_msg`: Delete a message from the dialog.
- &`add_msg`: Add/update a message to the queue to show after code execution completes.
- &`update_msg`: Update an existing message.
- &`url2note`: Read URL as markdown, and add a note below current message with the result
- &`msg_insert_line`: Insert text at a specific location in a message.
- &`msg_str_replace`: Find and replace text in a message.
- &`msg_strs_replace`: Find and replace multiple strings in a message.
- &`msg_replace_lines`: Replace a range of lines in a message with new content.
  - Always first use `read_msg( msgid=msgid, n=0, relative=True, nums=True)` to view the content with line numbers.'''
    add_msg(cts)

# %% ../nbs/00_core.ipynb
def fc_tool_info():
    cts='''Tools available from `fastcore.tools`:

- &`rg`: Run the `rg` command with the args in `argstr` (no need to backslash escape)
- &`sed`: Run the `sed` command with the args in `argstr` (e.g for reading a section of a file)
- &`view`: View directory or file contents with optional line range and numbers
- &`create`: Creates a new file with the given content at the specified path
- &`insert`: Insert new_str at specified line number
- &`str_replace`: Replace first occurrence of old_str with new_str in file
- &`strs_replace`: Replace for each str pair in old_strs,new_strs
- &`replace_lines`: Replace lines in file using start and end line-numbers'''
    add_msg(cts)
    add_msg('from fastcore.tools import *', msg_type='code')
