Metadata-Version: 2.4
Name: solveit_dmtools
Version: 0.0.30
Summary: Personalized tools to add functionality to SolveIt
Author-email: Duane Milne <duane.milne@gmail.com>
License: Apache-2.0
Project-URL: Repository, https://github.com/shuane/solveit_dmtools
Project-URL: Documentation, https://shuane.github.io/solveit_dmtools
Keywords: nbdev,jupyter,notebook,python
Classifier: Natural Language :: English
Classifier: Intended Audience :: Developers
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dialoghelper
Requires-Dist: contextkit
Requires-Dist: lisette
Dynamic: license-file

# solveit_dmtools


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Usage and examples

``` python
from dialoghelper.core import *
from solveit_dmtools import * # core, dhb, dhp, fab
import solveit_dmtools # This will be just for the next cell to print the version
```

``` python
print(solveit_dmtools.__version__); del solveit_dmtools
```

    0.0.30

**TL;DR:**

| module | what it gives you | try |
|----|----|----|
| `dhb` | chat with any LiteLLM model you have a key for | `bc = dhb.c('gemini/gemini-flash-lite-latest'); bc("hi")` |
| `dhp` | Polya’s four stages as one-liners that write prompt cells | `dhp.help()`, `dhp.u.summary()` |
| `fab` | 200+ open-source [fabric](https://github.com/danielmiessler/fabric) prompts | `fab.p.summarize()` |

Full docs for each module are on its own page; `await add_msg(modulename.doc, msg_type='note')` prints more details in a cell if you’d rather stay here.

## dhb - Backup Chat for SolveIt

First calling with no model will prompt you to type in part of a model name to search

``` python
dhb.c() # `dhb.c()` with no model prompts you for a substring and lists matching models
```

    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/gem':
    gemini/gemini-live-2.5-flash-preview-native-audio-09-2025
    gemini/gemini-robotics-er-1.5-preview
    gemini/gemini-robotics-er-2-preview
    gemini/gemini-robotics-er-1.6-preview
    gemini/gemini-embedding-001
    gemini/gemini-embedding-2-preview
    gemini/gemini-embedding-2
    gemini/gemini-1.5-flash
    gemini/gemini-2.0-flash
    gemini/gemini-2.0-flash-001
    gemini/gemini-2.0-flash-lite
    gemini/gemini-2.5-flash
    gemini/gemini-2.5-flash-image
    gemini/gemini-3-pro-image
    gemini/gemini-3-pro-image-preview
    gemini/gemini-3.1-flash-image
    gemini/gemini-3.1-flash-image-preview
    gemini/gemini-2.5-flash-lite
    gemini/gemini-2.5-flash-lite-preview-09-2025
    gemini/gemini-2.5-flash-preview-09-2025
    gemini/gemini-flash-latest
    gemini/gemini-flash-lite-latest
    gemini/gemini-2.5-flash-lite-preview-06-17
    gemini/gemini-2.5-flash-preview-tts
    gemini/gemini-2.5-pro
    gemini/gemini-2.5-computer-use-preview-10-2025
    gemini/gemini-3-pro-preview
    gemini/gemini-3.1-flash-lite-preview
    gemini/gemini-3.1-flash-lite
    gemini/gemini-3.5-flash-lite
    gemini/gemini-3-flash-preview
    gemini/gemini-3.5-flash
    gemini/gemini-3.6-flash
    gemini/gemini-omni-flash-preview
    gemini/gemini-3.1-pro-preview
    gemini/gemini-3.1-pro-preview-customtools
    gemini/gemini-2.5-pro-preview-tts
    gemini/gemini-exp-1114
    gemini/gemini-exp-1206
    gemini/gemini-gemma-2-27b-it
    gemini/gemini-gemma-2-9b-it
    gemini/gemma-3-27b-it
    gemini/gemini-2.0-flash-exp-image-generation
    gemini/gemini-2.0-flash-lite-001
    gemini/gemini-2.5-flash-native-audio-latest
    gemini/gemini-2.5-flash-native-audio-preview-09-2025
    gemini/gemini-2.5-flash-native-audio-preview-12-2025
    gemini/gemini-3.1-flash-live-preview
    gemini/gemini-pro-latest
    ### The following ones are listed by OpenRouter but not LiteLLM (may still work)

    <solveit_dmtools.dhb.BackupChat>

``` python
bc = dhb.c("openrouter/openai/gpt-5.2-codex")
# bc = dhb.c("openrouter/moonshotai/kimi-k2.5")
```

The following will be automatically commented out when run, then a prompt cell is added after it with input/output from the other LLM.

``` python
bc("Hi, can you use tools?")
```

Yes—I can use the available tools (e.g., `read_url`), but I’ll ask for confirmation before accessing any URLs. What would you like to do?

<details>

- id: `gen-1786095705-Y8KGvpeB6uBr8yLeAjp2`
- model: `openai/gpt-5.2-codex`
- finish_reason: `stop`
- usage: `Usage(completion_tokens=71, prompt_tokens=1738, total_tokens=1809, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=97, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=0, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0040355, is_byok=False, cost_details={'upstream_inference_cost': 0.0040355, 'upstream_inference_prompt_cost': 0.0030415, 'upstream_inference_completions_cost': 0.000994})`

</details>

**Prompt (openrouter/openai/gpt-5.2-codex):** Hi, can you use tools?

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

Yes—I can use the available tools (e.g., `read_url`), but I’ll ask for confirmation before accessing any URLs. What would you like to do?

``` json
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 1738, "completion_tokens": 71, "total_tokens": 1809, "cached_tokens": 0, "cache_creation_tokens": 0, "cost": 0.0043995}
```

``` python
def bad_joke():
    "Tells a bad joke"
    return "Why are engineers bad at telling jokes timing?"
```

``` python
bc.add_tools('bad_joke')
len(bc.tool_schemas)
```

    2

``` python
%bc Tell me a bad joke using your tools please
```

Why are engineers bad at telling jokes timing?

<details>

- id: `gen-1786095857-BqlLTP3a2yngmpz7ZTGr`
- model: `openai/gpt-5.2-codex`
- finish_reason: `stop`
- usage: `Usage(completion_tokens=13, prompt_tokens=1922, total_tokens=1935, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=1792, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0007231, is_byok=False, cost_details={'upstream_inference_cost': 0.0007231, 'upstream_inference_prompt_cost': 0.0005411, 'upstream_inference_completions_cost': 0.000182})`

</details>

**Prompt (openrouter/openai/gpt-5.2-codex):** Tell me a bad joke using your tools please

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

``` json
{
  "id": "call_tXyLFSyyNoargmQNSStFu1c7",
  "name": "bad_joke",
  "args": {},
  "result": "Why are engineers bad at telling jokes timing?"
}
```

Why are engineers bad at telling jokes timing?

``` json
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 3811, "completion_tokens": 44, "total_tokens": 3855, "cached_tokens": 1792, "cache_creation_tokens": 0, "cost": 0.00446285}
```

**Prompt (openrouter/openai/gpt-5.2-codex):** Tell me a bad joke using your tools please

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

I don’t have access to `bad_joke` in this session. Can you add it with `bc.add_tools('bad_joke')` (or `bc.add_vars_and_tools`) and then share it here?

``` json
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 1874, "completion_tokens": 114, "total_tokens": 1988, "cached_tokens": 1536, "cache_creation_tokens": 0, "cost": 0.0024563}
```

``` python
bc("Can you please read https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md and give the elevator pitch and some sample code? No need to store the raw content.")
```

Reading that URL is fine if you confirm. Want me to fetch it now with `read_url`?

<details>

- id: `gen-1786095913-XosONBUH1ZYe2DxcSfAS`
- model: `openai/gpt-5.2-codex`
- finish_reason: `stop`
- usage: `Usage(completion_tokens=41, prompt_tokens=2650, total_tokens=2691, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=1664, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0025907, is_byok=False, cost_details={'upstream_inference_cost': 0.0025907, 'upstream_inference_prompt_cost': 0.0020167, 'upstream_inference_completions_cost': 0.000574})`

</details>

**Prompt (openrouter/openai/gpt-5.2-codex):** Can you please read https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md and give the elevator pitch and some sample code? No need to store the raw content.

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

Reading that URL is fine if you confirm. Want me to fetch it now with `read_url`?

``` json
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 2650, "completion_tokens": 41, "total_tokens": 2691, "cached_tokens": 1664, "cache_creation_tokens": 0, "cost": 0.0025907}
```

The above is built-in to ask for confirmation… some models will not bother to ask each time, though.

``` python
%%bc
Yes that
is fine
```

Here’s a concise summary based on the README you linked.

**Elevator pitch**  
**fhdaisy** is a Python wrapper that maps DaisyUI components to FastHTML-friendly Python components. Instead of writing raw HTML with class strings (e.g., `class="btn btn-primary"`), you use components like `Btn('Click me', cls='-primary')`. Every DaisyUI class becomes a title‑case component, and modifiers can be shortened (`btn-primary` → `-primary`). There’s also an `xtras` module with helper builders (e.g., accordions) to reduce repetitive markup.

**Sample code (minimal examples)**

``` python
from fhdaisy import *
c = Btn("Hey there", cls="-primary")
```

``` python
from fhdaisy import *
c = Alert("Saved!", cls="-success -soft")
```

``` python
from fhdaisy.xtras import *
acc = mk_accordion(("Q1", "A1"), ("Q2", "A2"), itemcls="-arrow")
```

------------------------------------------------------------------------

Want a specific example (e.g., cards, forms, modals) or a tiny FastHTML page stub to see these components in context?

<details>

- id: `gen-1786095942-VNg6t7CR0H1QZMAR9Fu4`
- model: `openai/gpt-5.2-codex`
- finish_reason: `stop`
- usage: `Usage(completion_tokens=250, prompt_tokens=16259, total_tokens=16509, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=2944, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.02731645, is_byok=False, cost_details={'upstream_inference_cost': 0.02731645, 'upstream_inference_prompt_cost': 0.02381645, 'upstream_inference_completions_cost': 0.0035})`

</details>

**Prompt (openrouter/openai/gpt-5.2-codex):** Yes that
is fine

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

``` json
{
  "id": "call_8gSnY8pSbnzVTUt3UIPFo84Y",
  "name": "read_url",
  "args": {
    "url": "https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md",
    "as_md": true,
    "extract_section": true,
    "selector": "",
    "ai_img": false
  },
  "result": "# fhdaisy\n\n\n<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->\n\n`fhdaisy` is a Python wrapp<TRUNCATED>"
}
```

Here’s a concise summary based on the README you linked.

**Elevator pitch**  
**fhdaisy** is a Python wrapper that maps DaisyUI components to FastHTML-friendly Python components. Instead of writing raw HTML with class strings (e.g., `class="btn btn-primary"`), you use components like `Btn('Click me', cls='-primary')`. Every DaisyUI class becomes a title‑case component, and modifiers can be shortened (`btn-primary` → `-primary`). There’s also an `xtras` module with helper builders (e.g., accordions) to reduce repetitive markup.

**Sample code (minimal examples)**

``` python
from fhdaisy import *
c = Btn("Hey there", cls="-primary")
```

``` python
from fhdaisy import *
c = Alert("Saved!", cls="-success -soft")
```

``` python
from fhdaisy.xtras import *
acc = mk_accordion(("Q1", "A1"), ("Q2", "A2"), itemcls="-arrow")
```

------------------------------------------------------------------------

Want a specific example (e.g., cards, forms, modals) or a tiny FastHTML page stub to see these components in context?

``` json
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 19266, "completion_tokens": 320, "total_tokens": 19586, "cached_tokens": 5376, "cache_creation_tokens": 0, "cost": 0.0297283}
```

## dhp - Polya helpers for SolveIt

**Dialog Helper for Polya’s Problem-Solving Method**

The dhp module provides quick access to Polya’s four-stage problem-solving process
through interactive prompts.

Each stage has multiple questions/prompts:
- To preview one you can just print it/type its name in a cell and hit Submit
- e.g. `dhp.act.next` shows you “(prompt) What is next?”.
- To execute one you call it by adding () after the name
- e.g. `dhp.act.next()` will replace the current message cell with a prompt cell having “What is next?” in it
- It will be automatically executed, you can hit Esc to stop it and/or Enter to edit the prompt

**TYPICAL FLOW:**
- Start with `dhp.u` (`understand`) (even briefly) to clarify your understanding of the problem
- Move to `dhp.p` (`plan`) to develop initial strategies
- Switch between `plan` and `act` (or `dhp.a`, or `dhp.x`/ `execute`) as you develop and test approaches
- Use `dhp.r` (`review`) to gain deeper understanding of your approach and findings
- You might loop back to other steps after `dhp.r` (`review`)

If you feel stuck, run `dhp.help()` and it will submit the prompt cell it creates - SolveIt will help you pick a next prompt!

The problem we want to solve is demonstrating the dhp module

The following cell is from running `dhp.help()` then running the prompt cell it creates

Please pick an appropriate next-step/prompt from the below:

**Dialog Helper for Polya’s Problem-Solving Method**

This module provides quick access to Polya’s four-stage problem-solving process
through interactive prompts.

Each stage has multiple questions/prompts:
- To preview one you can just print it/type its name in a cell and hit Submit
- e.g. `dhp.act.next` shows you “(prompt) What is next?”.
- To execute one you call it by adding () after the name
- e.g. `dhp.act.next()` will replace the current message cell with a prompt cell having “What is next?” in it
- It will be automatically executed, you can hit Esc to stop it and/or Enter to edit the prompt

**TYPICAL FLOW:**
- Start with `dhp.u` (`understand`) (even briefly) to clarify your understanding of the problem
- Move to `dhp.p` (`plan`) to develop initial strategies
- Switch between `plan` and `act` (or `dhp.a`, or `dhp.x`/`execute`) as you develop and test approaches
- Use `dhp.r` (`review`) to gain deeper understanding of your approach and findings
- You might loop back to other steps after `r` (`review`)

If you feel stuck, run `dhp.help()` and it will submit the prompt cell it creates - SolveIt will help you pick a next prompt!

**UNDERSTAND STAGE - Clarify the problem before solving**
- dhp.u.summary() - Creates a prompt cell asking SolveIt to give a **concise summary** of the problem
- dhp.u.info() - Creates a prompt cell asking SolveIt to inventory **known/unknown information**
- dhp.u.similar() - Creates a prompt cell asking SolveIt if it has **seen a similar problem** before
- dhp.u.lateral() - Creates a prompt cell to **explore problem relationships and scope**
- dhp.u.related() - Creates a prompt cell to **identify similar or simpler problems**
- dhp.u.viz() - Creates a prompt cell asking SolveIt to **create a figure or diagram** to represent the problem
- dhp.u.notation() - Creates a prompt cell asking SolveIt to **pick suitable notation** (symbols for quantities/data, states, transitions)
- dhp.u.simplest() - Creates a prompt cell asking SolveIt for the **simplest way to look at** the problem
- dhp.u.simplify() - Creates a prompt cell asking SolveIt to **separate problem parts** (break down complex conditions into simpler ones)
- dhp.u.objective() - Creates a note cell asking you to **state the objective in one sentence, without reference to algorithms, models, or data**
- dhp.u.levers() - Creates a note cell asking you to **separate what you can control from what you cannot**
- dhp.u.ideal_data() - Creates a prompt cell asking SolveIt **what data you would collect if you could measure anything**

**PLAN STAGE - Develop strategies and approaches**
- dhp.p.chunks() - Creates a prompt cell asking SolveIt to **break down the problem** into smaller sub-problems
- dhp.p.partial() - Creates a prompt cell asking SolveIt if there’s a **smaller part or representation** of the problem to solve
- dhp.p.known_approach() - Creates a prompt cell asking SolveIt to **use a known algorithm or library** to solve the problem
- dhp.p.verifiable() - Creates a prompt cell asking SolveIt **how to verify** if the solution is consistent and correct
- dhp.p.backward() - Creates a prompt cell asking SolveIt to **work backward** from the desired result
- dhp.p.aux() - Creates a prompt cell asking SolveIt to **use an auxiliary element** (variable, diagram, or example) to clarify the path
- dhp.p.analogy() - Creates a prompt cell asking SolveIt to **use analogy or similarity** to relate the problem to a known solution
- dhp.p.review() - Creates a prompt cell asking SolveIt to **critique the plan of attack** (be frank and critical)
- dhp.p.completeness() - Creates a prompt cell asking SolveIt if **you’ve used all the data and taken into account all essential notions**

**ACT STAGE - Execute your plan while monitoring progress**
- dhp.a.all() - Creates a prompt cell asking SolveIt if we **covered all of the data or examples** for this step
- dhp.a.check() - Creates a prompt cell asking SolveIt if **this step seems correct**
- dhp.a.doubt() - Creates a prompt cell asking SolveIt if we’re **using the right approach**
- dhp.a.next() - Creates a prompt cell asking SolveIt **what is next**
- dhp.a.other() - Creates a prompt cell asking SolveIt if there’s **another way to look at this**
- dhp.a.partial() - Creates a prompt cell asking SolveIt about **intermediate results or milestones** to aim for
- dhp.a.simpler() - Creates a prompt cell asking SolveIt if there was a **simpler way** to do this step
- dhp.a.symmetry() - Creates a prompt cell asking SolveIt about **symmetries or patterns** in the problem to exploit
- dhp.a.valid() - Creates a prompt cell asking SolveIt if **this step was a valid step**

**REVIEW STAGE - Verify results, reflect on process, and extract lessons**
- dhp.r.all() - Creates a prompt cell asking SolveIt if we **covered all of the data or examples** for this problem
- dhp.r.alter() - Creates a prompt cell asking SolveIt for **alternative solutions or approaches** that might be more efficient or effective
- dhp.r.general() - Creates a prompt cell asking SolveIt if we can **generalize the solution** to other similar problems
- dhp.r.grok() - Creates a note cell with the text “**To consider:** Can I understand the solution without having to perform all the steps?”
- dhp.r.learned() - Creates a prompt cell asking SolveIt **what lessons have been learned** from this
- dhp.r.mistakes() - Creates a prompt cell asking SolveIt about **common mistakes** made
- dhp.r.other() - Creates a prompt cell asking SolveIt if we can **derive the result differently**
- dhp.r.principles() - Creates a prompt cell asking SolveIt to **identify underlying principles or patterns** that emerged during the solution process
- dhp.r.sanity() - Creates a prompt cell asking SolveIt if the **result makes sense** and can be verified by substitution or another method
- dhp.r.simpler() - Creates a prompt cell asking SolveIt if we can **derive the result in a simpler way**
- dhp.r.test() - Creates a prompt cell asking SolveIt for **different ways to test** this
- dhp.r.framing() - Creates a note cell asking you to **reflect on whether you started with the objective or with a tool**
- dhp.r.levers_overlooked() - Creates a prompt cell asking SolveIt **what levers you might have overlooked**
- dhp.r.future_data() - Creates a prompt cell asking SolveIt **what data you would collect differently next time**

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

Since the stated problem is simply “demonstrating the dhp module,” the natural starting point is the **understand** stage. A good first step is `dhp.u.summary()` — it creates a prompt cell asking for a concise summary of the problem, which will also show what running one of these helpers actually looks like end-to-end.

``` python
dhp.u.summary()
```

``` json
{"model": "claude-sonnet-5", "prompt_tokens": 60981, "completion_tokens": 219, "total_tokens": 61200, "cached_tokens": 8701, "cache_creation_tokens": 52278, "cost": 0.009311400000000001}
```

## fab - leverage Daniel Miessler’s fabric module of shared prompts

``` python
# Uncomment and submit the line below if you do not already have a copy of fabric in your /app/data/fabric folder
#!cd /app/data; git clone --depth 1 https://github.com/danielmiessler/fabric.git
```

``` python
await add_msg(content=fab.doc, msg_type="note")
```

    '_3719b4eb'

**fab - Open Source ‘fabric’ prompts made quickly available in SolveIt**

This module leverages over 200 open source LLM prompts that are available in
Daniel Miesller’s ‘fabric’ project.

If you import as fab, Submit the following to see an overview of all the prompts:
`fab.p`

**HOW TO USE IT**

Most Common Syntax: `prompt="Your Prompt"` in one cell then `fab.p.pattern_name()` in another, where pattern_name is any of the 200+ available fabric patterns.

**MOST IMPORTANT AND USED OPTIONS AND FEATURES**

- **Variable Targeting**: Use `fab.p.pattern_name('variable_name')` to process content from a specific variable instead of the default ‘prompt’ variable.

- **Pattern Discovery**: Use `fab.p.help()` (an alias for suggest_pattern()) to get suggestions of which pattern to pick for your prompt.

- **Compression Feature**: Use `fab.compress()` after running a pattern to save tokens by marking the previous cell as skipped and compressing the output to a new note.

- **Default Variable**: Most patterns work with a variable called ‘prompt’ by default, making it easy to process your main content.

**COMMON PATTERNS**

- For Summarizing Content: `fab.p.summarize()`
- For Explaining Code: `fab.p.explain_code()`
- For Analyzing Claims: `fab.p.analyze_claims()`
- For Extracting Wisdom from Text: `fab.p.extract_wisdom()`
- For Creating Quizzes: `fab.p.create_quiz()`

``` python
fab.p.explain_code
```

    Fabric pattern: explain_code - Analyze/explain code, security tool outputs, and configs.

``` python
prompt = read_url("https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py")
```

The next cell is running `fab.p.explain_code()`, which we then run to get a result.

The call to any `fab.p` function also adds another cell below that has `fab.compress()`, which helps to reduce the tokens for later use.

(From fab.p.explain_code folded below)
\# IDENTITY and PURPOSE

You are an expert coder that takes code and documentation as input and do your best to explain it.

Take a deep breath and think step by step about how to best accomplish this goal using the following steps. You have a lot of freedom in how to carry out the task to achieve the best result.

# OUTPUT SECTIONS

- If the content is code, you explain what the code does in a section called EXPLANATION:.

- If the content is security tool output, you explain the implications of the output in a section called SECURITY IMPLICATIONS:.

- If the content is configuration text, you explain what the settings do in a section called CONFIGURATION EXPLANATION:.

- If there was a question in the input, answer that question about the input specifically in a section called ANSWER:.

# OUTPUT

- Do not output warnings or notes—just the requested sections.

# INPUT:

INPUT:

\$`prompt`

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

EXPLANATION:

This is a [marimo](https://marimo.io) notebook script (a reactive, cell-based Python UI, run with `uv run` given the inline PEP 723 metadata block at the top) that turns a web article into a two-host podcast, complete with synthesized audio.

**Setup cells**
- Declares dependencies (`marimo`, `gaspare`, `openai`, `pydub`, `contextkit`) via the `# ///script` header, so `uv run` can provision them automatically.
- Imports `OpenAI` for text-to-speech, `gaspare` (aliased `gp`) for calling an LLM (Gemini) to write the script, `contextkit.read_url` to fetch and clean web content, and `pydub.AudioSegment` for stitching audio clips together.
- Asserts that `OPENAI_API_KEY` and `GEMINI_API_KEY` are set in the environment, then creates an `OpenAI` client.

**UI form**
- Builds a marimo form with: source URL, output filename, two host names, two OpenAI TTS voice names, a slider for approximate episode length (3–60 min), and a “Run!” button. Nothing downstream executes until the button is clicked (`mo.stop(not run_button.value)` gates every later cell).

**Helper functions**
- `clean_text_for_tts`: repairs common mis-encoded punctuation (broken ellipses, curly quotes), strips non-ASCII characters, collapses whitespace, and NFKD-normalizes the text so it’s safe to feed to a TTS engine.
- `parse_podcast_script`: parses a raw script into a list of `{"speaker": ..., "text": ...}` segments, splitting on lines that start with `HOSTNAME:` (case-insensitive match against the two configured host names) and cleaning each segment’s text.

**Pipeline (only runs once “Run!” is clicked)**
1. `read_url` fetches and converts the source page into text/markdown.
2. A large prompt is built asking Gemini to convert that source material into a two-host podcast script, with detailed instructions on hosts’ personalities, structure, tone, and a strict `HOSTNAME: text` output format, targeting the requested approximate spoken length.
3. `gp.Chat(model="gemini-2.5-pro-preview-03-25")` sends that prompt and the response is parsed into `segments` via `parse_podcast_script`.
4. For each segment, `client.audio.speech.with_streaming_response.create` (OpenAI’s `gpt-4o-mini-tts` model) synthesizes audio using the voice and persona instructions matching that speaker, streaming bytes into a `BytesIO` buffer and loading it as a `pydub.AudioSegment`.
5. All segments are concatenated (`sum(parts, AudioSegment.empty())`) into a single `preview` audio object, previewed inline, then exported to the configured output MP3 path.
6. The generated script text is also cleaned and written to a companion `_script.md` file, and rendered inline as markdown for review.

**Overall purpose**: given nothing but a URL and some UI settings, the script scrapes an article, has an LLM turn it into a natural two-host dialogue script of a target length, and synthesizes it into a playable/downloadable podcast MP3 with two distinct AI voices — all inside an interactive marimo app.

``` json
{"model": "claude-sonnet-5", "prompt_tokens": 68864, "completion_tokens": 1259, "total_tokens": 70123, "cached_tokens": 8701, "cache_creation_tokens": 60161, "cost": 0.011273850000000002}
```

### Prompt (From fab.p.explain_code) for \$`prompt`

EXPLANATION:

This is a [marimo](https://marimo.io) notebook script (a reactive, cell-based Python UI, run with `uv run` given the inline PEP 723 metadata block at the top) that turns a web article into a two-host podcast, complete with synthesized audio.

**Setup cells**
- Declares dependencies (`marimo`, `gaspare`, `openai`, `pydub`, `contextkit`) via the `# ///script` header, so `uv run` can provision them automatically.
- Imports `OpenAI` for text-to-speech, `gaspare` (aliased `gp`) for calling an LLM (Gemini) to write the script, `contextkit.read_url` to fetch and clean web content, and `pydub.AudioSegment` for stitching audio clips together.
- Asserts that `OPENAI_API_KEY` and `GEMINI_API_KEY` are set in the environment, then creates an `OpenAI` client.

**UI form**
- Builds a marimo form with: source URL, output filename, two host names, two OpenAI TTS voice names, a slider for approximate episode length (3–60 min), and a “Run!” button. Nothing downstream executes until the button is clicked (`mo.stop(not run_button.value)` gates every later cell).

**Helper functions**
- `clean_text_for_tts`: repairs common mis-encoded punctuation (broken ellipses, curly quotes), strips non-ASCII characters, collapses whitespace, and NFKD-normalizes the text so it’s safe to feed to a TTS engine.
- `parse_podcast_script`: parses a raw script into a list of `{"speaker": ..., "text": ...}` segments, splitting on lines that start with `HOSTNAME:` (case-insensitive match against the two configured host names) and cleaning each segment’s text.

**Pipeline (only runs once “Run!” is clicked)**
1. `read_url` fetches and converts the source page into text/markdown.
2. A large prompt is built asking Gemini to convert that source material into a two-host podcast script, with detailed instructions on hosts’ personalities, structure, tone, and a strict `HOSTNAME: text` output format, targeting the requested approximate spoken length.
3. `gp.Chat(model="gemini-2.5-pro-preview-03-25")` sends that prompt and the response is parsed into `segments` via `parse_podcast_script`.
4. For each segment, `client.audio.speech.with_streaming_response.create` (OpenAI’s `gpt-4o-mini-tts` model) synthesizes audio using the voice and persona instructions matching that speaker, streaming bytes into a `BytesIO` buffer and loading it as a `pydub.AudioSegment`.
5. All segments are concatenated (`sum(parts, AudioSegment.empty())`) into a single `preview` audio object, previewed inline, then exported to the configured output MP3 path.
6. The generated script text is also cleaned and written to a companion `_script.md` file, and rendered inline as markdown for review.

**Overall purpose**: given nothing but a URL and some UI settings, the script scrapes an article, has an LLM turn it into a natural two-host dialogue script of a target length, and synthesizes it into a playable/downloadable podcast MP3 with two distinct AI voices — all inside an interactive marimo app.

``` json
{"model": "claude-sonnet-5", "prompt_tokens": 68864, "completion_tokens": 1259, "total_tokens": 70123, "cached_tokens": 8701, "cache_creation_tokens": 60161, "cost": 0.011273850000000002}
```

To go back full-circle, we can have a different LLM validate the results… Note that the `%%bc` cell magic allows you to pick a different BackupChat instance (`%bc` line magic does not)

``` python
gemini = dhb.c("gemini/gemini-3.1-pro-preview")
```

``` python
%%bc gemini
Please compare the answer above to https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md and comment on any issues? It's OK, it's not a big download, so go ahead and read it!
```

The previous code explanation was remarkably accurate and aligns perfectly with what the `README.md` describes! The `explain_code` prompt did a great job dissecting the pipeline (Contextkit -\> Gemini 2.5 Pro -\> OpenAI TTS -\> Pydub) exactly as the author intended.

However, cross-referencing with the `README.md` reveals a few practical, user-facing details that the raw code analysis missed:

1.  **Python Version Constraint:** The README explicitly notes that you need a Python version **before 3.13** (specifically recommending 3.12). This is due to a limitation with the `pydub` audio processing library.
2.  **Execution Method:** While the code explanation noted it can be run via `uv run` due to the PEP 723 block, the README specifically recommends running it headlessly/sandboxed with:
    `uvx marimo run --sandbox https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py`
3.  **Patience Required:** The README gives the user a heads-up that clicking “Run!” might take **6 to 10 minutes** to process.

Are there any specific parts of the pipeline (like how it chunks the script or handles the TTS streams) you want to dive deeper into?

<details>

- id: `OW14apiNOcyB-8YPgcW2mAI`
- model: `gemini-3.1-pro-preview`
- finish_reason: `stop`
- usage: `Usage(completion_tokens=1235, prompt_tokens=10316, total_tokens=11551, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=None, reasoning_tokens=948, rejected_prediction_tokens=None, text_tokens=287, image_tokens=None, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=8059, text_tokens=2257, image_tokens=None, video_tokens=None), cache_read_input_tokens=8059)`

</details>

**Prompt (gemini/gemini-3.1-pro-preview):** Please the answer above to https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md and comment on any issues? It’s OK, it’s not a big download, so go ahead and read it!

##### 🤖Reply🤖<!-- SOLVEIT_SEPARATOR_7f3a9b2c -->

``` json
{
  "id": "g4hepim7__thought__EswLCskLARFNMg8h/YeY1L8Iw6DSZ4ywSjFnmvqeqxTw5rQAWun4NunuJMsRwA4dSZ7dHOJ1lbKO6uZQG3z9Hp2y7/UGagi2YVINseLt7LSetvJBKvIJjobShhDo2Zz3N2B5SSMdlQ71bB7GbtICA8TVjaiU+IsH2nHJLElbBY6Mzi3UuHkniGQcEtay0BqY0D/sICEfa43WaEm698v/Y6CmGrGqZwDP441vxCTErekJBBd43A9p89ydbZTMawTm1KrnTMgnxEZcaz3PQFQq7AcbBekPe2bj6KMaPapRGa81SMMAZLjfZLqRp8fRerTR2e1PpC8as06L/r4yKSiyOxiirsLmHshJimIhwVItbUo6ToXpqBkIs4BZ1pZJ3/+Qa/lziyNvP9Ig6KCvroDFWeMSw1TNWGi9uepS9TAkwF/h8R9YtJVAJcV/BNcbdyKdH+8rc1lDPb6R+TettXIIYjnMRijYmCWjNgppx2+1QLOVoe/rNYN74ar0FfRFuWBlZ1g675W1V3vY6erVlN1CNiAiKrHvYZHPdx19phAQlJX+5yYxavQTmMpynfVOuLqUgOsSySxV52ZzdxH+ve5BGsrOMT4wk/aCgfOHhDJYMFOxnGWWmFUNOT5ncQbVPEi4QUwxRV3LBi6ubZhkiDM0NS7eC/OZc/6X8iLmdEuZlGWvMwb7NvXNBKnhfhpVz8q4GCXFy+Wbw4N5TqbP1mqKTVi7OP7dbR55zZ8+aCzJMsq/Zi4pCxRSApMt/kXd7yxawNTq+smzhnhWkCPHO8KntWJT1Eunv3b6xOqlVOqc7RW//tRli22skuUKjlyAqr7xcIGBNUsyfwNArQX79WSXT43VxJ8m+L/6+xjAvfgEHX9+MAEBbwctmwSAGyJyiR6DIOxp+iYpqdW2pp4rdvFcYyTkAOK9dITjGJFnEmriA02RD/zrPQdsV5lihcK4FsbJBqsi9Xl8cqkP4buAsAdshXeqO+URDfsyMngeRJo+4R5u424sEZRc5DI893/HTokU7XzaXmjil3NHyu+TbK3gD+aROxKu6Lz7TmS2C7d77RjaA4JxGPWDCY7CTpx01AmL23wZBxdarX9f/3/nnkV0A0O9mkvC+aSg4mriQbgsszRi9a945Eg4xGViTffeMWRs8m6VDssVEN80Hucj4K7vQ+alZ0nP67DpjaPD/EMKcrLXQmE/ymR+ZBzJHguGP6OYjLp91UByLUoCB7/B4kII9i3/OzVwu3XP0rNVZapNsURorvmOLlUTH2q9Zd5b6e2yHEOMkuPi3vL8iafm9FQ/Hptc3UspQXW6oB2WX6PCgeQD7u8TI84mvunKmh+BJJVqG/7BoaOJyIi54rOs+PzzirJuTGJtSJRqPOH5bGUk/eiZgunzPs79mShJKACzK0+I0+ZMBrhzWxs4+NX4KTCrTE8lbhC1yWyrMVSx/Oi5J8I9xhmHwfRr8oe9YE4Ykng371jZdHi8XaIN/gw++/Kgpg6IfPf1sTUPtg3gofGC3+l7T9k5CoOciPSjkUCzS54AAbVd6j9qPqOL3/mlRR4FTCKKR3yOrDZ+lVIUYNR4NC9QKFr1ThLkGvZRnZk/NXaM6QRauNTqAUbWe+ejL+BtDPhDt8V/8hPEqQLG8Q2u6I91SCQN2HOhwU/VsCmQHE50NJ56tmHxIfdrC1vYuJx3Z1cS+/+4hQ7jnKuIJcJcDX9F4PUfJZ6M7/3lB1KqjWy8UYAXzVxiNyuSrQ1wdZzuJ/iYjBwp42Lo58MRe2SRxSM4XZrAdm4eHyRM+gm0CO9/vYIhsTexPC+/XiQjRR1Xwrj2DH9m6ESD8zmeRasthKJfdsgzkA+SMDe5JjxI2Mu7cWaA060ib9V9ARSyqoYYzib6V7k47SAcyZF0Si2qZDQXPojv9i8bErRaupsLWCUSVjiELxVJVTigw+zvWSla/hkf+dFQFldKjf1qRkO0k/M4FKYCzrRnIQRJfIAbsEg=",
  "name": "read_url",
  "args": {
    "url": "https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md"
  },
  "result": "# blogtopod: Turn blog posts into podcast episodes\n\n*Convert blog posts into podcast episodes using <TRUNCATED>"
}
```

The previous code explanation was remarkably accurate and aligns perfectly with what the `README.md` describes! The `explain_code` prompt did a great job dissecting the pipeline (Contextkit -\> Gemini 2.5 Pro -\> OpenAI TTS -\> Pydub) exactly as the author intended.

However, cross-referencing with the `README.md` reveals a few practical, user-facing details that the raw code analysis missed:

1.  **Python Version Constraint:** The README explicitly notes that you need a Python version **before 3.13** (specifically recommending 3.12). This is due to a limitation with the `pydub` audio processing library.
2.  **Execution Method:** While the code explanation noted it can be run via `uv run` due to the PEP 723 block, the README specifically recommends running it headlessly/sandboxed with:
    `uvx marimo run --sandbox https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py`
3.  **Patience Required:** The README gives the user a heads-up that clicking “Run!” might take **6 to 10 minutes** to process.

Are there any specific parts of the pipeline (like how it chunks the script or handles the TTS streams) you want to dive deeper into?

``` json
{"model": "gemini/gemini-3.1-pro-preview", "prompt_tokens": 19260, "completion_tokens": 1657, "total_tokens": 20917, "cached_tokens": 8059, "cache_creation_tokens": 0, "cost": 0.0438978}
```

## Developer Guide

If you are new to using `nbdev` here are some useful pointers to get you
started.

### Installing solveit_dmtools in Development mode

1.  local dev install:

`$ pip install -e .`

2.  make changes under nbs/ directory: …
3.  prepare for commit:

`$ alias nbprep='nbdev-clean ; nbdev-export'`

`$ nbprep`

``` python
#!pip install -e ..
# OR
#!pip install -Uqq solveit_dmtools
```

### Installation

Install latest from the GitHub
[repository](https://github.com/shuane/solveit_dmtools):

``` sh
$ pip install git+https://github.com/shuane/solveit_dmtools.git
```

or from [pypi](https://pypi.org/project/solveit_dmtools/)

``` sh
$ pip install solveit_dmtools
```

### Documentation

Documentation can be found hosted on this GitHub
[repository](https://github.com/shuane/solveit_dmtools)’s
[pages](https://shuane.github.io/solveit_dmtools/). Additionally you can
find package manager specific guidelines on the
[pypi](https://pypi.org/project/solveit_dmtools/) site.
