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

# %% auto 0
__all__ = ['Placements', 'empty', 'find_var', 'call_endp', 'find_msgs', 'find_msg_id', 'msg_idx', 'read_msg', 'add_html',
           'del_msg', 'add_msg', 'update_msg', 'load_gist', 'gist_file', 'import_string', 'is_usable_tool',
           'mk_toollist', 'import_gist', '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, json=False, raiseex=False, **data):
    res = xpost(f'http://localhost:5001/{path}', data=data)
    if raiseex: res.raise_for_status()
    return res.json() if json else res.text

# %% ../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?
)->list[dict]:
    "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_', 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 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 msg_idx(
    msgid=None # Message id to find (defaults to current message)
):
    "Get relative index of message in dialog."
    if not msgid: msgid = find_msg_id()
    return call_endp('msg_idx_', json=True, msgid=msgid)['msgid']

# %% ../nbs/00_core.ipynb
def read_msg(
    n:int=-1,     # Message index (if relative, +ve is downwards)
    msgid=None,  # Message id to find (defaults to current message)
    relative:bool=True  # Is `n` relative to current message (True) or absolute (False)?
    ):
    "Get the `Message` object indexed in the current dialog."
    if not msgid: msgid = find_msg_id()
    return call_endp('read_msg_', json=True, msgid=msgid, n=n, relative=relative)['msg']

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

# %% ../nbs/00_core.ipynb
def del_msg(
    msgid:str=None, # id of message that placement is relative to (if None, uses current message)
):
    "Delete a message from the dialog. Be sure to pass a `sid`, not a `mid`."
    call_endp('rm_msg_', raiseex=True, msid=msgid)

# %% ../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?
    **kwargs):
    "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_', 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, **kwargs)

# %% ../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)
    run:bool=False, # For prompts, send it to the AI; for code, execute it (*DANGEROUS -- be careful of what you run!)
    **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"""
    return add_msg(content=content, run=run, **kwargs)

# %% ../nbs/00_core.ipynb
def _umsg(
    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)
    content:str|None=None, # Content of the message (i.e the message prompt, code, or note text)
    msg:Optional[Dict]=None, # Dictionary of field keys/values to update
    **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_', content=content, placement='update', msgid=msgid, **kwargs)

# %% ../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`:

- &`add_html`: Send HTML to the browser to be swapped into the DOM using hx-swap-oob.
- &`find_dialog_id`: Get the current dialog id.
- &`find_msg_id`: Get the current message id.
- &`find_msgs`: Find messages in current specific dialog that contain the given information.
- &`read_msg`: Get the message indexed in the current dialog.
- &`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.'''
    add_msg(cts)
