Skip to content

feat(ollama): add ollama-eval.mjs interactive evaluator#681

Open
mattkirkey wants to merge 3 commits into
santifer:mainfrom
mattkirkey:feat/ollama-eval
Open

feat(ollama): add ollama-eval.mjs interactive evaluator#681
mattkirkey wants to merge 3 commits into
santifer:mainfrom
mattkirkey:feat/ollama-eval

Conversation

@mattkirkey
Copy link
Copy Markdown

@mattkirkey mattkirkey commented May 17, 2026

Summary

Adds ollama-eval.mjs (~365 lines) — interactive single-offer evaluator that mirrors the gemini-eval.mjs pattern, using a local Ollama model instead of a cloud API.

  • Probes Ollama reachability at startup (GET /api/tags, 5 s timeout) so the user gets a clear error before prompt assembly begins.
  • Loopback guard: rejects non-localhost OLLAMA_BASE_URL by default; opt-out via OLLAMA_ALLOW_REMOTE=1 (strict equality, not truthy).
  • Reads modes/_shared.md + modes/oferta.md + cv.md as context.
  • Calls /v1/chat/completions (Ollama OpenAI-compat) with temperature: 0.4, num_ctx: 32768.
  • Saves evaluation to reports/{###}-{company}-{date}.md; prints tracker entry reminder to stdout.
  • JSDoc on readFile and nextReportNumber helpers.
  • Adds ollama:eval npm script.

Usage

# Inline JD
node ollama-eval.mjs "We are looking for a Senior AI Engineer..."

# From file
node ollama-eval.mjs --file ./jds/openai-swe.txt

# Specific model
node ollama-eval.mjs --model qwen2.5:32b --file ./jds/job.txt

# Or via npm
npm run ollama:eval

Prerequisites: Ollama running locally with a model pulled:

ollama pull llama3.3   # or qwen2.5:32b, mistral-nemo, gemma3:27b
ollama serve

OLLAMA_BASE_URL and OLLAMA_MODEL env vars are documented in .env.example (added in companion PR #680).

Test plan

  • node ollama-eval.mjs --help prints usage
  • With Ollama stopped: startup probe exits with clear error message
  • OLLAMA_BASE_URL=http://remote:11434 node ollama-eval.mjs "test" triggers loopback guard
  • Inline JD produces a full A-G evaluation and saves report to reports/
  • --no-save flag skips report write

Companion PR

PR #680 adds ollama-batch.mjs + batch-runner.sh --backend ollama (batch mode).
This PR covers the interactive/single-offer use case. Both PRs are independent and can merge in any order.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Ollama-based evaluation command to score job descriptions with selectable model/endpoint, inline or file input, and reachability/security checks
    • Produces detailed markdown reports with a machine-readable scored summary, role archetype classification, and a compact one-line score/archetype/legitimacy output
    • Option to skip saving evaluations
  • Chores

    • Updated npm scripts to include the Ollama evaluation command

Review Change Stack

Mirrors the gemini-eval.mjs pattern for local/free/private use.

- Probes Ollama reachability at startup (GET /api/tags, 5 s timeout)
  so the user gets a clear error before prompt assembly begins.
- Loopback guard: rejects non-localhost OLLAMA_BASE_URL by default;
  opt-out via OLLAMA_ALLOW_REMOTE=1 (strict equality, not truthy).
- Reads modes/_shared.md + modes/oferta.md + cv.md as context.
- Calls /v1/chat/completions (Ollama OpenAI-compat) with temperature 0.4,
  num_ctx 32768 to handle typical 10K-15K token prompts.
- Saves evaluation to reports/{###}-{company}-{date}.md; prints tracker
  entry reminder to stdout.
- JSDoc on readFile and nextReportNumber helpers.
- Add `ollama:eval` npm script.

Requires OLLAMA_BASE_URL / OLLAMA_MODEL (documented in .env.example,
added in the companion batch PR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Welcome to career-ops, @mattkirkey! Thanks for your first PR.

A few things to know:

  • Tests will run automatically — check the status below
  • Make sure you've linked a related issue (required for features)
  • Read CONTRIBUTING.md if you haven't

We'll review your PR soon. Join our Discord if you have questions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b5134a7-3e78-4014-acdf-e6dd258ccd70

📥 Commits

Reviewing files that changed from the base of the PR and between fe9ffe8 and d31106f.

📒 Files selected for processing (1)
  • ollama-eval.mjs

📝 Walkthrough

Walkthrough

Adds a new CLI tool (ollama-eval.mjs) that embeds local context, validates and probes an Ollama endpoint, sends a chat completion request to evaluate a job description, parses a machine-readable score block, optionally saves a numbered markdown report, and adds an npm script to run it.

Changes

Ollama Job Evaluator CLI

Layer / File(s) Summary
Setup and CLI argument parsing
ollama-eval.mjs (1–120)
Script entrypoint, optional dotenv, compute ROOT, define fixed context/report paths, implement help and parse flags (--file,--model,--url,--no-save) and inline JD text; validate input.
File utilities and report numbering
ollama-eval.mjs (121–152)
readFile() loads context files with missing-file warnings and placeholders; nextReportNumber() scans reports/ for NNN- files and returns the next zero-padded id.
Loopback guard and reachability probe
ollama-eval.mjs (154–197)
Blocks non-local Ollama endpoints unless OLLAMA_ALLOW_REMOTE=1; performs a short-timeout GET /api/tags probe against the base URL before proceeding.
System prompt construction and Ollama API call
ollama-eval.mjs (199–306)
Loads modes/_shared.md, modes/oferta.md, and cv.md, composes a system prompt requiring a ---SCORE_SUMMARY--- block, calls POST /v1/chat/completions with configurable timeout, handles errors/timeouts, and prints the model output.
Score parsing and report persistence
ollama-eval.mjs (310–371)
Parses the ---SCORE_SUMMARY--- block into COMPANY/ROLE/SCORE/ARCHETYPE/LEGITIMACY (with defaults), creates reports/ if needed, writes a numbered slugged markdown report (removing the machine-readable block), prints a tracker-table row, and prints a compact final summary.
Script command registration
package.json (18–19)
Adds ollama:eval npm script alongside gemini:eval to run the new CLI.

Sequence Diagram

sequenceDiagram
  participant User as User / CLI
  participant Script as ollama-eval.mjs
  participant Ollama as Ollama Server
  participant FS as File System

  User->>Script: npm run ollama:eval [--file FILE] [--model MODEL] [--url URL] [--no-save]
  Script->>FS: Read cv.md, modes/_shared.md, modes/oferta.md
  Script->>Script: Build system prompt (include SCORE_SUMMARY instructions)
  Script->>Ollama: GET /api/tags (reachability probe)
  Ollama-->>Script: 200 OK / tags
  Script->>Ollama: POST /v1/chat/completions (system + job description)
  Ollama-->>Script: Full evaluation text including ---SCORE_SUMMARY---
  Script->>Script: Parse SCORE_SUMMARY via regex -> COMPANY/ROLE/SCORE/ARCHETYPE/LEGITIMACY
  Script->>FS: Create reports/ and write NNN-slug-date.md (strip machine block)
  Script->>User: Print full evaluation and compact score/archetype/legitimacy line
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • santifer/career-ops#349: Adds a Gemini-based evaluator that uses the same prompt embedding (modes/_shared.md + modes/oferta.md + cv.md) and structured ---SCORE_SUMMARY--- parsing, parallel to this Ollama backend change.

Suggested labels

📄 docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding ollama-eval.mjs, an interactive evaluator using Ollama, which is the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ollama-eval.mjs`:
- Around line 92-99: The current --file handling uses existsSync and then calls
readFileSync(filePath, 'utf-8') which can still throw on permission errors or if
filePath is a directory; wrap the read in a try/catch around readFileSync (and
optionally use statSync or fs.promises.stat to verify it's a regular file) and
on any error log a clear message including filePath and error.message, then call
process.exit(1) instead of letting the exception bubble; update the block that
assigns jdText to perform this guarded read and handle errors gracefully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 510cbe1e-bd4e-4df0-b490-c0a64363cba9

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f3a3 and 6c8c077.

📒 Files selected for processing (2)
  • ollama-eval.mjs
  • package.json

Comment thread ollama-eval.mjs
ollama-eval.mjs:
- Wrap readFileSync in try/catch when reading --file input. The
  existsSync guard handles missing files, but a directory path, broken
  symlink, or permission error would throw an uncaught exception and
  print a raw stack trace. Now exits cleanly with a user-facing message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ollama-eval.mjs`:
- Around line 144-152: The nextReportNumber function will drop files once
numbers exceed 999; update it to accept any number of leading digits and to pad
to a larger width: change the filename filter from /^\d{3}-/ to /^\d+-/ (or use
a regex match to extract the leading digits) and replace the fixed slice(0, 3)
extraction with extracting the matched digit group before parseInt, then return
the next number padded with padStart(4, '0') (still using PATHS.reports and the
nextReportNumber function names to locate the change).
- Line 254: The parsed timeout value assigned to timeoutMs using parseInt can be
NaN for non-numeric OLLAMA_TIMEOUT_MS, which later leads to
AbortSignal.timeout(NaN) throwing; change the code around timeoutMs to validate
the parsed value (use Number.parseInt or Number and check
isFinite/Number.isInteger or !Number.isNaN), and if invalid either (a) fall back
to the default 300000 or (b) throw a clear configuration error that mentions
OLLAMA_TIMEOUT_MS must be a positive integer; update the code that calls
AbortSignal.timeout to use the validated numeric timeoutMs so
AbortSignal.timeout never receives NaN.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a287cdd8-5fd7-473d-801a-14998f90e606

📥 Commits

Reviewing files that changed from the base of the PR and between 6c8c077 and fe9ffe8.

📒 Files selected for processing (1)
  • ollama-eval.mjs

Comment thread ollama-eval.mjs
Comment thread ollama-eval.mjs
ollama-eval.mjs:
- Fix report numbering above 999: replace fixed /^\d{3}-/ filter and
  slice(0,3) with a regex capture group (/^(\d+)-/) that handles any
  number of digits. padStart(3,'0') still zero-pads short numbers while
  allowing 4+ digit names to pass through correctly.
- Validate OLLAMA_TIMEOUT_MS at startup: parseInt can return NaN on
  non-numeric values (e.g. "30s"), which causes AbortSignal.timeout(NaN)
  to throw a TypeError. Now exits with a clear error message instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant