Skip to content

Repository files navigation

AI - Digests

Daily newsletters on AI/ML/DevOps topics from k8s to Agentic Workflows, fully generated by a self-hosted agentic system running on Ollama.

Available to read on my website: https://sean-michael.dev/digest

Inspired by a comment Addy Osmani made in his recent chat with Tim O'Reilly on agentic AI systems, where he proposed one could create an AI Agent to stay on top of all the latest innovations in the space. I thought this was very apt because I wanted to experiment with agentic systems more AND I struggle with FOMO whenever new technologies come around.

How It Works

Three agents collaborate in a loop that mirrors how a small editorial team would work. Each agent run goes through chat_with_ollama() in agents.py which wraps the Ollama Python SDK chat() call and instruments it with OpenTelemetry spans.

Agents

Researcher (agents.py:researcher)

  • Input: Raw RSS entries from all 26 feeds (dict of source → entries)
  • Tools: fetch_article() via ThreadPoolExecutor — for thin RSS entries (<200 chars), fetches the full page and parses with BeautifulSoup
  • Prompts: RESEARCHER_SYSTEM_PROMPT + RESEARCHER_USER_PROMPT — tells it to pick the top 10 articles matching my interests and return just a JSON array of links
  • Then: For each curated article, runs summarize_article() with SUMMARY_SYSTEM_PROMPT + SUMMARY_USER_PROMPT
  • Output: list[dict] — each with source, title, summary, link

Writer (agents.py:writer)

  • Input: Curated articles from researcher, plus any previous draft and editor feedback
  • Prompts: WRITER_SYSTEM_PROMPT + WRITER_USER_PROMPT — template vars: $date_str, $articles, $feedback, $draft
  • Output: Markdown newsletter with "Story of the Day" deep dive + "Quick Hits" section

Editor (agents.py:editor)

  • Input: The writer's draft
  • Prompts: EDITOR_SYSTEM_PROMPT + EDITOR_USER_PROMPT — template vars: $date_str, $draft
  • Output: Either "LGTM" or specific actionable feedback (never both)

The writer and editor loop up to MAX_REVISIONS times. The final edition gets written to digests/ as markdown with YAML frontmatter, and optionally published to S3 for my website.

Prompt Architecture

All prompts live in prompts.py as Pydantic BaseModel instances:

class Prompt(BaseModel):
    agent: str        # which agent owns this prompt
    prompt_type: str  # "system" or "user"
    template: str     # python string.Template syntax ($var)
    version: str      # semver, e.g. "v1.0.0"

    def render(self, **kwargs) -> str:
        return Template(self.template).substitute(**kwargs)

This gives each prompt a structured identity (agent, type, version) that gets carried into tracing. When an agent runs, the prompt version and template are set as span attributes so I can correlate model behavior with specific prompt versions in Phoenix.

Tracing with OpenInference

Every agent call is wrapped with openinference.instrumentation.using_prompt_template() which attaches the prompt template and version to the current OpenTelemetry span. Combined with the llm.chat spans from chat_with_ollama(), each run captures:

  • Prompt template text + version
  • System and user prompt content
  • Token counts (prompt, completion, total)
  • Latency breakdown (total, prompt eval, generation, model load) in ms
  • Token throughput (tokens/sec)
  • Session ID via using_session() so all spans from one run are grouped

All of this lands in Arize Phoenix at localhost:6006.

Stack

Runtime Python 3.13, uv
LLM Ollama (currently gemma4:e4b)
Feeds feedparser, BeautifulSoup for content enrichment
Observability OpenTelemetry → Arize Phoenix (token counts, latencies, prompts)
Publishing boto3 (S3), markdown with YAML frontmatter
Prompts Versioned templates with Pydantic models (prompts.py)

Configuration

All in config.py — models, context window, revision limits, interests list, etc.

Variable Default
RESEARCHER_MODEL / WRITER_MODEL / EDITOR_MODEL gemma4:e4b Ollama model for each agent
NUM_CTX 65536 Context window size
MAX_REVISIONS 3 Editorial loop cap
TIMEFRAME_HOURS 24 How far back to look for articles

Feeds

26 feeds across official blogs (Docker, HuggingFace, AWS AI, Kubernetes, CNCF), Hacker News filters, Substacks (Addy Osmani, Pragmatic Engineer, Byte Byte Go, etc.), and independent blogs (Simon Willison, Eugene Yan, SemiAnalysis). Full list in feeds.json.

Local Development

Prerequisites: Ollama running locally with your model pulled (e.g. ollama pull gpt-oss:20b)

Start Phoenix for trace collection and the app:

docker compose up

Or run them separately:

# Phoenix
docker run -p 6006:6006 -p 4317:4317 -it arizephoenix/phoenix:latest

# Agents
uv run main.py

Phoenix UI is at http://localhost:6006 — you can see every LLM call, token counts, and latencies there.

Docker + Ollama

Ollama runs on port 11434. The Dockerfile and docker-compose already handle this — OLLAMA_HOST is set to http://host.docker.internal:11434 so the container talks to Ollama on your host machine. Just make sure Ollama is running before you docker compose up.

Output

  • digests/ — Final published newsletters (markdown + YAML frontmatter)
  • drafts/{date}/ — Every draft and edit from the editorial loop, saved per-revision
  • logs/{date}/ — Structured JSON logs

See notes/ for the full learning journal and TODO list.

About

AI news digest curation and creation application.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages