Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 2synthesize-recruitly/HrFlow/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Backend package for the AI Candidate Synthesis Agent."""
62 changes: 62 additions & 0 deletions 2synthesize-recruitly/HrFlow/backend/jobs_fallback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[
{
"key": "391db21d-fc9f-4db5-b281-ff8005548735",
"title": "Senior Python Backend Engineer",
"skills": ["docker", "fastapi", "postgresql", "rest apis", "python", "microservices", "communication", "teamwork", "problem solving", "autonomy"],
"summary": "Senior backend engineer role focused on Python, FastAPI microservices, PostgreSQL and containerized deployments."
},
{
"key": "a7c142e8-3d21-4f90-b6e5-cc9012345678",
"title": "Full Stack Developer",
"skills": ["react", "typescript", "node.js", "postgresql", "rest apis", "git", "communication", "adaptability", "teamwork", "creativity"],
"summary": "Full stack developer building modern web applications with React frontend and Node.js backend."
},
{
"key": "b2d389fa-91cc-4a77-8f31-dd4456789abc",
"title": "DevOps Engineer",
"skills": ["docker", "kubernetes", "ci/cd", "gitlab", "prometheus", "linux", "bash", "rigor", "autonomy", "stress management"],
"summary": "DevOps engineer responsible for CI/CD pipelines, container orchestration and infrastructure monitoring."
},
{
"key": "c4e501bc-77ab-4b88-9e42-ee5567890def",
"title": "Data Engineer",
"skills": ["python", "sql", "spark", "airflow", "postgresql", "data pipelines", "analytical thinking", "rigor", "communication"],
"summary": "Data engineer designing and maintaining scalable data pipelines and analytics infrastructure."
},
{
"key": "d5f612cd-88bc-4c99-af53-ff6678901ghi",
"title": "HR Business Partner",
"skills": ["talent acquisition", "employee relations", "performance management", "hris", "onboarding", "labor law", "empathy", "active listening", "conflict resolution", "leadership"],
"summary": "HR Business Partner supporting managers and employees across the full employee lifecycle."
},
{
"key": "e6a723de-99cd-4d00-bg64-aa7789012jkl",
"title": "Talent Acquisition Specialist",
"skills": ["sourcing", "interviewing", "ats tools", "employer branding", "linkedin recruiter", "negotiation", "communication", "organization", "persuasion", "empathy"],
"summary": "Recruiter responsible for sourcing, screening and hiring top talent across technical and business roles."
},
{
"key": "f7b834ef-00de-4e11-ch75-bb8890123mno",
"title": "Financial Analyst",
"skills": ["financial modeling", "excel", "sql", "reporting", "budget management", "forecasting", "powerbi", "rigor", "analytical thinking", "attention to detail"],
"summary": "Financial analyst responsible for budgeting, forecasting and financial reporting across business units."
},
{
"key": "g8c945fa-11ef-4f22-di86-cc9901234pqr",
"title": "Finance Controller",
"skills": ["accounting", "ifrs", "consolidation", "erp", "excel", "audit", "tax compliance", "rigor", "leadership", "communication"],
"summary": "Finance controller overseeing accounting operations, financial close process and compliance reporting."
},
{
"key": "h9d056gb-22fa-4a33-ej97-dd0012345stu",
"title": "Product Manager",
"skills": ["product strategy", "roadmap planning", "agile", "jira", "user research", "data analysis", "leadership", "communication", "prioritization", "stakeholder management"],
"summary": "Product Manager driving product vision, roadmap and delivery in close collaboration with engineering and design."
},
{
"key": "i0e167hc-33ab-4b44-fk08-ee1123456vwx",
"title": "UX/UI Designer",
"skills": ["figma", "user research", "wireframing", "prototyping", "design systems", "accessibility", "creativity", "empathy", "communication", "attention to detail"],
"summary": "UX/UI designer crafting intuitive user experiences from research to high-fidelity prototypes."
}
]
103 changes: 97 additions & 6 deletions 2synthesize-recruitly/HrFlow/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,37 @@

import json as _json
import os
from typing import Any, Dict
from contextlib import asynccontextmanager
from typing import Any, Dict, List

from dotenv import load_dotenv
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware

load_dotenv()

from schemas import InterviewInput
from services.fusion_service import build_fusion_object
from services.hrflow_service import parse_and_score, parse_cv
from services.llm_service import extract_interview_signals, generate_synthesis
try:
from .schemas import InterviewInput
from .services.fusion_service import build_fusion_object
from .services.hrflow_service import parse_and_score, parse_cv
from .services.jobs_service import get_jobs, load_jobs_from_file
from .services.llm_service import extract_candidate_name, extract_interview_signals, generate_synthesis
except ImportError:
from schemas import InterviewInput
from services.fusion_service import build_fusion_object
from services.hrflow_service import parse_and_score, parse_cv
from services.jobs_service import get_jobs, load_jobs_from_file
from services.llm_service import extract_candidate_name, extract_interview_signals, generate_synthesis, parse_test_sheet

app = FastAPI(title="AI Candidate Synthesis Agent", version="1.0.0")

@asynccontextmanager
async def lifespan(app: FastAPI):
"""Load jobs from local JSON file on startup."""
load_jobs_from_file()
yield


app = FastAPI(title="AI Candidate Synthesis Agent", version="1.0.0", lifespan=lifespan)

app.add_middleware(
CORSMiddleware,
Expand All @@ -48,7 +65,9 @@ async def parse_cv_endpoint(
"""
Receive a CV file, forward it to HrFlow, return structured profile data.
Uses HrFlow POST /v1/profile/parsing/file.
Name is extracted independently via LLM to avoid HrFlow name parsing bugs.
"""
import io
content = await file.read()
try:
result = await parse_cv(
Expand All @@ -60,8 +79,29 @@ async def parse_cv_endpoint(
except Exception as exc:
raise HTTPException(status_code=502, detail=f"HrFlow parsing failed: {exc}")

# Extract candidate name via LLM from raw PDF text (more reliable than HrFlow)
full_name = result["full_name"]
try:
filename_lower = (file.filename or "").lower()
if filename_lower.endswith(".pdf") or file.content_type == "application/pdf":
from pypdf import PdfReader
reader = PdfReader(io.BytesIO(content))
cv_text = "\n".join(p.extract_text() or "" for p in reader.pages)
else:
cv_text = content.decode("utf-8", errors="ignore")

if cv_text.strip():
llm_name = await extract_candidate_name(cv_text)
if llm_name:
full_name = llm_name
except Exception:
pass # keep HrFlow name as fallback

return {
"profile_key": result["profile_key"],
"full_name": full_name,
"first_name": result["first_name"],
"last_name": result["last_name"],
"skills": result["skills"],
"experience_count": len(result["experiences"]),
"education_count": len(result["educations"]),
Expand Down Expand Up @@ -134,6 +174,44 @@ async def generate_candidate_synthesis(payload: dict) -> Dict[str, Any]:
return report


# ---------------------------------------------------------------------------
# 14.X — Parse Test Sheet
# ---------------------------------------------------------------------------

@app.post("/api/test/parse")
async def parse_test_sheet_endpoint(file: UploadFile = File(...)) -> Dict[str, Any]:
"""
Parse a technical test sheet (PDF or text) and extract structured scores.
Returns scores dict (category.skill → 1-5) and detected target_skills list.
"""
import io
content = await file.read()
filename = (file.filename or "").lower()

if filename.endswith(".pdf") or file.content_type == "application/pdf":
try:
from pypdf import PdfReader
reader = PdfReader(io.BytesIO(content))
text = "\n".join(p.extract_text() or "" for p in reader.pages)
except Exception as exc:
raise HTTPException(status_code=422, detail=f"Could not read PDF: {exc}")
else:
try:
text = content.decode("utf-8")
except UnicodeDecodeError:
raise HTTPException(status_code=422, detail="File must be a PDF or UTF-8 text file")

if not text.strip():
raise HTTPException(status_code=422, detail="Could not extract text from file")

try:
result = await parse_test_sheet(text)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"LLM test parsing failed: {exc}")

return result


# ---------------------------------------------------------------------------
# 14.5 — Full Pipeline (main demo endpoint)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -229,6 +307,19 @@ async def full_pipeline(
}


# ---------------------------------------------------------------------------
# Jobs list (autocomplete)
# ---------------------------------------------------------------------------

@app.get("/api/jobs/list")
async def jobs_list() -> Dict[str, Any]:
"""
Return all cached jobs for frontend autocomplete.
Each item: { key, title, skills, summary }
"""
return {"jobs": get_jobs()}


# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions 2synthesize-recruitly/HrFlow/backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pydantic>=2.7.0
anthropic>=0.28.0
openai>=1.30.0
python-dotenv>=1.0.0
pypdf>=4.0.0
1 change: 1 addition & 0 deletions 2synthesize-recruitly/HrFlow/backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,4 @@ class CandidateSynthesisReport(BaseModel):
behavioral_assessment: str
consistency_analysis: str
justification: str
domain_fit: str
19 changes: 18 additions & 1 deletion 2synthesize-recruitly/HrFlow/backend/services/hrflow_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,21 @@ async def parse_cv(
body = response.json()

parsing = body.get("data", {}).get("parsing", {})
profile_key = body.get("data", {}).get("profile", {}).get("key", "")
profile = body.get("data", {}).get("profile", {})
profile_key = profile.get("key", "")

# Candidate name from profile info
info = profile.get("info", {})
raw_full = info.get("full_name", "")
first_name = info.get("first_name", "")
last_name = info.get("last_name", "")
# Prefer building from first+last (more reliable than HrFlow full_name which can be shuffled)
if first_name and last_name:
full_name = f"{first_name} {last_name}"
elif first_name or last_name:
full_name = (first_name or last_name).strip()
else:
full_name = raw_full

# Flatten skill names
skills: List[str] = [
Expand All @@ -67,6 +81,9 @@ async def parse_cv(

return {
"profile_key": profile_key,
"full_name": full_name,
"first_name": first_name,
"last_name": last_name,
"skills": skills,
"experiences": parsing.get("experiences", []),
"educations": parsing.get("educations", []),
Expand Down
37 changes: 37 additions & 0 deletions 2synthesize-recruitly/HrFlow/backend/services/jobs_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Jobs service — loads jobs from local JSON file.

No HrFlow API call needed. The jobs_fallback.json at the backend root
is the source of truth for autocomplete and skill hints.
"""

import json
import os
from typing import Any, Dict, List, Optional

_JOBS_FILE = os.path.join(os.path.dirname(__file__), "..", "jobs_fallback.json")

_jobs_cache: List[Dict[str, Any]] = []


def load_jobs_from_file() -> None:
"""Load jobs from local JSON file into memory."""
global _jobs_cache
try:
with open(_JOBS_FILE, "r", encoding="utf-8") as f:
_jobs_cache = json.load(f)
print(f"[jobs_service] Loaded {len(_jobs_cache)} jobs from local file.")
except Exception as exc:
print(f"[jobs_service] Could not load jobs_fallback.json: {exc}")
_jobs_cache = []


def get_jobs() -> List[Dict[str, Any]]:
return _jobs_cache


def get_job_by_key(job_key: str) -> Optional[Dict[str, Any]]:
for job in _jobs_cache:
if job["key"] == job_key:
return job
return None
Loading
Loading