feat: PYOK LLM integration, LinkedIn import, job post tailoring (v0.3.0)#96
feat: PYOK LLM integration, LinkedIn import, job post tailoring (v0.3.0)#96
Conversation
Add pass-your-own-key (PYOK) infrastructure for LLM-powered features: - LLMProvider ABC and LLMConfig dataclass in core/llm/protocol.py - Prompt templates for tailoring, job extraction, LinkedIn, colors - LiteLLM wrapper client implementing LLMProvider protocol - API key resolution from CLI args and environment variables - requires_llm decorator for feature gating behind [llm] optional dep - LLMNotAvailableError and LLMError exception types - 58 unit tests covering all new modules
Phase 2a - LinkedIn Import: - core/importers/linkedin.py: pure transform from LinkedIn profile dict to simple-resume YAML schema - shell/importers/linkedin_fetcher.py: HTML and text file parsing with BeautifulSoup, supporting .html and .txt formats - 27 unit tests for importer and fetcher Phase 2b - Job Post Tailoring: - core/ats/tailor.py: gap analysis comparing resume skills/keywords against job requirements, weighted match scoring, and LLM prompt construction for resume tailoring - JobRequirements and GapAnalysis dataclasses - 14 unit tests for tailor module
- Add LLMProvider, LLMConfig, LLMError, LLMNotAvailableError exports - Add linkedin_to_simple_resume export - Add JobRequirements, GapAnalysis, analyze_gaps, build_tailoring_prompt - Update API surface contract test with new symbols - Bump version from 0.2.5 to 0.3.0 in pyproject.toml and __init__.py
PR Review: feat: PYOK LLM integration, LinkedIn import, job post tailoring (v0.3.0)Branch: Verdict: APPROVE with non-blocking suggestionsScope ValidationPR implements 3 issues (#8, #9, #10) matching the design plan in Architecture ✅Clean FCIS (Functional Core / Imperative Shell) separation:
Blocking IssuesNone. The PR is clean and well-structured. Non-Blocking Improvements1. Missing LLM response None check ( return response.choices[0].message.content # Could be NoneIf the model returns no content, this silently returns content = response.choices[0].message.content
if content is None:
raise LLMError("LLM returned empty response", provider="litellm", model=self.config.model)
return content2. Tailored output not validated ( 3. Dead parameter in 4. raw = raw.strip()
if raw.startswith("```"):
raw = "\n".join(raw.split("\n")[1:-1])5. Slop Detection ✅
Code Quality ✅
Security ✅
Version Consistency ✅
|
Test PlanAutomated Verification
Manual Verification ChecklistPYOK Infrastructure (#8)
LinkedIn Import (#9)
Job Post Tailoring (#10)
Regression
|
PR Review Addendum - Deep Analysis FindingsAdditional findings from parallel code review and test coverage analysis. Elevated to Non-Blocking (from agent reviews)6. Hard 7. 8. Duplicated default model string ( 9. Missing Test Coverage Gaps (from test analysis agent)
The Fragile Test Pattern
|
PR Review Addendum 2 - Core Domain Findings (Verified)Three additional findings from deep core domain analysis, all verified with reproduction evidence. Elevated Findings10. SCORING BUG: When a job posting has only required skills (no preferred, no keywords), a perfect match scores 0.5 instead of 1.0. The formula computes # Reproduction:
reqs = JobRequirements(title='Eng', required_skills=['Python', 'Docker'])
resume = {'keyskills': ['Python', 'Docker']}
gap = analyze_gaps(resume, reqs)
# gap.match_score = 0.5 ← should be 1.0Fix: normalize by active weight sum: active = [c for c in counts if c.total > 0]
if not active:
return 0.0
weighted = sum(c.weight * (c.matched / c.total) for c in active)
return round(weighted / sum(c.weight for c in active), 3)11. SECURITY: API key leaked in Default dataclass Fix: 12. Substring matching false positives in skill checking (
reqs = JobRequirements(title='Eng', required_skills=['Go'])
resume = {'keyskills': ['Django']}
gap = analyze_gaps(resume, reqs)
# gap.matched_required_skills = ['Go'] ← false positiveFix: use word-boundary regex ( Recommendation UpdateItems 10-11 are worth fixing before merge. Item 12 is a known limitation of simple string matching and can be deferred if documented. |
Comprehensive PR Review (pr-review-toolkit) — Consolidated FindingsFour specialized agents analyzed PR #96 in parallel: silent-failure-hunter, type-design-analyzer, comment-analyzer, and code-simplifier. Findings are deduplicated and cross-referenced with the earlier sanctum review. Critical Issues (3)C1. C2. Scoring formula doesn't normalize by active weights — C3. API key leaked in Important Issues (8)I1. I2. Raw LLM output written without validation — I3. I4. Substring matching produces false positives — I5. Frozen dataclasses with mutable I6. Broad I7. No logging in CLI error handlers — I8. Missing Suggestions (12)
Strengths (Positive Observations)
Recommended Action PlanBefore merge (high confidence):
Should fix (medium priority): Can defer to follow-up: Review generated by: silent-failure-hunter, type-design-analyzer, comment-analyzer, code-simplifier |
- Hide API key from LLMConfig repr to prevent accidental exposure - Add null check for LLM response content, raise LLMError if empty - Normalize scoring weights by active categories so required-only perfect matches score 1.0 instead of 0.5 - Preserve exception type name in LLMError messages for debuggability
- Catch yaml.YAMLError and validate dict type in _load_resume_yaml - Strip markdown code fences and validate YAML before writing LLM-tailored output to disk - Add logger.error() calls to all CLI error handlers in _tailor.py and _import.py for diagnostic traceability
Address PR review feedback on import placement: - Extract _handle_unexpected_error to _errors.py to break circular imports between main.py and CLI subcommand modules - Move lazy imports to top level in _tailor.py, _screen.py - Use top-level try/except for optional litellm in client.py, gate.py - Move lazy imports to top level in resume_extensions.py, loader.py - Keep linkedin imports lazy in _import.py (optional [linkedin] dep) - Remove docs/plans/ design doc (content captured in PR description) - Fix missing import re in tailor.py _check_skills - Update test mock paths to match new import locations
PR Fix Summary — Import Cleanup (ab0fdf9)All 9 review threads addressed and resolved. What changedCircular import fix: Extracted Top-level imports: Moved all lazy imports to module top level in:
Intentionally kept lazy: Docs: Removed Also fixed: Missing Verification
|
Summary
LLMProviderprotocol,LiteLLMProviderwrapper, automatic API key resolution from environment variables, and graceful degradation viarequires_llmdecorator when[llm]extra is not installed.simple-resume importCLI subcommand that parses HTML and plain-text LinkedIn profile exports and converts them to simple-resume YAML format.simple-resume tailorCLI subcommand with resume-job gap analysis (analyze_gaps), optional LLM-assisted tailoring viabuild_tailoring_prompt, and--reportflag for gap-only analysis without LLM.Fixes #8, Fixes #9, Fixes #10
Architecture
Follows Functional Core / Imperative Shell pattern:
core/llm/protocol.pyLLMProviderABC +LLMConfigdataclasscore/llm/prompts.pycore/ats/tailor.pycore/importers/linkedin.pyshell/llm/client.pyLiteLLMProvider(litellm wrapper)shell/llm/config.pyshell/llm/gate.pyrequires_llmdecorator, availability checkshell/importers/linkedin_fetcher.pyshell/cli/_import.pyimportCLI subcommandshell/cli/_tailor.pytailorCLI subcommandNew Optional Dependencies
Test plan
simple-resume --versionreports 0.3.0