{'role': 'user', 'content': '```python\n#|default_exp dhb\n```\nOutput: '}
{'role': 'user', 'content': '```python\n#|export\ndoc = """**Backup Chat for SolveIt using dialoghelper and lisette**\n\nSometimes we may have a problem in SolveIt while Sonnet is down (E300), or maybe we want a different perspective.\n\nThis module helps us to leverage any other LLM that is available to LiteLLM by providing our own keys and the model name.\n\nUsage: \n```python\nfrom solveit_dmtools import dhb\n\n# then in another cell\n# bc = dhb.c() to search model names\nbc = dhb.c("model-name")\n# then in another cell\nbc("Hi")\n```\n"""\n```\nOutput: '}
{'role': 'user', 'content': '```python\n#|export\nimport json\nimport re\nfrom dialoghelper.core import *\nfrom lisette import *\nfrom solveit_dmtools.core import run_async\nfrom typing import Optional, Union\nfrom ipykernel_helper import read_url\nimport inspect\nfrom fastcore.all import patch\n\ndef _default_sanitize(text: str) -> str:\n "Strip [sanitized tool: tool] and [sanitized var: var] references from untrusted input, replacing with a labeled placeholder."\n def replace(m):\n kind = \'var\' if m.group(0)[0] == \'$\' else \'tool\'\n name = re.search(r\'`([^`]*)`\', m.group(0)).group(1).strip()\n return f\'[sanitized {kind}: {name}]\'\n return re.sub(r\'[&$]\\s*`[^`]*`\', replace, text)\n\n_DEFAULT_SP = """You\'re continuing a conversation from another session. Variables are marked as [sanitized var: varname] and tools as [sanitized tool: toolname] in the context.\n\n**Available Resources**\n\nIf you see references to variables or tools that might be relevant but aren\'t fully available, ask the user which ones they want to include by calling their `bc.add_vars`, `bc.add_tools`, or `bc.add_vars_and_tools` methods (if they called their chat instance `bc`). These methods accept either a list of names or a space-delimited string.\n\n**Tool Usage Notes**\n\n- Tool results from earlier conversations may be truncated to ~100 characters. If you need complete information, ask the user to run the tool and store results in a variable, then make that variable available using `bc.add_vars`.\n- You have access to the `read_url` tool, but confirm before reading URLs as access may be expensive.\n\n**Code Execution**\n\nYou cannot run code yourself or store variables. Instead, provide Python code in fenced markdown blocks. The user can execute these in their environment.\n\n**Teaching Approach**\n\nUse a Socratic method - guide through questions rather than providing direct answers - unless the user explicitly requests otherwise. When providing code examples:\n\n- Keep code snippets brief (1-3 lines maximum) unless the user explicitly asks you to write more\n- Encourage the user to implement solutions themselves\n- Ask clarifying questions about their expertise and goals to customize your responses\n"""\n\nclass BackupChat(Chat):\n models = None\n vars_for_hist = None\n model = None\n\n def __init__(self,\n model: str = None,\n sp=None,\n temp=0,\n search=False,\n tools: list = None,\n hist: list = None,\n ns: Optional[dict] = None,\n cache=False,\n cache_idxs: list = [-1],\n ttl=None,\n var_names: Union[list,str] = None,\n hide_msg:bool=False, # whether to hide the cell that includes a BackupChat.__call__\n sanitize_fn=_default_sanitize, # applied to all messages; pass None to disable\n ):\n if sp is None or sp == \'\': sp = _DEFAULT_SP\n if self.models is None:\n self.models = self.get_litellm_models()\n if model is None:\n _m1 = input("Please enter part of a model name to pick your model. Remember you also need to have secret for their API key already defined in your secrets:")\n print(f"Please try again by using e.g. `bc = dhb.c(\'model_name\')` with a model name e.g. pick from these found by searching for \'{_m1}\':")\n # search case-insensitively and return models that match\n print(\'\\n\'.join([m for m in self.models if _m1.lower() in m.lower() or \'###\' in m]))\n return None\n if model not in self.models:\n raise ValueError(f"Model {model} not found in LiteLLM models. Please check the model name or use a different model.")\n self.model = model\n self.hide_msg = hide_msg\n self.sanitize_fn = sanitize_fn\n self.vars_for_hist = dict()\n if var_names is not None:\n self.add_vars(var_names)\n if tools is None:\n tools = [read_url]\n if ns is None:\n ns = inspect.currentframe().f_back.f_globals\n try: self._dname = ns.get(\'__dialog_name\') or find_var(\'__dialog_name\')\n except ValueError: self._dname = \'\'\n super().__init__(model=model, sp=sp, temp=temp, search=search, tools=tools, hist=hist, ns=ns, cache=cache, cache_idxs=cache_idxs, ttl=ttl)\n\n def get_openrouter_ignored(self):\n url = "https://raw.githubusercontent.com/cheahjs/free-llm-api-resources/refs/heads/main/src/data.py"\n code = read_url(url, as_md=False)\n \n # Find the OPENROUTER_IGNORED_MODELS set definition\n pattern = r\'OPENROUTER_IGNORED_MODELS\\s*=\\s*\\{([^}]+)\\}\'\n match = re.search(pattern, code, re.DOTALL)\n models = []\n \n if match:\n # Extract the content and parse the strings\n content = match.group(1)\n models = re.findall(r\'"([^"]+)"\', content)\n return list(models)\n \n def fetch_openrouter_models(self, already_listed:list=None):\n r = read_url("https://openrouter.ai/api/v1/models", as_md=False)\n models = json.loads(r)[\'data\']\n ignored_models = self.get_openrouter_ignored()\n ret_models = []\n for model in models:\n pricing = float(model.get("pricing", {}).get("completion", "1")) + float(\n model.get("pricing", {}).get("prompt", "1")\n )\n if pricing != 0 or ":free" not in model["id"] or model["id"].lower() in [im.lower() for im in ignored_models]:\n continue\n if not (already_listed and model["id"].lower() in [al.replace(\'openrouter/\', \'\').lower() for al in already_listed]):\n ret_models.append(\n {\n "id": f"openrouter/{model[\'id\']}",\n "limits": {\n "requests/minute": 20,\n "requests/day": 50,\n },\n }\n )\n return ret_models\n \n def get_litellm_models(self):\n url = "https://raw.githubusercontent.com/BerriAI/litellm/refs/heads/main/model_prices_and_context_window.json"\n data = read_url(url, as_md=False)\n models = json.loads(data)\n already_listed = [k for k in models.keys() if k != \'sample_spec\']\n return already_listed + [f"### The following ones are listed by OpenRouter but not LiteLLM (may still work)"] + sorted([orm[\'id\'] for orm in self.fetch_openrouter_models(already_listed)])\n \n def add_vars(self, var_names:Union[list,str]=None):\n "Add variables to conversation as user message"\n if isinstance(var_names, str):\n var_names = var_names.split()\n if not isinstance(var_names, list):\n raise ValueError(f"var_names must be a string or list of strings, not {type(var_names)}")\n \n # Add each var to the self.vars_for_hist dictionary\n for v in var_names:\n self.vars_for_hist[v.strip()] = self.ns.get(v.strip(), \'NOT AVAILABLE\')\n```\nOutput: '}
{'role': 'user', 'content': '```python\n#|export\n@patch\nasync def _async_call(self:BackupChat,\n msg=None,\n prefill=None,\n temp=None,\n think=None,\n search=None,\n stream=False,\n max_steps=2,\n final_prompt=\'You have no more tool uses. Please summarize your findings. If you did not complete your goal please tell the user what further work needs to be done so they can choose how best to proceed.\',\n return_all=False,\n var_names=None, # list of variable names to add to the chat\n last_msg=None,\n curr_msg=None,\n **kwargs,\n ):\n dname = \'/\' + self._dname.lstrip(\'/\') if self._dname else \'\'\n msgs = [{k: m[k] for k in [\'id\', \'msg_type\', \'content\', \'output\', \'pinned\', \'skipped\']} for m in await find_msgs(dname=dname, include_output=True, include_skipped=True)]\n if var_names: self.add_vars(var_names)\n if msg and self.sanitize_fn: msg = self.sanitize_fn(msg)\n self.hist = self._build_hist(msgs, last_msg=last_msg)\n start = len(self.hist)\n instance_name = next((k for k, v in self.ns.items() if v is self), None)\n if instance_name and f"{instance_name}(" in curr_msg[\'content\']:\n await update_msg(id=curr_msg[\'id\'], content="# " + curr_msg[\'content\'].replace(\'\\n\', \'\\n# \'), skipped=self.hide_msg, dname=dname)\n response = Chat.__call__(self, msg=msg, prefill=prefill, temp=temp, think=think, search=search, stream=stream, max_steps=max_steps, final_prompt=final_prompt, return_all=return_all, **kwargs)\n output = self._new_msgs_to_output(start)\n if instance_name and f"{instance_name}(" in curr_msg[\'content\']:\n await update_msg(id=curr_msg[\'id\'], o_collapsed=True, dname=dname)\n await add_msg(content=f"**Prompt ({self.model}):** {msg}", output=output, msg_type=\'prompt\', id=curr_msg[\'id\'], dname=dname)\n return response\n\n@patch\ndef __call__(self:BackupChat,\n msg=None,\n prefill=None,\n temp=None,\n think=None,\n search=None,\n stream=False,\n max_steps=2,\n final_prompt=\'You have no more tool uses. Please summarize your findings. If you did not complete your goal please tell the user what further work needs to be done so they can choose how best to proceed.\',\n return_all=False,\n var_names=None, # list of variable names to add to the chat\n msg_id=None, # if provided, use this message id as the anchor instead of the current message\n **kwargs,\n ):\n dname = \'/\' + self._dname.lstrip(\'/\') if self._dname else \'\'\n if msg_id is not None:\n last_msg = call_endp(\'read_msg_\', dname, json=True, id=msg_id, n=-1, relative=True)\n curr_msg = call_endp(\'read_msg_\', dname, json=True, id=msg_id, n=0, relative=True)\n else:\n last_msg = call_endp(\'read_msg_\', dname, json=True, n=-1, relative=True)\n curr_msg = call_endp(\'read_msg_\', dname, json=True, n=0, relative=True)\n return run_async(self._async_call(msg=msg, prefill=prefill, temp=temp, think=think, search=search, stream=stream, max_steps=max_steps, final_prompt=final_prompt, return_all=return_all, var_names=var_names, last_msg=last_msg, curr_msg=curr_msg, **kwargs))\n\n@patch\ndef _build_hist(self:BackupChat, msgs:list, last_msg=None):\n if last_msg is None: curr = len(msgs)-1\n else:\n try: curr = next(i for i,m in enumerate(msgs) if m[\'id\'] == last_msg[\'id\'])\n except StopIteration: curr = len(msgs)-1\n hist = []\n for m in msgs[:curr+1]:\n if m[\'pinned\'] or not m[\'skipped\']:\n eol = \'\\n\'\n san = self.sanitize_fn or (lambda x: x)\n if m[\'msg_type\'] == \'code\': hist.append({\'role\': \'user\', \'content\': f"```python{eol}{san(m[\'content\'])}{eol}```{eol}Output: {san(m.get(\'output\', \'[]\'))}"})\n elif m[\'msg_type\'] == \'note\' or m[\'msg_type\'] == \'raw\': hist.append({\'role\': \'user\', \'content\': san(m[\'content\'])})\n elif m[\'msg_type\'] == \'prompt\':\n hist.append({\'role\': \'user\', \'content\': san(m[\'content\'])})\n if m.get(\'output\'): hist.append({\'role\': \'assistant\', \'content\': san(m[\'output\'])})\n \n hist = hist + self._vars_as_msg() + [{\'role\': \'assistant\', \'content\': \'.\'}] # empty assistant msg to prevent flipping chat msg to look like prefill\n return hist\n\n@patch\ndef _vars_as_msg(self:BackupChat):\n if self.vars_for_hist and len(self.vars_for_hist.keys()):\n content = "Here are the requested variables:\\n" + json.dumps(self.vars_for_hist)\n return [{\'role\': \'user\', \'content\': content}]\n else:\n return []\n\n@patch\ndef _new_msgs_to_output(self:BackupChat, start):\n new_msgs = self.hist[start+1:]\n parts = []\n for i, m in enumerate(new_msgs):\n if m.get(\'role\') == \'assistant\' and m.get(\'tool_calls\'):\n for tc in m[\'tool_calls\']:\n result_msg = next((r for r in new_msgs if r.get(\'tool_call_id\') == tc[\'id\']), None)\n if result_msg: parts.append(self._format_tool_details(tc[\'id\'], tc[\'function\'][\'name\'], json.loads(tc[\'function\'][\'arguments\']), result_msg[\'content\'], is_last_msg=(i == len(new_msgs)-1)))\n elif m.get(\'role\') == \'assistant\' and m.get(\'content\'):\n content = m[\'content\']\n if \'You have no more tool uses\' not in content: parts.append(content)\n return \'\\n\\n\'.join(parts)\n\n@patch\ndef _trunc_tool_result(self:BackupChat, result, max_len=100, is_last_msg=False):\n if len(str(result)) <= max_len or is_last_msg: return result\n return str(result)[:max_len] + \'<TRUNCATED>\'\n\n@patch\ndef _format_tool_details(self:BackupChat, tool_id, func_name, args, result, is_last_msg=False):\n result_str = self._trunc_tool_result(result)\n tool_json = json.dumps({"id": tool_id, "call": {"function": func_name, "arguments": args}, "result": result_str}, indent=2)\n return f"<details class=\'tool-usage-details\'>\\n\\n```json\\n{tool_json}\\n```\\n\\n</details>"\n```\nOutput: '}
{'role': 'user', 'content': '```python\n#|export\n@patch\ndef add_tools(self:BackupChat, tool_names:Union[list,str]=None):\n "Add tools to the chat\'s tool list"\n if isinstance(tool_names, str):\n tool_names = tool_names.split()\n tools = [self.ns.get(t) for t in tool_names if self.ns.get(t)]\n self.tools = list(self.tools or []) + tools\n self.tool_schemas = [lite_mk_func(t) for t in self.tools] if self.tools else None\n \n@patch\ndef add_vars_and_tools(self:BackupChat, var_names:Union[list,str]=None, tool_names:Union[list,str]=None):\n "Add both variables and tools to the chat\'s lists"\n self.add_tools(tool_names)\n self.add_vars(var_names)\n```\nOutput: '}
{'role': 'user', 'content': '```python\n#|export\r\nc = BackupChat\n```\nOutput: '}
{'role': 'user', 'content': "```python\n#|eval: false\nbc = c()\n```\nOutput: Please try again by using e.g. `bc = dhb.c('model_name')` with a model name e.g. pick from these found by searching for 'gemini-3.1':\ngemini-3.1-flash-image-preview\ngemini-3.1-flash-lite-preview\ngemini-3.1-pro-preview\ngemini-3.1-pro-preview-customtools\nvertex_ai/gemini-3.1-pro-preview\nvertex_ai/gemini-3.1-pro-preview-customtools\ngemini/gemini-3.1-flash-image-preview\ngemini/gemini-3.1-flash-lite-preview\ngemini/gemini-3.1-pro-preview\ngemini/gemini-3.1-pro-preview-customtools\nopenrouter/google/gemini-3.1-pro-preview\nvertex_ai/gemini-3.1-flash-image-preview\nvertex_ai/gemini-3.1-flash-lite-preview\n### The following ones are listed by OpenRouter but not LiteLLM (may still work)\n"}
{'role': 'assistant', 'content': '.'}
{'role': 'user', 'content': 'hi'}
Message(content="Hello! It looks like you've set up the `BackupChat` module (`dhb`).\n\nSince you've already initialized the module and seen the list of available models, how would you like to proceed? Are you looking to pick one of those models to start a conversation, or do you have questions about how to use the `bc` instance you're about to create?", role='assistant', tool_calls=None, function_call=None, images=[], thinking_blocks=[], provider_specific_fields={'thought_signatures': ['EjQKMgG+Pvb7YD0VzAVogCEPFpQn2Kom/ifBMHIUQUMYgZm2Mf9JWp83jXb94cQCjT1z4/bV']})