With and Without Skills — Word Frequency¶
This notebook is a continuation of
additional_resource_python.ipynb. It reuses the sameword-frequencyskill, so reading that notebook first is recommended.
This notebook illustrates the core value proposition of skills: reusability. Running a word-frequency task without a skill requires the caller to know the implementation details: the script path, that text must be piped as stdin, how to invoke the tool. A skill packages all of that knowledge once, so every future call is just run_with_skill("word-frequency", prompt=text) regardless of the input.
Note: An LLM backbone alone — without any tools — cannot perform exact word counting reliably. This notebook uses
TOOLS_FOR_SKILL_RESOURCESin both runs; the skill's role is not to add capability but to encode how to use the available tools correctly.
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
Setup¶
from llm_agents_from_scratch.notebook_utils import make_llm
import logging
from llm_agents_from_scratch import TOOLS_FOR_SKILL_RESOURCES, LLMAgent
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
llm = make_llm()
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!
"""
✓ Using Ollama Cloud (kimi-k2.7-code:cloud)
Without Skill¶
The agent has TOOLS_FOR_SKILL_RESOURCES and is handed the script path directly in the prompt. It correctly calls from_scratch__python_interpreter with the passage as stdin, but only because the prompt spelled out exactly what to do. Every new use of this workflow requires the same prompt template as well as the additional python script from the caller.
from pathlib import Path
script_path = Path(
".agents/skills/word-frequency/scripts/word_freq.py",
).resolve()
task = Task(
instruction=(
"What are the top-10 most frequent words in the following passage?"
f" Use the script at {script_path} to compute the answer.\n\n{PASSAGE}"
),
)
agent_without = LLMAgent(llm=llm, tools=TOOLS_FOR_SKILL_RESOURCES)
result = await agent_without.run(task, skills_scopes=[], max_steps=5)
print(result)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: What are the top-10 most frequent words in the following passage? Use the script at /home/nerdai/Projects/llm-agents-from-scratch/mor...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: What are the top-10 most frequent words in the following passage? Use the script at /home/nerdai/Projects/llm-agents-from-scratch/...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__read_file
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: """Read text from stdin and print the top-10 most frequent words.
Output is a markdown table.
"""
import re
import sys
from ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "9c06e637-ee26-4896-9a4c-f6f39975224a",
"tool_name": "from_scratch__python_in...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the from_scratch__python_interpreter tool call with id 9c06e637-ee26-4896-9a4c-f6f39975224a: run /home/nerdai/Projects/llm-agents-...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the from_scratch__python_interpreter tool call with id 9c06e637-ee26-4896-9a4c-f6f39975224a: run /home/nerdai/Projects/llm...[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 |
With Skill¶
With the word-frequency skill the caller doesn't need to know anything about the script or stdin. The skill encodes that knowledge — run_with_skill() is the entire interface.
agent_with_skill = LLMAgent(llm=llm, tools=TOOLS_FOR_SKILL_RESOURCES)
result = await agent_with_skill.run_with_skill(
"word-frequency",
prompt=PASSAGE,
max_steps=5,
)
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_": "1de1e92b-451c-445b-940c-bef25239467c",
"tool_name": "from_scratch__python_in...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the from_scratch__python_interpreter tool call with id 1de1e92b-451c-445b-940c-bef25239467c, passing path='/home/nerdai/Projects/l...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the from_scratch__python_interpreter tool call with id 1de1e92b-451c-445b-940c-bef25239467c, passing path='/home/nerdai/Pr...[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: Here is the result from the word-frequency script:
**Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | ...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: Here is the result from the word-frequency script:
**Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1...[TRUNCATED]
Here is the result from the word-frequency script:
**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 |
Running on a Different Passage¶
The skill makes reuse trivial. Here is the same one-liner on a completely different text. No additional prompt engineering, no knowledge of script paths or stdin required.
PASSAGE_2 = """\
It was the best of times, it was the worst of times,
it was the age of wisdom, it was the age of foolishness,
it was the epoch of belief, it was the epoch of incredulity,
it was the season of Light, it was the season of Darkness,
it was the spring of hope, it was the winter of despair.
"""
result = await agent_with_skill.run_with_skill(
"word-frequency",
prompt=PASSAGE_2,
)
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_": "ff57908a-3f18-425e-9fde-88bf1208f3aa",
"tool_name": "from_scratch__python_in...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the planned tool call: run from_scratch__python_interpreter with path '/home/nerdai/Projects/llm-agents-from-scratch/more-examples...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the planned tool call: run from_scratch__python_interpreter with path '/home/nerdai/Projects/llm-agents-from-scratch/more-...[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 | it | 10 |
| 2 | was | 10 |
| 3 | the | 10 |...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: **Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | it | 10 |
| 2 | was | 10 |
| 3 | the | 10 |
| 4 | of...[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 | it | 10 |
| 2 | was | 10 |
| 3 | the | 10 |
| 4 |...[TRUNCATED]
**Top-10 word frequencies**
| Rank | Word | Count |
|------|------|-------|
| 1 | it | 10 |
| 2 | was | 10 |
| 3 | the | 10 |
| 4 | of | 10 |
| 5 | times | 2 |
| 6 | age | 2 |
| 7 | epoch | 2 |
| 8 | season | 2 |
| 9 | best | 1 |
| 10 | worst | 1 |
Key Takeaway¶
A skill is a reusable, self-contained bundle and not just a capability. Without the word-frequency skill, the caller is responsible for managing two separate artifacts on their own:
- the prompt template (script path,
stdinconvention, instructions for the agent) - the Python script (
word_freq.py) that actually does the computation
Both must be stored, potentially versioned, and resupplied on every invocation. If the script moves or the invocation details change, every prompt that references it must be updated as well.
A skill bundles both artifacts together in a single directory (SKILL.md + scripts/). That bundle travels with the project, can be committed to version control, and is discovered automatically by any LLMAgent running in that directory. The same skill can be shared across team members, reused by different agent clients, and invoked identically by all of them with one line:
agent.run_with_skill("word-frequency", prompt=text)
On LLM backbone reliability: without
PythonInterpreterToola language model cannot perform exact word counting on raw text. LLMs are well known to miscount tokens and words. The skill doesn't add that capability;PythonInterpreterTooldoes. What the skill adds is the encoded knowledge of how to use that tool correctly, so no caller ever has to rediscover it.