DEV Community

shashank ms
shashank ms

Posted on

Building Utilities Tools with LLMs: A Step-by-Step Guide

We are going to build a small CLI utility called notes2tasks that reads raw meeting notes and emits a structured Markdown task list with owners and due dates. It is useful for anyone who ends a meeting with a wall of text and needs an actionable checklist without manual copy-editing.

What you'll need

Oxlo.ai works as a drop-in replacement here. Because its pricing is request-based rather than token-based, you pay the same flat cost whether the notes are two sentences or two pages. You can see the details at https://oxlo.ai/pricing.

Step 1: Read the raw notes

I want the tool to accept a plain text file. If no file is provided, it should read from stdin so it can be piped.

import argparse
import sys

def load_notes(path=None):
    if path is None:
        return sys.stdin.read()
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Turn meeting notes into tasks.")
    parser.add_argument("file", nargs="?", help="Path to raw notes text file")
    args = parser.parse_args()

    raw_notes = load_notes(args.file)
    print(f"Loaded {len(raw_notes)} characters.")

Step 2: Define the system prompt

The prompt is the entire product. It tells the model to extract tasks, owners, and due dates, and to return strict JSON.

SYSTEM_PROMPT = """You are a structured-data extractor.
Read the user's raw meeting notes and extract every actionable task.
Return a single JSON object with this exact schema:

{
  "tasks": [
    {
      "task": "string",
      "owner": "string or null",
      "due": "string or null (ISO 8601 date if mentioned, otherwise null)",
      "priority": "high | medium | low"
    }
  ]
}

Rules:
- Do not include tasks that are purely informational with no action.
- If an owner is not mentioned, use null.
- If a due date is not mentioned, use null.
- Return only the JSON object, with no markdown fences."""

Step 3: Call Oxlo.ai

We initialize the OpenAI client pointing at Oxlo.ai and send the notes. I use llama-3.3-70b here because it handles instruction following cleanly, but qwen-3-32b or kimi-k2.6 work just as well depending on your context length needs. Oxlo.ai has no cold starts on these popular models, so the CLI feels snappy even when you run it ad-hoc.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

def extract_tasks(notes_text):
    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": notes_text},
        ],
    )

    content = response.choices[0].message.content
    # Some models occasionally wrap JSON in markdown fences; strip them.
    content = content.removeprefix("

```json").removeprefix("```

").removesuffix("

```

").strip()
    return json.loads(content)

Step 4: Parse and validate

LLMs can return slightly malformed JSON when notes are ambiguous, so we wrap the parser and fall back to an empty task list rather than crashing the CLI.

def safe_extract(notes_text):
    try:
        data = extract_tasks(notes_text)
        if not isinstance(data.get("tasks"), list):
            return []
        return data["tasks"]
    except Exception as e:
        print(f"Warning: failed to parse model output: {e}", file=sys.stderr)
        return []

Step 5: Format the report

Finally, we render the tasks as a Markdown checklist. This prints to stdout, so it can be redirected to a file or pasted into a ticket tracker.

def render_markdown(tasks):
    if not tasks:
        print("No actionable tasks found.")
        return

    print("# Extracted Tasks\n")
    for t in tasks:
        owner = f"**@{t.get('owner')}**" if t.get("owner") else "*unassigned*"
        due = f" | Due: `{t.get('due')}`" if t.get("due") else ""
        priority = t.get("priority", "medium")
        print(f"- [ ] {t['task']} | Owner: {owner} | Priority: {priority}{due}")

def main():
    parser = argparse.ArgumentParser(description="Turn meeting notes into tasks.")
    parser.add_argument("file", nargs="?", help="Path to raw notes text file")
    args = parser.parse_args()

    raw_notes = load_notes(args.file)
    tasks = safe_extract(raw_notes)
    render_markdown(tasks)

if __name__ == "__main__":
    main()

Run it

Save everything as notes2tasks.py, replace YOUR_OXLO_API_KEY with your key, and pass a text file.

python notes2tasks.py meeting_notes.txt

Example input in meeting_notes.txt:

Q3 Planning Sync
Sarah said she will update the Terraform modules by next Friday.
We need to audit AWS egress costs, someone from finance should do that by 2024-08-30.
The frontend migration is blocked on the design system tokens. Mike owns that but no hard date yet.

Example output:

# Extracted Tasks

- [ ] Update the Terraform modules | Owner: **@Sarah** | Priority: medium | Due: `2024-08-16`
- [ ] Audit AWS egress costs | Owner: *unassigned* | Priority: medium | Due: `2024-08-30`
- [ ] Unblock frontend migration on design system tokens | Owner: **@Mike** | Priority: medium

Next steps

Pipe the Markdown output directly to a file and commit it to your repo as a living TODO, or extend the script to POST the JSON payload to your issue tracker using the requests library.

If you start processing long transcripts or running this on a cron job, the flat per-request pricing on Oxlo.ai keeps the cost predictable no matter how verbose your meetings get. That makes it a solid backend for utility scripts that run unattended.

Top comments (0)