feat: complete generalization — smoke tests, README, all personal refs extracted
- UserProfile class drives all personal data - First-run wizard gates app until user.yaml exists - Docker Compose stack: remote/cpu/single-gpu/dual-gpu profiles - Vision service containerized (single-gpu/dual-gpu) - All Alex/Library references removed from app and scripts - Circuit Forge LLC / Peregrine branding throughout
This commit is contained in:
parent
a61fd43eb1
commit
a70b9f5627
2 changed files with 94 additions and 0 deletions
71
README.md
Normal file
71
README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Peregrine
|
||||
|
||||
**AI-powered job search pipeline — by [Circuit Forge LLC](https://circuitforge.io)**
|
||||
|
||||
Automates the full job search lifecycle: discovery → matching → cover letters → applications → interview prep.
|
||||
Privacy-first, local-first. Your data never leaves your machine.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://git.circuitforge.io/circuitforge/peregrine
|
||||
cd peregrine
|
||||
cp .env.example .env
|
||||
docker compose --profile remote up -d
|
||||
```
|
||||
|
||||
Open http://localhost:8501 — the setup wizard will guide you through the rest.
|
||||
|
||||
---
|
||||
|
||||
## Inference Profiles
|
||||
|
||||
| Profile | Services | Use case |
|
||||
|---------|----------|----------|
|
||||
| `remote` | app + searxng | No GPU; LLM calls go to Anthropic/OpenAI |
|
||||
| `cpu` | app + ollama + searxng | No GPU; local models on CPU (slow) |
|
||||
| `single-gpu` | app + ollama + vision + searxng | One GPU for cover letters + research + vision |
|
||||
| `dual-gpu` | app + ollama + vllm + vision + searxng | GPU 0 = Ollama, GPU 1 = vLLM |
|
||||
|
||||
Set the profile in `.env`:
|
||||
```bash
|
||||
# .env
|
||||
DOCKER_COMPOSE_PROFILES=single-gpu
|
||||
```
|
||||
|
||||
Or select it during the setup wizard.
|
||||
|
||||
---
|
||||
|
||||
## First-Run Wizard
|
||||
|
||||
On first launch, the app shows a 5-step setup wizard:
|
||||
|
||||
1. **Hardware Detection** — auto-detects NVIDIA GPUs and suggests a profile
|
||||
2. **Your Identity** — name, email, career summary (used in cover letters and prompts)
|
||||
3. **Sensitive Employers** — companies masked as "previous employer (NDA)" in research briefs
|
||||
4. **Inference & API Keys** — Anthropic/OpenAI keys (remote), or Ollama model (local)
|
||||
5. **Notion Sync** — optional; syncs jobs to a Notion database
|
||||
|
||||
Wizard writes `config/user.yaml`. Re-run by deleting that file.
|
||||
|
||||
---
|
||||
|
||||
## Email Sync (Optional)
|
||||
|
||||
Peregrine can monitor your inbox for job-related emails (interview requests, rejections, survey links) and automatically update job stages.
|
||||
|
||||
Configure via **Settings → Email** after setup. Requires:
|
||||
- IMAP access to your email account
|
||||
- For Gmail: enable IMAP + create an App Password
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Core discovery pipeline: [MIT](LICENSE-MIT)
|
||||
AI features (cover letter generation, company research, interview prep): [BSL 1.1](LICENSE-BSL)
|
||||
|
||||
© 2026 Circuit Forge LLC
|
||||
23
tests/test_app_gating.py
Normal file
23
tests/test_app_gating.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from pathlib import Path
|
||||
import yaml
|
||||
from scripts.user_profile import UserProfile
|
||||
|
||||
|
||||
def test_wizard_gating_logic(tmp_path):
|
||||
"""Wizard gate should trigger when user.yaml is absent."""
|
||||
missing = tmp_path / "user.yaml"
|
||||
assert not UserProfile.exists(missing)
|
||||
|
||||
|
||||
def test_wizard_gating_passes_after_setup(tmp_path):
|
||||
"""Wizard gate should clear once user.yaml is written."""
|
||||
p = tmp_path / "user.yaml"
|
||||
p.write_text(yaml.dump({"name": "Test User", "services": {}}))
|
||||
assert UserProfile.exists(p)
|
||||
|
||||
|
||||
def test_wizard_gating_empty_file_still_exists(tmp_path):
|
||||
"""An empty user.yaml still clears the gate (wizard already ran)."""
|
||||
p = tmp_path / "user.yaml"
|
||||
p.write_text("")
|
||||
assert UserProfile.exists(p)
|
||||
Loading…
Reference in a new issue