TheFocus.AI TheFocus.AI
PROJECT

Data Extractor

HANDS-ON

TUTOR WITH THEFOCUS.AI

Agent Integration

Copy this prompt into Claude, ChatGPT, or any external AI assistant. It points the assistant to the course instructions and links it to your student profile to track your progress and customize observations.

Please tutor me in this lesson using the following context. First, read the instructions at: https://courses.thefocus.ai/llms.txt My Student ID is: <none> The lesson markdown source is at: https://courses.thefocus.ai/run-ai-locally/projects/data-extractor.md

You are not enrolled yet. Enroll to generate a Student ID to track lesson completions and store learning notes.

Project: Data Extractor

Source: 2-extract/
Model: gemma4:26b
Type: TypeScript (Bun runtime)

What It Does

The data extractor parses Ollama’s output format. Given a directory containing ollama-output.md, it produces:

  • reasoning.md — the model’s internal thinking
  • response.md — the model’s actual answer
  • code — the first code block extracted, made executable

This is the tool that turns raw model output into runnable, inspectable artifacts.

The Prompt

The original prompt was a one-shot specifying the Ollama output format:

write a standalone typescript script that has a shebang header using bun

it accepts a directory and an optional filename

by default it loads directory/ollama-output.md

and it extracts it to directory/reasoning.md, directory/response.md and directory/code

below is a sample file

The section seperators are

Thinking...
...done thinking.

and when you get the code out dont include the ```

make the code exectuable once you have it

output 2 sections

# New Prompt

An updated version of this prompt with your assumptions and choices filled out

# Source Code

The source code

no other commentary

Prompt Evolution

This prompt went through multiple refinements. The author iterated:

  1. Initial prompt → worked but output was garbled
  2. Updated to store reasoning, response, AND code properly
  3. Further refined to ensure code blocks only use backticks from the start of a line
  4. Final prompt2.md version consistently produces clean output

The iteration process itself used the self-improving loop:

# Test the extractor
./2-extract/test 2>&1 | tee ./2-extract/test-output

# Feed results back for improvement
./1-combind/code -m "update the prompt to make sure all of the
reasoning gets stored, all of the response gets stored, the code
stuff was great" \
  2-extract/prompt.md 2-extract/code 2-extract/test-output \
  0-weather/code 0-weather/reasoning.md 0-weather/response.md \
  | ollama run gemma4:26b --nowordwrap

The Code

#!/usr/bin/env bun
import { writeFileSync, chmodSync } from "node:fs";
import path from "node:path";

async function main() {
  const args = process.argv.slice(2);
  if (args.length < 1) {
    console.error("Usage: ./extract.ts <directory> [filename]");
    process.exit(1);
  }

  const dir = args[0];
  const filename = args[1] || "ollama-output.md";
  const inputPath = path.join(dir, filename);

  try {
    const content = await Bun.file(inputPath).text();
    const lines = content.split(/\r?\n/);

    const reasoning: string[] = [];
    const response: string[] = [];
    const code: string[] = [];

    let state: "searching" | "reasoning" | "response" = "searching";
    let isInsideCodeBlock = false;
    let codeExtracted = false;

    for (const line of lines) {
      // Separator detection (must be only content on line)
      if (state === "searching" && line === "Thinking...") {
        state = "reasoning";
        continue;
      }
      if (state === "reasoning" && line === "...done thinking.") {
        state = "response";
        continue;
      }

      if (state === "reasoning") {
        reasoning.push(line);
      } else if (state === "response") {
        response.push(line);

        // Code block extraction logic
        if (!codeExtracted && line.startsWith("```")) {
          if (!isInsideCodeBlock) {
            isInsideCodeBlock = true;
          } else {
            isInsideCodeBlock = false;
            codeExtracted = true;
          }
          continue; // Skip the fence line
        }

        if (isInsideCodeBlock) {
          code.push(line);
        }
      }
    }

    // Write outputs
    writeFileSync(path.join(dir, "reasoning.md"), reasoning.join("\n"));
    writeFileSync(path.join(dir, "response.md"), response.join("\n"));

    const codePath = path.join(dir, "code");
    writeFileSync(codePath, code.join("\n"));
    chmodSync(codePath, 0o755);

    console.log("Extraction complete.");
  } catch (error) {
    console.error(`Error: ${error}`);
    process.exit(1);
  }
}

main();

The Test

#!/bin/bash
set -e
./2-extract/code 0-weather

This tests the extractor against the 0-weather exercise output. If 0-weather/ollama-output.md exists and is valid, the test should produce 0-weather/reasoning.md, 0-weather/response.md, and 0-weather/code.

Key Lessons

  • State machines are a natural fit for parsing structured text. Three states: searching, reasoning, response.
  • The thinking/response split is specific to Gemini-family models — other model families may have different output formats.
  • Prompt refinement works. The initial extractor worked but was fragile. Feeding specific failure cases back to the model produced a robust version.
  • This is a building block. The extractor + combiner + prompt runner together form the complete agentic loop.

Next: Prompt Improver →