跳到正文
DEV Community· Matheus Ferreira·· 2 小時前AI 評分48

SlothScout:凌晨 3 時代讀求職提醒的本機智能體

SlothScout: a local agent that reads my job alerts at 3 a.m. so I don't have to

AI 導讀

SlothScout 是一個以 Ollama 本機執行 Gemma 3 4B 的智能體,會在凌晨 3 時讀取求職提醒電郵,按履歷和條件為職位評分。首次運行讀取 15 封提醒、評估 49 個職位,只有 1 個高於 70 分門檻,耗時 152 秒。作者表示,SlothScout 每日可節省約 3 小時,履歷和收件箱內容不會傳送至任何 API。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 3 — Slow and Steady

What I Built

I'm a junior dev in São José dos Campos, Brazil, looking for my next job. Like everyone, I signed up for job alerts on LinkedIn, Indeed and Gupy. And like everyone, my inbox turned into a wall of "47 new jobs for you" emails.

I was spending about 4 hours a day on them: reading the alerts, opening each site, reading the requirements, and finding out (again) that "Junior" meant 5 years of experience.

So I built SlothScout 🦥. Every night at 3 a.m., while I'm asleep, it:

  1. reads only my Vagas Gmail label over IMAP (read-only),
  2. pulls every job posting out of the alert emails,
  3. dedupes them (the same job shows up in three alerts),
  4. scores each one against my résumé and my criteria with Gemma 3 4B running locally on Ollama,
  5. and leaves me a morning page with two lines: apply to these, ignore the rest.

My first real run:

Alert emails read 15
Job postings scored 49
Above my cutoff (70) 1
Total time 152 seconds

One job worth my attention: a remote Backend Developer Jr role. The other 48 got a score and a reason, and I never had to open them.

The lazy payoff: it saves me about 3 hours a day. I still spend around an hour on the shortlist and the borderline ones, actually reading them and applying. The other three hours of clicking through postings that were never a fit are gone. That's very sloth-approved.

Demo

This is a real excerpt of the report SlothScout wrote this morning (reports/2026-10-24.md, titles trimmed):

# SlothScout · 2026-10-24

49 vagas avaliadas · corte 70 · modelo `gemma3:4b`

## Aplica nessas

### 75 · Desenvolvedor Backend Jr · ABC Fitness · Brasil (Remoto)
- ✓ Desenvolvedor Backend
- ✓ Remoto
- ✓ Crescimento rápido

## Ignora o resto (48)

- 65 · Developer Fullstack Jr (Java + Angular) · CI&T
- 65 · Desenvolvedor Java Jr · Firedev IT
- 25 · Desenvolvedor(a) Fullstack Sênior – Integrações de Sistemas · Lyncas
- 10 · Tech Lead – AI Engineering · VENHA SER #SANGUELARANJA
- 5  · Cplug I Engenheiro (a) de Dados Sênior I Remoto · LWSA
- ...

("Aplica nessas" / "Ignora o resto" = "apply to these" / "ignore the rest".) The same content is served as a Thymeleaf page at localhost:8080, with a Rodar agora ("run now") button so I don't have to wait for 3 a.m. Every ✓ means the quoted reason really appears in the posting (more on that below).

Code

GitHub logo MathFe / SlothScout

Local agent that triages job-alert emails overnight with Gemma via Ollama

🦥 SlothScout

A local agent that reads your job-alert emails at 3 a.m., scores every posting against your résumé with a local open-weight model, and greets you in the morning with two lines: apply to these, ignore the rest.

Nothing leaves your machine except the IMAP fetch. Your résumé and inbox never touch anyone's API.

How it works

  1. Schedule: a Spring @Scheduled job runs at 03:00 America/Sao_Paulo.
  2. Collect: reads only the alerts folder over IMAP, read-only. Processed emails are tracked by Message-ID in a local H2 DB, so your Gmail "read" state is never touched.
  3. Extract: one Jsoup parser per sender (LinkedIn, Indeed, Gupy). Each one anchors on the job link and its ID, which stay stable while the email layouts keep changing.
  4. Dedupe: postings are keyed by source:jobId, so the same job from three alerts is scored once.
  5. Score: one call per…

How I Built It

Java 25 + Spring Boot, Spring AI with the Ollama starter, Jsoup, H2, Thymeleaf.

One parser per sender, anchored on the job link

Alert email layouts change all the time, but the job link and its ID don't. Every parser (LinkedIn, Indeed, Gupy) finds the job links first and then reads title/company/location around them. Postings are keyed by source:jobId, so a job that arrives in three alerts is scored once.

Criteria live in config, not in the prompt

slothscout:
  criteria:
    levels: [Júnior, Estágio]
    on-site-cities: [São José dos Campos, Taubaté, Caçapava, Jacareí]
    remote: remoto no Brasil ou internacional
    stacks: [Java, Python, JavaScript, .NET]
    max-years-experience: 2
    exclusions:
      - senioridade alta (pleno, sênior, especialista, lead)
      - viagem obrigatória para São Paulo
    cutoff: 70

I tune what I'm looking for without touching code or prompts.

Structured output, with the "reading" fields before the score

Spring AI generates a JSON Schema from a Java record, Ollama enforces it, and I get a typed object back. The field order is deliberate: the model has to write down what level and location it read in the posting before it's allowed to give a score. With a 4B model, that noticeably cut down on guessing.

public record JobScore(
    String levelInText,     // what the posting actually says
    String locationInText,
    boolean techRole,
    int score,              // only after the above
    List<String> reasons,   // each must quote the posting
    List<String> flags) {}

Reasons must quote the posting, and I check them

Alerts only carry a snippet of each job, so small models love to fill the gaps. Every reason has to quote a word or phrase from the posting, and a tiny Grounding class checks (accent- and case-insensitive) whether that quote really exists in the text. The page marks each reason ✓ or not. It doesn't correct the model; it just makes it obvious when it's making things up.

Built to be interrupted

Each job is saved as soon as it's scored, and processed emails are tracked by Message-ID in H2 (so my Gmail "read" state is never touched). If a run dies halfway, the next one continues where it stopped.

Two-line summary with a safety net

A second Gemma call writes the two-line summary from the shortlist. If it leaves out a shortlisted company, a deterministic fallback summary is used instead. I'd rather have a boring summary than one that hides the only job worth applying to.

Why Open Matters Here

  • Privacy, concretely. My résumé and the contents of my inbox never go to anyone's API. The only network traffic is the IMAP fetch; all inference runs on my own GPU.
  • Zero recurring cost. This runs every night, forever. With a paid API, every scored job would cost something, every day, for as long as I'm job hunting. Locally, it's electricity.
  • Swapping models is one line. spring.ai.ollama.chat.model: gemma3:4b → qwen2.5:3b. Same pipeline, same schema.

Where It Fell Short (Honestly)

  • Scores cluster. 16 of the 49 jobs got exactly 65, just under my cutoff. A remote "Desenvolvedor Java Jr" landed at 65 while a remote "Backend Developer Jr" got 75. With only a snippet of each job, Gemma 4B seems to fall back to a few "default" scores when it's unsure. I'd rather it be cautious than pad my shortlist, but the boundary is fuzzy, and I check the 65s by hand for now.
  • Messy titles are my fault, not the model's. LinkedIn alerts glue extra text into the title area ("Recrutando agora", "Crescimento rápido", "3 ex-alunos…"), and my parser still picks some of it up. That's next on the list.

What's Next

  • Clean up the LinkedIn title extraction.
  • Run the same night's alerts through qwen2.5:3b (replay mode already makes this a one-flag test) and compare.
  • Tune the scoring guide so "unsure" doesn't always mean 65.

Prize Categories

Best Use of Gemma: Gemma 3 4B via Ollama does all the scoring and the morning summary, locally, with schema-enforced structured output.


Related reading on DEV:

來源:DEV Community · dev.to