Files
bin/docs/projekt-matching-user-manual.md
tlg 58b64c6d5f docs(projekt-matching): document poison-mail incident and fixes
Design doc: new §4.6 post-mortem (quote char -> guided-decoding
whitespace loop -> dead IMAP conn -> non-terminal mail), updated
module inventory, vLLM/e-mail experience sections, test counts
(51 -> 63) and the latency limitation (now bounded by max_tokens).
User manual: version note + troubleshooting row for repeating alerts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:54:06 +02:00

12 KiB

Projekt-Matching — User Manual

The automated pipeline that turns freelancermap project e-mails into evaluated EspoCRM opportunities. Version of 2026-07-09 (all acceptance gates passed); updated 2026-07-14 (robustness fixes: alert-loop bug resolved, outgoing mails now also appear in the Sent folder).


1. What the system does

Every 5 minutes, the system checks the mailbox chancen@destengs.com for new e-mails containing freelancermap project descriptions. For every project it finds, it:

  1. resolves the project's canonical URL and checks whether it already exists in the CRM (if yes: nothing happens — no duplicates),
  2. fetches the full project description from freelancermap,
  3. lets the local AI (vLLM, AxionML/Qwen3.5-9B-NVFP4 — nothing leaves your machine) extract the requirements and classify them as Must / Nice / Misc,
  4. lets the AI rate every Must/Nice requirement against your CV ( covered / not covered / partial evidence),
  5. computes the match values deterministically in Python (the AI never invents the numbers): = 100, = 25, = 0, averaged per category,
  6. decides: Must-have-Match > 85 % → the project is considered; otherwise it is rejected,
  7. for considered projects: creates the company (Account), contact person (Contact) and the opportunity in EspoCRM — with the same description table the old Claude skill produced — and sends you a notification e-mail,
  8. records every project evaluation as a trace in Langfuse for your review,
  9. moves the processed trigger e-mail to the server's Trash folder.
   e-mail arrives                     every 5 min (systemd timer)
        │                                   │
        ▼                                   ▼
   INBOX (chancen@) ◀──── poll ──── Langflow "PM Ingest"
        │                                   │  per project:
        │  non-project mail:                ▼
        │  left untouched          Langflow "PM Projekt bewerten"
        │                          fetch → AI extract → AI match CV
        │                          → deterministic match calc
        ▼                          → gate: Must > 85 %?
   processed mail                     │yes            │no
   → Trash                            ▼               ▼
                                  EspoCRM entry    (nothing;
                                  + notification    trace only)
                                    e-mail
                                       │
                                       ▼
                              Langfuse trace (always)

2. What you receive

Notification (a project matched)

  • To: Thomas.Langer@destengs.com
  • Subject: [Projekt-Match] Freelancermap — <Must> % — <Projektname>
  • Body: both match values and the direct link to the CRM opportunity (https://crm.creature-go.com/#Opportunity/view/<id>).

Alert (something went wrong)

  • To: Thomas.Langer@destengs.com
  • Subject: [Projekt-Match-Fehler] Freelancermap — <n> Projekt(e)
  • Body: one entry per failed project with title, URL and the error reason.

Important: a failed project is not retried. The alert e-mail is your record — if the project matters, evaluate it manually (the URL is in the alert). This is deliberate: it prevents endless retry loops and inbox congestion.

What you will NOT see

  • Rejected projects produce no e-mail and no CRM entry. Their full evaluation (table + match values) is in the Langfuse trace.
  • Non-project e-mails in the inbox are inspected once, invisibly marked with an IMAP keyword ($ProjektChecked), and left untouched forever after.
  • The system never processes its own notification/alert mails (recognized by the subject prefix).

3. The CRM entry

Considered projects get the full structure the Claude skill used to create:

Element Rule
Opportunity name project title; duplicate names get (2), (3)
Description exact skill format: match line + | Nr. | Kat. | ❔ | Anforderung | table, Must → Nice → Misc
cProjektlink canonical project URL (also the duplicate-detection key)
Team Projekt → DesTEngS · Arbeitnehmer-Angebot → Arbeitnehmer · ANÜ → ANÜ
Account deduplicated by name; agency → Reseller + linked via Über Agentur (cAccount1), direct client → Customer + linked via account
Contact created and linked when the posting names a person and a company

Ambiguities the skill used to ask you about are resolved by defaults: unclear offer type → Projekt; unclear buyer type → agency.

4. Reviewing quality in Langfuse

Open http://127.0.0.1:8091 (login admin@example.com / langfuse-admin-pw unless you changed it), project projekt-matching.

  • Every evaluated project is one trace named projekt-match, tagged with its outcome (created / rejected / failed). The trace shows the canonical URL, the full requirements table, both match values and — for created projects — the CRM link.
  • Your evaluation workflow: open the annotation queue projekt-matching-review and score each trace on the two dimensions:
    • extraction-correct — were Must/Nice/Misc correctly derived from the posting? (correct / partially-correct / wrong)
    • matching-correct — are the // ratings against your CV right?
  • The scores are your quality record. Nothing acts on them automatically — they tell you whether the local 9B model stays good enough over time.

Langflow's own execution traces (component level, LLM prompts/outputs) land in the same project and are useful for debugging.

5. Everyday operations

All commands run as user lwc on this host.

Is it running?

systemctl --user list-timers | grep projekt-matching   # next/last tick
curl -s http://127.0.0.1:8090/health                   # Langflow: {"status":"ok"}
journalctl --user -u projekt-matching.service -n 5     # last trigger output

To see what recent runs did, use the Langfuse traces (section 4). Do not use podman logs langflow_ctr — it is flooded by a known harmless logging loop and tells you nothing.

Pause / resume the automation

systemctl --user stop  projekt-matching.timer    # pause (mail queues up safely in INBOX)
systemctl --user start projekt-matching.timer    # resume; next tick processes the backlog

Restart Langflow (e.g. after a hang)

systemctl --user restart container-langflow_ctr.service

Never use podman restart langflow_ctr — with the systemd-managed container this can leave the container removed and the pipeline dead.

Update your CV or the Rahmenbedingungen

Edit the originals in /home/tlg/mkt/bewerb/vorgaben/, then:

/home/lwc/bin/projekt-matching/deploy/deploy_files.sh

The script copies the files, redeploys the code, restarts Langflow safely and waits until it is healthy again (≈ 1 minute).

Change the notification recipient or the 85 % threshold

Both live in Langflow global variables (UI: http://127.0.0.1:8090 → Settings → Global Variables, or via API): PM_NOTIFY_TO, PM_ALERT_TO, PM_THRESHOLD (also: PM_VLLM_BASE, PM_VLLM_MODEL, PM_ESPO_BASE). Changes take effect on the next run — no restart needed.

After a reboot

Everything is systemd-managed and starts automatically (pod-langflow_pod for the services, projekt-matching.timer for the polling). Nothing to do.

6. Troubleshooting

Symptom Likely cause / what to do
No mails processed, INBOX grows systemctl --user list-timers — timer active? Langflow healthy (curl …/health)? Restart per section 5. Mails are never lost: the next successful tick processes the backlog.
Alert mail: Abruf fehlgeschlagen / Seitentext zu kurz freelancermap page unreachable or behind a login wall. Evaluate that project manually via the URL in the alert.
Alert mail: vLLM HTTP … / LLM lieferte kein JSON / Read timed out the local AI was down or overloaded. Occasional single failures are absorbed by built-in retries; if alerts pile up, check the vLLM service on port 8081.
The same alert arrives again and again for the same project this was a bug (fixed 2026-07-14): one poisonous posting could block the pipeline and repeat its alert every ~35 min. A failed project is now reliably final — if you ever see identical repeating alerts again, restart Langflow (section 5) and report it.
Alert mail: Team … nicht gefunden the CRM user cowork-api lost read access to Teams, or a team was renamed. Fix in EspoCRM admin.
A project you expected is missing check Langfuse: if its trace says rejected, the Must-match was ≤ 85 % — the table shows why. If there is no trace at all, check the alert mails.
Duplicate opportunity suspected duplicates are keyed on the canonical URL (cProjektlink). The same project posted by two different agencies has two different URLs and is intentionally kept as two opportunities.
Everything looks dead after an experiment systemctl --user start container-langflow_ctr.service, then check health.

7. Test utilities (use with care)

In /home/lwc/bin/projekt-matching/tests/e2e/ (run with the project venv, secrets sourced: set -a && source deploy/secrets.local.env && set +a):

  • check_state.py [token] — shows CRM hits for a URL token plus the last INBOX/Trash subjects. Safe, read-only.
  • run_flow2.py <canonical-url> <title> — evaluates one project directly (bypasses e-mail). Writes to the CRM if it matches!
  • send_test_mail.py "<subject>" [fallback-url] — re-sends a stored example mail to yourself to trigger the full pipeline.
  • cleanup_crm.py [tokens…]deletes opportunities whose cProjektlink matches the tokens. Default tokens are the two historical test projects. Never run it with a token that matches real opportunities.
  • flag_inbox.py — marks every current INBOX mail as "already checked" (used once at go-live; rarely needed again).

8. Where things live

What Where
Code + tests /home/lwc/bin/projekt-matching/ (git)
Flows (source of truth) built by deploy/build_flows.py into Langflow ("PM Ingest", "PM Projekt bewerten")
Deployed runtime copy ~/.local/share/langflow_pod/langflow-data/ (/app/langflow in-container)
Secrets ~/.config/projekt-matching/ (0600) + Langflow global variables (encrypted); EspoCRM key sourced from /home/tlg/mkt/bewerb/.secrets/espocrm-api.md
systemd units ~/.config/systemd/user/projekt-matching.{service,timer}
Services Langflow http://127.0.0.1:8090 · Langfuse http://127.0.0.1:8091 · vLLM http://127.0.0.1:8081 · pod script /home/lwc/bin/create_pod_langflow.sh
Design details docs/projekt-matching-design.md, spec/plan under docs/superpowers/

9. Known limitations

  • freelancermap only. Mails from other portals are ignored (left in the inbox untouched). Extending detection is a small code change in projektmatch/mailparse.py.
  • Login-protected postings fail with an alert (no page text, no snippet fallback — a snippet cannot support honest matching).
  • Nice-to-have-Match varies a few points between runs of the same project (single / flips by the 9B model). The Must-match — the gate — has been stable in all verification runs. Watch this in the annotation queue.
  • Rare edge case: if the CRM write succeeds but the notification mail fails to send, the opportunity exists but no mail arrives and none is re-sent. The Langfuse trace records it (error: notify: …).
  • Misc rows (start date, workload, …) are occasionally missing from the extraction; they never influence the match values.