Skill with Additional Python Resource¶
This notebook demonstrates using a skill bundle that includes a Python script resource. The word-frequency skill bundles:
scripts/word_freq.py— a script that reads text from stdin and prints the top-10 most frequent words as a markdown table
The skill instructs the agent to execute the script using PythonInterpreterTool, passing the user's text as stdin. The user supplies any text passage directly in their task instruction. Word counting is a task LLMs get wrong on raw text, so the script guarantees deterministic, accurate results.
The skill bundle is at .agents/skills/word-frequency/ and is discovered automatically when running from more-examples/ch06/.
Note: Run this notebook from within the more-examples/ch06/ directory so the word-frequency skill is discovered as a project-scoped skill.
Required tools¶
By default an LLMAgent.TaskHandler is constructed with the UseSkillTool if any skills are found during discovery. However, in cases where the skill has additional resources such as assets or scripts, then the LLM agent would require more tools to properly execute the skill. I've gone ahead and added two tools in the llm_agents_from_scratch.tools.default subpackage: ReadFileTool and PythonInterpreterTool. For convenience, these can also be imported via the TOOLS_FOR_SKILL_RESOURCES constant in llm_agents_from_scratch.skills.
Setup Instructions¶
To ensure you have the required dependencies to run this notebook, you'll need to have our llm-agents-from-scratch framework installed on the running Jupyter kernel. To do this, you can launch this notebook with the following command while within the project's root directory:
uv run --with jupyter jupyter lab
Alternatively, if you just want to use the published version of llm-agents-from-scratch without local development, you can install it from PyPI by uncommenting the cell below.
Setting up the LLM¶
I recommend running these notebooks on Ollama Cloud. It serves much larger models than most laptops can run. The default cloud model is picked by benchmarking candidates on these notebooks, and an automated run checks every notebook against it each week. The notebook status page shows the current model and results.
- Create an API key at https://ollama.com/settings/keys.
- Export it before launching Jupyter:
export OLLAMA_API_KEY=<your key>. - That's all. When a notebook builds its LLM, it detects the key and uses Ollama Cloud.
Prefer to run locally? Install Ollama from https://ollama.com/download
and pull the model with ollama pull qwen3:14b. If no key is set, the
notebook starts the Ollama service for you, so there is no need to run
ollama serve. Local models are smaller, so your outputs will differ from
the recorded ones.
Prefer OpenAI or Anthropic?
- Install the extra:
uv sync --extra openaioruv sync --extra anthropic. - Export
OPENAI_API_KEYorANTHROPIC_API_KEYbefore launching Jupyter. If you forget, you will be prompted for it, which keeps the key out of the saved notebook. - Set
LLM_PROVIDER=openai(oranthropic), or passprovider="openai"tomake_llm(). A key on its own never switches providers, so one exported for unrelated work cannot reroute you.
The switch applies wherever a notebook builds its LLM with make_llm().
A notebook that constructs OllamaLLM directly stays on Ollama. Output on
gpt-5 or Claude will differ from the recorded outputs, and prompt-sensitive
examples (the ch09 evaluator pattern, ch08 supervised trajectories) may
behave noticeably differently.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPI
# !pip install llm-agents-from-scratch
from llm_agents_from_scratch.notebook_utils import make_llm
Inspecting the skill bundle¶
Before running the agent, let's verify the skill is discovered and that Skill.resources picks up scripts/word_freq.py.
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.data_structures import Task
llm = make_llm()
agent = LLMAgent(llm=llm)
task = Task(instruction="placeholder")
handler = LLMAgent.TaskHandler(llm_agent=agent, task=task)
skill = handler.skills_registry["word-frequency"]
print(f"Name: {skill.frontmatter.name}")
print(f"Scope: {skill.scope}")
print("Resources:")
for r in skill.resources:
print(f" - {r}")
print()
print("--- Body ---")
print(skill.read_body())
✓ Using Ollama Cloud (kimi-k2.7-code:cloud) Name: word-frequency Scope: SkillScope.PROJECT Resources: - scripts/word_freq.py --- Body --- # Word Frequency This skill counts word frequencies in a text passage provided by the user and reports the top-10 results as a markdown table. The computation is performed by a Python script to ensure deterministic, accurate counts. ## Steps ### 1. Execute the script Run the script and pass the user's text passage as the `stdin` argument. The script reads from stdin, so the `stdin` argument **must** contain the full text provided by the user: ``` from_scratch__python_interpreter(path="<skill_dir>/scripts/word_freq.py", stdin="<user_text>") ``` Replace `<skill_dir>` with the value of **Skill directory** shown at the bottom of this skill content, and `<user_text>` with the **complete, verbatim** text passage supplied by the user. ### 2. Report the result Report the markdown table printed by the script exactly as-is.
Running the agent¶
Now let's run the agent on a task that includes a text passage. The agent will activate the word-frequency skill and follow its instructions to execute scripts/word_freq.py with the user's text piped as stdin.
import logging
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
from llm_agents_from_scratch import TOOLS_FOR_SKILL_RESOURCES
PASSAGE = """\
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
"""
agent_with_tools = LLMAgent(llm=llm, tools=TOOLS_FOR_SKILL_RESOURCES)
result = await agent_with_tools.run_with_skill("word-frequency", prompt=PASSAGE)
print(result)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: This is a user-explicit skill activation. Call the from_scratch__use_skill tool with name='word-frequency'. Use exactly this name — i...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: This is a user-explicit skill activation. Call the from_scratch__use_skill tool with name='word-frequency'. Use exactly this name ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_skill
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: <skill_content name="word-frequency">
# Word Frequency
This skill counts word frequencies in a text passage provided by the u...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "c98ae04d-eb68-484a-bb2c-3f3abdaf8fa9",
"tool_name": "from_scratch__python_in...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the from_scratch__python_interpreter tool call with id c98ae04d-eb68-484a-bb2c-3f3abdaf8fa9, passing the user's full text via stdi...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the from_scratch__python_interpreter tool call with id c98ae04d-eb68-484a-bb2c-3f3abdaf8fa9, passing the user's full text ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__python_interpreter
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: **Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | is | 10 |
| 2 | better | 8 |
| 3 | than | 8...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: **Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | is | 10 |
| 2 | better | 8 |
| 3 | than | 8 |
| 4 | ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: **Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | is | 10 |
| 2 | better | 8 |
| 3 | than | 8 |
| 4...[TRUNCATED]
**Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | is | 10 |
| 2 | better | 8 |
| 3 | than | 8 |
| 4 | to | 5 |
| 5 | the | 5 |
| 6 | although | 3 |
| 7 | never | 3 |
| 8 | be | 3 |
| 9 | one | 3 |
| 10 | idea | 3 |