Chapter 30

Building Your First Skill: From Zero to Published

Chapter 30: Building Your First Skill: From Zero to Published

Theory must eventually meet practice. This chapter walks through a complete hands-on project, building a "Daily News Digest" Skill from scratch โ€” covering project initialization, SKILL.md authoring, tool call integration, local testing, debugging techniques, and every step to publishing on ClawHub. By the end of this chapter, you'll have a Skill that actually runs and can be shared.


30.1 Project Planning: Daily News Digest Skill

Feature Design

We're building: Daily News Digest

Description:
  Fetches today's news on specified topics, generates structured
  summaries, supports multilingual output, and can save to Markdown files.

Core capabilities:
  1. Fetch today's news from multiple sources
  2. Categorize and rank by topic/importance
  3. Generate structured report with summaries, sources, timestamps
  4. Optionally save to local file

Required tools:
  - web_search (news search)
  - fetch_url (article content retrieval)
  - write_file (optional, save report)
  - get_current_time (accurate date context)

Target File Structure

daily-news-digest/
โ”œโ”€โ”€ SKILL.md
โ”œโ”€โ”€ skill.json
โ”œโ”€โ”€ tools/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ news_fetcher.py
โ”‚   โ””โ”€โ”€ digest_formatter.py
โ”œโ”€โ”€ prompts/
โ”‚   โ””โ”€โ”€ system_fragment.md
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ test_news_fetcher.py
โ”‚   โ”œโ”€โ”€ test_formatter.py
โ”‚   โ””โ”€โ”€ fixtures/
โ”‚       โ””โ”€โ”€ sample_news_response.json
โ”œโ”€โ”€ requirements.txt
โ””โ”€โ”€ README.md

30.2 Project Initialization

Step 1: Create Skill Scaffold with Hermes CLI

# Install Hermes CLI if not already installed
pip install hermes-cli

# Create new Skill project
hermes skill new daily-news-digest

# Output:
# โœ“ Created directory: daily-news-digest/
# โœ“ Generated: SKILL.md (template)
# โœ“ Generated: skill.json (template)
# โœ“ Generated: tools/__init__.py
# โœ“ Generated: tests/test_basic.py
# โœ“ Generated: requirements.txt

cd daily-news-digest

Step 2: Set Up Python Environment

python -m venv .venv
source .venv/bin/activate  # macOS/Linux

pip install hermes-sdk anthropic pytest httpx python-dotenv
pip freeze > requirements.txt

Step 3: Configure Environment Variables

cat > .env << 'EOF'
ANTHROPIC_API_KEY=your_api_key_here
SEARCH_API_KEY=your_search_api_key_here
HERMES_MODEL=claude-3-5-sonnet-20241022
EOF

30.3 Writing SKILL.md

---
id: daily-news-digest
version: 1.0.0
name: Daily News Digest
description: >
  Fetches today's top news on specified topics, generates structured 
  summaries with sources, and optionally saves to a Markdown file.
author: your-username
license: MIT
tags:
  - news
  - research
  - summarization
  - daily-digest
hermes_version: ">=3.0.0"
tools_required:
  - web_search
  - fetch_url
tools_optional:
  - write_file
  - get_current_time
language: en
created: 2024-11-20
updated: 2024-11-20
---

# Daily News Digest

## Overview

This skill enables Hermes to create a comprehensive daily news digest on any topic.

**When to use this skill:**
- User says "give me today's news on X"
- User wants "a summary of what happened with X today/this week"
- User requests "daily briefing", "news digest", or "news roundup"
- User wants to stay updated without browsing

**Do NOT use this skill when:**
- User asks a specific factual question (use direct search)
- User wants analysis/opinion rather than news reporting
- User asks about events older than 2 weeks (use general research)
- User is in an offline environment

## Usage

### Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `topics` | array[string] | Yes | โ€” | News topics to research (1โ€“5) |
| `time_range` | string | No | "today" | "today" / "this_week" / "24h" |
| `max_articles` | integer | No | 5 | Articles per topic (1โ€“10) |
| `output_language` | string | No | auto | Auto-detect from user |
| `save_to_file` | boolean | No | false | Save digest to Markdown file |

### Execution Process

**Step 1: Topic Analysis**
Extract topics, formulate 2 search queries per topic.

**Step 2: News Search**
For each topic, call `web_search`:
- Query 1: "[topic] news today [current_year]"
- Query 2: "[topic] latest developments [current_month] [current_year]"

**Step 3: Article Selection**
Top 3โ€“5 most recent, authoritative results. Prioritize major outlets.

**Step 4: Content Extraction**
Call `fetch_url` for each selected article. Fall back to snippet if fetch fails.

**Step 5: Synthesis**
Generate structured digest with executive summary, key developments, quotes.

**Step 6: Output**
Format in user's language. If `save_to_file`, call `write_file`.

## Examples

### Example 1: Single Topic

**User input:**

Give me today's news about AI regulation in Europe.


**Expected output:**
```markdown
# Daily News Digest: AI Regulation in Europe
*Generated: November 20, 2024*

## Executive Summary
The European Parliament is accelerating EU AI Act implementation...

## Key Developments

### 1. EU AI Act: New Compliance Deadlines
**Source:** Reuters | Nov 20, 2024
The European Commission confirmed...

## Sources
1. [Reuters] EU AI Act Update - https://... - Nov 20, 2024

Dependencies

Limitations

Configuration

skills:
  - id: daily-news-digest
    version: "^1.0"
    config:
      default_max_articles: 5
      trusted_sources:
        - reuters.com
        - bbc.com
        - apnews.com

Changelog

1.0.0 (2024-11-20)


---

## 30.4 Implementing Tool Call Integration

### tools/news_fetcher.py

```python
"""News fetcher: wraps search and content extraction logic."""
import httpx
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlparse

@dataclass
class NewsArticle:
    title: str
    url: str
    snippet: str
    source: str
    published_date: Optional[str]
    full_content: Optional[str] = None

class NewsFetcher:
    def __init__(self, search_api_key: str, timeout: int = 10):
        self.api_key = search_api_key
        self.timeout = timeout
        self.client = httpx.Client(timeout=timeout)
    
    def search_news(self, query: str, time_range: str = "today") -> list[NewsArticle]:
        """Search for news articles. Corresponds to web_search tool call."""
        date_restrict = {"today": "pd", "24h": "pd", "this_week": "pw"}.get(time_range, "pd")
        
        response = self.client.get(
            "https://api.search.brave.com/res/v1/news/search",
            params={"q": query, "count": 10, "freshness": date_restrict},
            headers={"Accept": "application/json", "X-Subscription-Token": self.api_key}
        )
        response.raise_for_status()
        data = response.json()
        
        return [
            NewsArticle(
                title=r.get("title", ""),
                url=r.get("url", ""),
                snippet=r.get("description", ""),
                source=urlparse(r.get("url", "")).netloc.replace("www.", ""),
                published_date=r.get("age", "Unknown")
            )
            for r in data.get("results", [])
        ]
    
    def fetch_article_content(self, url: str) -> Optional[str]:
        """Fetch full article text. Corresponds to fetch_url tool call."""
        try:
            response = self.client.get(url, headers={"User-Agent": "HermesNewsDigest/1.0"})
            response.raise_for_status()
            # Simple text extraction โ€” use trafilatura in production
            from html.parser import HTMLParser
            class Extractor(HTMLParser):
                def __init__(self):
                    super().__init__()
                    self.parts = []
                def handle_data(self, data):
                    if data.strip():
                        self.parts.append(data.strip())
            ex = Extractor()
            ex.feed(response.text)
            return " ".join(ex.parts)[:2000]  # truncate to avoid token overflow
        except Exception:
            return None  # degrade gracefully, don't crash

tools/digest_formatter.py

"""Format news articles into structured digest output."""
from datetime import datetime
from typing import Optional
from .news_fetcher import NewsArticle

class DigestFormatter:
    def format_digest(
        self,
        topic: str,
        articles: list[NewsArticle],
        max_articles: int = 5
    ) -> str:
        selected = articles[:max_articles]
        today = datetime.now().strftime("%B %d, %Y")
        
        lines = [
            f"# Daily News Digest: {topic}",
            f"*Generated: {today}*", "",
            "## Key Developments", ""
        ]
        
        for i, a in enumerate(selected, 1):
            lines += [
                f"### {i}. {a.title}",
                f"**Source:** {a.source} | {a.published_date}", "",
                a.full_content or a.snippet, ""
            ]
        
        lines += ["## Sources"]
        for i, a in enumerate(selected, 1):
            lines.append(f"{i}. [{a.source}] {a.title} โ€” {a.url}")
        
        return "\n".join(lines)

30.5 Local Testing

tests/test_news_fetcher.py

import pytest
from unittest.mock import patch, MagicMock
import httpx
from tools.news_fetcher import NewsFetcher, NewsArticle

@pytest.fixture
def fetcher():
    return NewsFetcher(search_api_key="test-key")

@pytest.fixture
def sample_response():
    return {"results": [
        {"title": "AI Act Update", "url": "https://reuters.com/ai", 
         "description": "The EU...", "age": "2 hours ago"},
        {"title": "Industry Response", "url": "https://bbc.com/tech",
         "description": "Companies...", "age": "5 hours ago"},
    ]}

class TestSearchNews:
    def test_returns_articles(self, fetcher, sample_response):
        with patch.object(fetcher.client, 'get') as mock_get:
            mock_get.return_value = MagicMock(
                json=lambda: sample_response,
                raise_for_status=MagicMock()
            )
            articles = fetcher.search_news("AI regulation")
            assert len(articles) == 2
            assert isinstance(articles[0], NewsArticle)
    
    def test_empty_results(self, fetcher):
        with patch.object(fetcher.client, 'get') as mock_get:
            mock_get.return_value = MagicMock(
                json=lambda: {"results": []},
                raise_for_status=MagicMock()
            )
            assert fetcher.search_news("xyz_nonexistent") == []
    
    def test_timeout_returns_none(self, fetcher):
        with patch.object(fetcher.client, 'get', side_effect=httpx.TimeoutException("timeout")):
            content = fetcher.fetch_article_content("https://slow.example.com")
            assert content is None

    @pytest.mark.parametrize("time_range,expected", [
        ("today", "pd"), ("24h", "pd"), ("this_week", "pw"), ("unknown", "pd"),
    ])
    def test_date_filter_mapping(self, fetcher, time_range, expected):
        result = fetcher._build_date_filter(time_range)
        assert result == expected

Running Tests

# Run all tests
pytest tests/ -v

# With coverage report
pytest tests/ --cov=tools --cov-report=html --cov-fail-under=80

# Validate Skill format
hermes skill validate
# Output:
# โœ“ SKILL.md format valid
# โœ“ skill.json valid
# โœ“ Required tools declared: web_search, fetch_url
# โœ“ Examples present (2 found)
# โœ“ All checks passed!

30.6 Debugging Techniques

Technique 1: Hermes Debug Mode

import hermes, logging
logging.basicConfig(level=logging.DEBUG)

agent = hermes.Agent(
    skills=["./daily-news-digest"],
    debug=True,
    trace_tokens=True
)
# Output:
# [DEBUG] Loading skill: daily-news-digest v1.0.0
# [DEBUG] Skill injected: 342 tokens added to system prompt
# [DEBUG] Tool called: web_search {"query": "AI regulation today 2024"}
# [DEBUG] Token usage: input=8412, output=623, cache_hit=False

Technique 2: Mock Tool Calls

from hermes.testing import SkillTestHarness

harness = SkillTestHarness(skill_path="./daily-news-digest")
harness.mock_tool("web_search", returns=[
    {"title": "AI News Today", "url": "https://example.com", "snippet": "..."},
])
harness.mock_tool("fetch_url", returns="Full article content here...")

result = harness.run("Give me today's news about AI")

assert "AI" in result
assert "Sources" in result
assert harness.tool_call_count("web_search") >= 1

Technique 3: Step-Through Debugging

harness.run_with_steps(
    "Morning briefing on crypto and AI",
    on_tool_call=lambda tool, args: print(f"โ†’ Calling {tool}: {args}"),
    on_tool_result=lambda tool, result: print(f"โ† {tool}: {str(result)[:100]}"),
)

30.7 Publishing to ClawHub

Pre-Publication Checklist

hermes skill validate    # All checks must pass โœ“
pytest tests/ -v        # All tests must pass โœ“
ls LICENSE README.md    # Both must exist โœ“
hermes skill build      # Creates: daily-news-digest-1.0.0.skill

Publishing

# Method 1: CLI publish
hermes skill publish
# ClawHub username: your-username
# ClawHub API token: [enter token]
# โœ“ Published! View at: https://clawhub.io/skills/your-username/daily-news-digest

CI/CD Auto-Publish (GitHub Actions)

name: Publish to ClawHub
on:
  push:
    tags: ['v*']
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with: {python-version: '3.11'}
      - name: Install CLI
        run: pip install hermes-cli
      - name: Validate
        run: hermes skill validate
      - name: Test
        run: pytest tests/ -v
      - name: Publish
        env:
          CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
        run: hermes skill publish --token $CLAWHUB_TOKEN

30.8 Summary

Through this complete hands-on project, you've mastered the full Hermes Skill development workflow:


Discussion Questions

  1. In SKILL.md's "Do NOT use" section, we say not to use this Skill for news older than 2 weeks. If a user asks about historical news, should the Skill error out or gracefully degrade to a general search? How would you express this in SKILL.md?

  2. The DigestFormatter._generate_executive_summary method notes "actually done by the LLM in real execution." Redesign this boundary โ€” which steps in a Skill should use code, and which should be done by the LLM?

  3. If the news digest Skill is used by 1,000 simultaneous users in production, search API rate limits become a bottleneck. How would you design a caching layer? What's the appropriate cache duration?

  4. Design a Skill "security audit" checklist โ€” what security issues must be checked and cleared before publishing to ClawHub?

Rate this chapter
4.7  / 5  (3 ratings)

๐Ÿ’ฌ Comments