Places a ✨ Suggest button inline with the Skills & Keywords subheader.
On click, calls suggest_resume_keywords() and stores results in session
state. Suggestions render as per-category chip panels (skills, domains,
keywords); clicking a chip appends it to the YAML and removes it from
the panel. A ✕ Clear button dismisses the panel entirely.
- Remove old inline _suggest_search_terms (no blocklist/profile awareness)
- Replace with import shim delegating to scripts/suggest_helpers.py
- Call site now loads blocklist.yaml + user.yaml and passes them through
- Update button help text to reflect blocklist, mission values, career background
- Settings → Search: add-title (+) and Import buttons crashed with
StreamlitAPIException when writing to _sp_titles_multi after it was
already instantiated. Fix: pending-key pattern (_sp_titles_pending /
_sp_locs_pending) applied before widget renders on next pass.
- Home setup banners: fired for email/notion/keywords even when those
features were already configured. Add 'done' condition callables
(_email_configured, _notion_configured, _keywords_configured) to
suppress banners automatically when config files are present.
- Services tab start/stop buttons: docker CLI was unavailable inside
the container so _docker_available was False and buttons never showed.
Bind-mount host /usr/bin/docker (ro) + /var/run/docker.sock into the
app container so it can control sibling containers via DooD pattern.
BYOK policy: if a user supplies any LLM backend (local ollama/vllm or
their own API key), they get full access to AI generation features.
Charging for the UI around a service they already pay for is bad UX.
app/wizard/tiers.py:
- BYOK_UNLOCKABLE frozenset: pure LLM-call features that unlock with
any configured backend (llm_career_summary, company_research,
interview_prep, survey_assistant, voice guidelines, etc.)
- has_configured_llm(): checks llm.yaml for any enabled non-vision
backend; local + external API keys both count
- can_use(tier, feature, has_byok=False): BYOK_UNLOCKABLE features
return True when has_byok=True regardless of tier
- tier_label(feature, has_byok=False): suppresses lock icon for
BYOK_UNLOCKABLE features when BYOK is active
Still gated (require CF infrastructure, not just an LLM call):
llm_keywords_blocklist, email_classifier, model_fine_tuning,
shared_cover_writer_model, multi_user, all integrations
app/pages/2_Settings.py:
- Compute _byok = has_configured_llm() once at page load
- Pass has_byok=_byok to can_use() for _gen_panel_active
- Update caption to mention BYOK as an alternative to paid tier
app/pages/0_Setup.py:
- Wizard generation widget passes has_byok=has_configured_llm()
to can_use() and tier_label()
tests/test_wizard_tiers.py:
- 6 new BYOK-specific tests covering unlock, non-unlock, and
label suppression cases
Adds a fully neutered public demo for menagerie.circuitforge.tech/peregrine
that shows the Peregrine UI without exposing any personal data or real LLM inference.
scripts/llm_router.py:
- Block all inference when DEMO_MODE env var is set (1/true/yes)
- Raises RuntimeError with a user-friendly "public demo" message
app/app.py:
- IS_DEMO constant from DEMO_MODE env var
- Wizard gate bypassed in demo mode (demo/config/user.yaml pre-seeds a fake profile)
- Demo banner in sidebar: explains read-only status + links to circuitforge.tech
compose.menagerie.yml (new):
- Separate Docker Compose project (peregrine-demo) on host port 8504
- Mounts demo/config/ and demo/data/ — isolated from personal instance
- DEMO_MODE=true, no API keys, no /docs mount
- Project name: peregrine-demo (run alongside personal instance)
demo/config/user.yaml:
- Generic "Demo User" profile, wizard_complete=true, no real personal info
demo/config/llm.yaml:
- All backends disabled (belt-and-suspenders alongside DEMO_MODE block)
demo/data/.gitkeep:
- staging.db is auto-created on first run, gitignored via demo/data/*.db
.gitignore: add demo/data/*.db
Caddy routes menagerie.circuitforge.tech/peregrine* → 8504 (demo instance).
Personal Peregrine remains on 8502, unchanged.
- compose.yml: pass STREAMLIT_SERVER_BASE_URL_PATH from .env into container
Streamlit prefixes all asset URLs with the path so Caddy handle_path routing works.
Without this, /static/* requests skip the /peregrine* route → 503 text/plain MIME error.
- config/server.yaml.example: document base_url_path + server_port settings
- .gitignore: ignore config/server.yaml (local gitignored instance of server.yaml.example)
- app/pages/2_Settings.py: add Deployment/Server expander under System tab
Shows active base URL path from env; saves edits to config/server.yaml + .env;
prompts user to run ./manage.sh restart to apply.
Refs: https://docs.streamlit.io/develop/api-reference/configuration/config.toml#server.baseUrlPath
- Job titles and locations: replaced text_area with st.multiselect + + add button + paste-list expander
- ✨ Suggest now populates the titles dropdown (not auto-selected) — user picks what they want
- Suggested exclusions still use click-to-add chip buttons
- Removed duplicate Notion expander from System Settings (handled by Integrations tab)
- Services panel: show host terminal copy-paste command when docker CLI unavailable (app runs inside container)
- Resume Profile tab: upload widget replaces error+stop when YAML missing;
collapsed "Replace Resume" expander when profile exists; saves parsed
data and raw text (for LLM context) in one step
- FILL_IN banner with clickable link to Setup wizard when incomplete fields detected
- Ollama not reachable hint references Services section below
- Fine-tune hint clarifies "My Profile tab above" with inference profile names
- vLLM no-models hint links to Fine-Tune tab
Removed the dropdown-based sidebar panel in favour of ✨ Generate buttons
placed directly below Career Summary, Voice & Personality, and each Mission
& Values row. Prompts now incorporate the live field value as a draft to
improve, plus resume experience bullets as context for Career Summary.
Replace chip-button tag management with st.multiselect backed by bundled
suggestions. Existing user tags are preserved as custom options alongside
the suggestion list. Custom tag input validates through filter_tag() before
adding — rejects URLs, profanity, overlong strings, and bad characters.
Changes auto-save on multiselect interaction; custom tags append on + click.
- preflight.py now writes PEREGRINE_GPU_COUNT and PEREGRINE_GPU_NAMES to
.env so the app container gets GPU info without needing nvidia-smi access
- compose.yml passes PEREGRINE_GPU_COUNT, PEREGRINE_GPU_NAMES, and
RECOMMENDED_PROFILE as env vars to the app service
- 0_Setup.py _detect_gpus() reads PEREGRINE_GPU_NAMES env var first;
falls back to nvidia-smi (bare / GPU-passthrough environments)
- 0_Setup.py _suggest_profile() reads RECOMMENDED_PROFILE env var first
- requirements.txt: add pdfplumber (needed for resume PDF parsing)
Without __init__.py, Python treats app/ as a namespace package that
doesn't resolve correctly when running from WORKDIR /app inside the
container. 'from app.wizard.step_hardware import ...' raises
ModuleNotFoundError: No module named 'app.wizard'; 'app' is not a package
- setup.sh: replace docker-image-based NVIDIA test with nvidia-ctk validate
(faster, no 100MB pull, no daemon required); add check_docker_running()
to auto-start the Docker service on Linux or warn on macOS
- prepare_training_data.py: also scan training_data/uploads/*.{md,txt}
so web-uploaded letters are included in training data
- task_runner.py: add prepare_training task type (calls build_records +
write_jsonl inline; reports pair count in task result)
- Settings fine-tune tab: Step 1 accepts .md/.txt uploads; Step 2 Extract
button submits prepare_training background task + shows status; Step 3
shows make finetune command + live Ollama model status poller
- llm.yaml + example: replace localhost URLs with Docker service names
(ollama:11434, vllm:8000, vision:8002); replace personal model names
(alex-cover-writer, llama3.1:8b) with llama3.2:3b
- user.yaml.example: update service hosts to Docker names (ollama, vllm,
searxng) and searxng port from 8888 (host-mapped) to 8080 (internal)
- wizard step 5: fix hardcoded localhost defaults — wizard runs inside
Docker, so service name defaults are required for connection tests to pass
- scrapers/companyScraper.py: bundle scraper so Dockerfile COPY succeeds
- setup.sh: remove host Ollama install (conflicts with Docker Ollama on
port 11434); Docker entrypoint handles model download automatically
- README + setup.sh banner: add Circuit Forge mission statement
- generate() accepts previous_result + feedback; appends both to LLM prompt
- task_runner cover_letter handler parses params JSON, passes fields through
- Apply Workspace: "Refine with Feedback" expander with text area + Regenerate
button; only shown when a draft exists; clears feedback after submitting
- 8 new tests (TestGenerateRefinement + TestTaskRunnerCoverLetterParams)
Replaces the old 5-step wizard with a 7-step orchestrator that uses the
step modules built in Tasks 2-8. Steps 1-6 are mandatory (hardware, tier,
identity, resume, inference, search); step 7 (integrations) is optional.
Each Next click validates, writes wizard_step to user.yaml for crash recovery,
and resumes at the correct step on page reload. LLM generation buttons
submit wizard_generate tasks and poll via @st.fragment(run_every=3). Finish
sets wizard_complete=True, removes wizard_step, and calls apply_service_urls.
Adds tests/test_wizard_flow.py (7 tests) covering validate() chain, yaml
persistence helpers, and wizard state inference.
LGBTQIA+ inclusion section in research briefs:
- user_profile.py: add candidate_lgbtq_focus bool accessor
- user.yaml.example: add candidate_lgbtq_focus flag (default false)
- company_research.py: gate new LGBTQIA+ section behind flag; section
count now dynamic (7 base + 1 per opt-in section, max 9)
- 2_Settings.py: add "Research Brief Preferences" expander with
checkboxes for both accessibility and LGBTQIA+ focus flags;
mission_preferences now round-trips through save (no silent drop)
Phase 2 fixes:
- manage-vllm.sh: MODEL_DIR and VLLM_BIN now read from env vars
(VLLM_MODELS_DIR, VLLM_BIN) with portable defaults
- search_profiles.yaml: replace personal CS/TAM/Bay Area profiles
with a documented generic starter profile
Phase 3 fix:
- llm.yaml: rename alex-cover-writer:latest → llama3.2:3b with
inline comment for users to substitute their fine-tuned model;
fix model-exclusion comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 3_Resume_Editor.py: replace "Alex's" in docstring and caption
- user_profile.py: expose mission_preferences and candidate_accessibility_focus
- user.yaml.example: add mission_preferences section + candidate_accessibility_focus flag
- generate_cover_letter.py: build _MISSION_NOTES from user profile instead of
hardcoded personal passion notes; falls back to generic defaults when not set
- company_research.py: gate "Inclusion & Accessibility" section behind
candidate_accessibility_focus flag; section count adjusts (7 or 8) accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add moondream2 vision service to compose.yml (single-gpu + dual-gpu profiles)
- Create scripts/vision_service/Dockerfile for the vision container
- Add VISION_PORT, VISION_MODEL, VISION_REVISION vars to .env.example
- Add Vision Service entry to SERVICES list in Settings (hidden unless gpu profile active)
- Add Fine-Tune Wizard tab (Task 10) to Settings with 3-step upload→preview→train flow
- Tab is always rendered; shows info message when non-GPU profile is active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded systemd/shell-script service commands with docker compose
profile-aware commands. Add inference_profile-based filtering (hidden flag
removes Ollama on remote profile, vLLM unless dual-gpu). Replace TCP socket
health check with HTTP-based _port_open() that accepts host/ssl/verify params
for remote/TLS-terminated service support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>