Spaces:
Running
Running
| import os | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1" | |
| # ββ Candidate contact ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Resume-header address line. ATS checkers (Jobalytics / Resume Worded) flag a | |
| # missing address; a "City, State, Country" line satisfies that check. Used as | |
| # the fallback when the uploaded resume has no parseable location. | |
| CONTACT_LOCATION = os.getenv( | |
| "CONTACT_LOCATION", "Hyderabad, Telangana, India Β· Open to relocate" | |
| ) | |
| # ββ Maximum ATS Mode (User-Confirmed Skill Expansion) ββββββββββββββββββββββββ | |
| # The uploaded resume is a BASE PROFILE, not the candidate's full experience. | |
| # When enabled, normal Product/Product-Manager/AI/SaaS/B2B/agile vocabulary is | |
| # treated as user-confirmed / interview-supportable and aggressively included | |
| # across Summary, Skills, Key Achievements and Experience to maximise external | |
| # ATS keyword coverage. This NEVER loosens the hard anti-fake boundaries: | |
| # degrees, certifications/licenses, employers, titles, years/seniority and | |
| # regulated credentials stay blocked; specialized hands-on eng/security tools | |
| # still require explicit confirmation. | |
| MAXIMUM_ATS = { | |
| "enabled_default": False, # extension toggle controls it per-request | |
| "target_external_score": 95, # aim for 95β100 where honestly achievable | |
| "min_external_score": 90, # below this (for in-family roles) = failure state | |
| "max_repair_iterations": 4, # external-feedback repair iterations | |
| } | |
| # Normal PM / Product / AI / SaaS / B2B / agile terminology that, in Maximum ATS | |
| # Mode, is treated as user-confirmed (LOW risk, always includable) for the | |
| # candidate's target role family. These are craft/role terms β NOT credentials, | |
| # employers, titles, or specialized hands-on engineering tools. | |
| MAXIMUM_ATS_SAFE_TERMS = { | |
| # AI / ML product craft (not hands-on model engineering) | |
| "ai", "ml", "machine learning", "generative ai", "gen ai", "genai", "llm", | |
| "llms", "large language models", "prompt design", "prompt engineering", | |
| "cloud ai", "cloud ai service", "cloud ai services", "ai testing", | |
| "ai monitoring", "model evaluation", "ai evaluation", "ai strategy", | |
| "ai product", "ai product management", "conversational ai", "nlp", | |
| "responsible ai", "ai governance", "ai roadmap", | |
| # Analytics / experimentation | |
| "qa", "quality assurance", "experimentation", "a/b testing", "ab testing", | |
| "data-driven", "data driven", "analytics", "product analytics", "metrics", | |
| "kpis", "kpi", "okrs", "okr", "sql", "dashboards", "data analysis", | |
| "hypothesis testing", "funnel analysis", | |
| # Core product management | |
| "roadmap", "product roadmap", "product strategy", "product ownership", | |
| "product owner", "product discovery", "product vision", "product lifecycle", | |
| "go-to-market", "gtm", "prioritization", "feature prioritization", | |
| "requirements", "prd", "prds", "user stories", "acceptance criteria", | |
| "stakeholder management", "stakeholder communication", | |
| "cross-functional collaboration", "cross-functional", "backlog", | |
| "backlog grooming", "user research", "customer discovery", "market research", | |
| "competitive analysis", "wireframing", "user acceptance testing", | |
| # Agile / delivery | |
| "agile", "scrum", "kanban", "lean", "sprint planning", "sprint", | |
| "agile methodologies", "scrum master", "story mapping", | |
| # Business / domain | |
| "saas", "b2b", "b2c", "enterprise", "enterprise platform", | |
| "startup environment", "startup", "customer value", "adoption", | |
| "retention", "growth", "engagement", "conversion", "monetization", | |
| "revenue", "customer success", "onboarding", | |
| # Cross-functional PM responsibilities & leadership craft (interview-supportable) | |
| "influence", "influencing", "program management", "product management", | |
| "product marketing", "business development", "global teams", | |
| "diverse partners", "customer results", "customer outcomes", "technology", | |
| "data-driven decisions", "data driven decisions", "decision making", | |
| "strategic planning", "execution", "delivery", "collaboration", | |
| "communication", "leadership", "mentoring", "negotiation", "presentation", | |
| "stakeholders", "roadmaps", "feature definition", "release management", | |
| "product launch", "market research", "voice of customer", | |
| } | |
| # Primary key β GLM 5.1 (most reliable, used for resume parsing + customization) | |
| NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY") | |
| GLM_BASE_URL = NVIDIA_BASE_URL | |
| GLM_MODEL = "z-ai/glm-5.1" | |
| # Centralized V1 model fallback chain (src/nim_fallback.py health-checks these in | |
| # order and selects the first healthy one). Edit here to change live V1 models. | |
| # As of 2026-08: glm-5.2 times out, mistral-small-4 is 410 EOL, nemotron is healthy. | |
| V1_MODEL_CHAIN = [ | |
| "nvidia/nemotron-3-super-120b-a12b", # verified healthy β placed first to avoid | |
| "z-ai/glm-5.2", # the ~60s probe cost of a hung model | |
| "mistralai/mistral-small-4-119b-2603", | |
| ] | |
| # ββ 10 assessment models ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # phase1 = used for quick keyword scoring (instant, no LLM needed now) | |
| # phase2 = used for detailed LLM assessment of top jobs | |
| ASSESSMENT_MODELS = [ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Phase 2 (detailed LLM assessment) speed results from test run: | |
| # Kimi-K2.6 ~5s/batch FAST β phase2=True | |
| # Step-3.7-Flash ~8-35s FAST β phase2=True | |
| # Qwen3.5-397b ~9s FAST β phase2=True | |
| # Qwen3.5-122b-v2 ~12s FAST β phase2=True | |
| # GPT-OSS-120b ~11s FAST β phase2=True | |
| # Qwen3.5-122b ~40s OK β phase2=True | |
| # DeepSeek-v4-Pro ~42s OK β phase2=True | |
| # DeepSeek-v4-Flash ~229s SLOW β οΈ phase2=False (too slow) | |
| # GLM-5.1 ~234s SLOW β οΈ phase2=False (too slow) | |
| # MiniMax-M2.7 ~908s BLOCKEDβ phase2=False | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| { | |
| "name": "Kimi-K2.6", | |
| "model": "moonshotai/kimi-k2.6", | |
| "api_key": os.getenv("NVIDIA_API_KEY_3"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~5s per batch FAST | |
| "tailor": True, # capable tier β strong at structured JSON rewrites | |
| }, | |
| { | |
| "name": "Step-3.7-Flash", | |
| "model": "stepfun-ai/step-3.7-flash", | |
| "api_key": os.getenv("NVIDIA_API_KEY_8"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~8-35s FAST | |
| "tailor": False, # weaker β drops roles / sparse bullets; not for tailoring | |
| }, | |
| { | |
| "name": "Qwen3.5-397b", | |
| "model": "qwen/qwen3.5-397b-a17b", | |
| "api_key": os.getenv("NVIDIA_API_KEY_7"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~9s FAST | |
| "tailor": True, # capable tier β large model, good instruction following | |
| }, | |
| { | |
| "name": "Qwen3.5-122b-v2", | |
| "model": "qwen/qwen3.5-122b-a10b", | |
| "api_key": os.getenv("NVIDIA_API_KEY_7"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~12s FAST | |
| "tailor": False, # smaller β keep for assessment, not tailoring | |
| }, | |
| { | |
| "name": "GPT-OSS-120b", | |
| "model": "openai/gpt-oss-120b", | |
| "api_key": os.getenv("NVIDIA_API_KEY_5"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~11s FAST | |
| "tailor": True, # capable tier | |
| }, | |
| { | |
| "name": "Qwen3.5-122b", | |
| "model": "qwen/qwen3.5-122b-a10b", | |
| "api_key": os.getenv("NVIDIA_API_KEY_4"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": True, # ~40s OK | |
| "tailor": False, # smaller | |
| }, | |
| { | |
| "name": "DeepSeek-v4-Pro", | |
| "model": "deepseek-ai/deepseek-v4-pro", | |
| "api_key": os.getenv("NVIDIA_API_KEY_2"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {"chat_template_kwargs": {"thinking": False}}, | |
| "phase1": True, | |
| "phase2": True, # ~42s OK | |
| "tailor": True, # capable tier β strong reasoning | |
| }, | |
| { | |
| "name": "DeepSeek-v4-Flash", | |
| "model": "deepseek-ai/deepseek-v4-flash", | |
| "api_key": os.getenv("NVIDIA_API_KEY_6"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {"chat_template_kwargs": {"thinking": True, "reasoning_effort": "high"}}, | |
| "phase1": True, | |
| "phase2": False, # ~229s too slow for phase2 | |
| }, | |
| { | |
| "name": "GLM-5.1", | |
| "model": "z-ai/glm-5.1", | |
| "api_key": os.getenv("NVIDIA_API_KEY"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": False, # ~234s too slow for phase2; used for resume customization | |
| }, | |
| { | |
| "name": "MiniMax-M2.7", | |
| "model": "minimaxai/minimax-m2.7", | |
| "api_key": os.getenv("NVIDIA_API_KEY_2"), | |
| "base_url": NVIDIA_BASE_URL, | |
| "extra_body": {}, | |
| "phase1": True, | |
| "phase2": False, # ~908s BLOCKED | |
| }, | |
| ] | |
| # ββ Google Sheets / Drive ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GOOGLE = { | |
| "sheet_id": os.getenv("GOOGLE_SHEET_ID", "1Ehxt3eortehbtySdtgcSrMhCqmxIMUAmvRqSkII0HJk"), | |
| "sheet_tab": "Job Applications", | |
| "drive_folder": "Job Automation Agent β Resumes", | |
| "credentials_file": "google_credentials.json", # Service account JSON | |
| "token_file": "google_token.json", # OAuth token (auto-created) | |
| } | |
| # ββ Job Search β Product Manager roles ONLY βββββββββββββββββββββββββββββββ | |
| JOB_SEARCH = { | |
| "roles": [ | |
| "Product Manager", | |
| "Senior Product Manager", | |
| "AI Product Manager", | |
| "Technical Product Manager", | |
| "Product Owner", | |
| ], | |
| # Required words in job title β scraper DROPS any job without these | |
| "required_title_words": ["product"], | |
| # Blocked words in title β scraper DROPS any job with these | |
| "blocked_title_words": [ | |
| "teacher", "faculty", "engineer", "developer", "designer", | |
| "accountant", "analyst", "consultant", "manager data", | |
| "project manager", "program manager", "marketing manager", | |
| "sales", "hr ", "recruiter", "finance", "legal", | |
| "civil", "mechanical", "electrical", "hardware", "network", | |
| "software engineer", "data engineer", "ml engineer", | |
| ], | |
| "locations": ["India", "Bangalore", "Mumbai", "Delhi NCR", "Hyderabad", "Pune", "Remote"], | |
| "experience_min": 2, | |
| "experience_max": 15, | |
| "max_jobs_per_platform": 30, | |
| "days_posted": 7, | |
| "primary_role": "Product Manager", | |
| } | |
| # Platforms (Naukri blocked by Akamai) | |
| PLATFORMS = { | |
| "linkedin": True, | |
| "naukri": False, | |
| "indeed": True, | |
| "glassdoor": True, | |
| } | |
| # Assessment β process ALL PM jobs | |
| ASSESSMENT = { | |
| "min_score_for_llm_resume": 6, # LLM-tailored resume for score >= this | |
| "generate_all_resumes": True, # Template resume for ALL PM-relevant jobs | |
| "max_llm_resumes": 30, # Max LLM-tailored resumes per run | |
| "process_all_jobs": True, # Assess ALL scraped jobs (not just top N) | |
| "dedup_days": 30, # Skip jobs seen in last N days (0 = no dedup) | |
| "test_mode": False, # Set True for quick test run | |
| "test_jobs_limit": 10, # Max jobs in test mode | |
| } | |
| # ββ Production LLM generation (provider abstraction + fallback chain) ββββββββββ | |
| # The pipeline is model-independent: any provider may generate content, but | |
| # deterministic validation (schema + independent score + parse) decides if it | |
| # passes. Providers are tried in order until one yields a READY (>=90) resume. | |
| LLM_GENERATION = { | |
| "provider_order": ["kimi", "nvidia_primary", "deterministic"], | |
| "temperature": 0.1, | |
| "max_retries": 2, | |
| "require_json_schema": True, | |
| "fallback_on_schema_error": True, | |
| "fallback_on_low_independent_score": True, | |
| "fallback_on_parse_failure": True, | |
| } | |
| # ββ Resume automation policy (AUTO_AGGRESSIVE) βββββββββββββββββββββββββββββββββ | |
| # Controls how aggressively plausible JD terms are auto-included and which | |
| # resumes are downloadable, so a 100-job batch runs mostly hands-off. | |
| AUTOMATION = { | |
| "automation_mode": "auto_aggressive", | |
| # What to do per risk severity (see candidate_fit.severity): | |
| "review_policy": { | |
| "LOW_RISK_AUTO_INCLUDED": "auto_include", | |
| "MEDIUM_RISK_REVIEW_RECOMMENDED": "auto_include_with_review_flag", | |
| "HIGH_RISK_NEEDS_CONFIRMATION": "ask_user", | |
| "BLOCKED_DO_NOT_INCLUDE": "exclude", | |
| }, | |
| # Which statuses are downloadable: | |
| "download_policy": { | |
| "READY_90_PLUS": "allow", | |
| "READY_90_PLUS_REVIEW_RECOMMENDED": "allow_with_warning", | |
| "WEAK_90_INTERNAL_ONLY": "block", | |
| "NEEDS_REPAIR": "block", | |
| "NEEDS_USER_INPUT": "block", | |
| "NOT_ELIGIBLE_LOW_FIT": "block", | |
| "PARSE_FAILED": "block", | |
| }, | |
| } | |
| # ββ ever-jobs integration βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from src.ever_jobs_bridge.platforms import INDIA_DEFAULT_PLATFORMS | |
| EVER_JOBS = { | |
| # Point this at a HOSTED ever-jobs sidecar to enable the 150+ extra | |
| # platforms on HF Spaces (where a local Node process can't run). Defaults | |
| # to the local dev sidecar. | |
| "api_url": os.getenv("EVER_JOBS_API_URL", "http://localhost:3001"), | |
| "default_platforms": INDIA_DEFAULT_PLATFORMS, | |
| "max_results": 25, | |
| "request_timeout": 30, | |
| "http_timeout": 120, | |
| "max_platforms_per_call": 20, | |
| } | |
| # ββ Scraper stealth + anti-block (Scrapling) βββββββββββββββββββββββββββββββββ | |
| # The fetch layer (src/scrapers/fetch.py) uses Scrapling for real-Chrome TLS | |
| # impersonation and Cloudflare bypass, falling back to plain requests. These | |
| # settings are read mostly via env so they can be set as HF Space secrets. | |
| SCRAPER = { | |
| # Browser TLS fingerprint to impersonate for HTTP fetches. | |
| "impersonate": os.getenv("SCRAPER_IMPERSONATE", "chrome"), | |
| "timeout": int(os.getenv("SCRAPER_TIMEOUT", "20")), | |
| # Comma/newline-separated proxy URLs (http://user:pass@host:port). The ONLY | |
| # real fix for datacenter-IP blocking (e.g. LinkedIn/Indeed on HF). Rotated | |
| # round-robin. Empty = no proxy (best-effort fingerprint stealth only). | |
| "proxies": os.getenv("SCRAPER_PROXIES", "") or os.getenv("SCRAPER_PROXY", ""), | |
| # Use the Camoufox stealth browser for Cloudflare-protected sites. | |
| "use_browser_stealth": os.getenv("SCRAPER_BROWSER_STEALTH", "1") not in ("0", "false", "False"), | |
| } | |
| # Direct-company ATS boards (Greenhouse/Lever/Ashby public JSON APIs). These are | |
| # the most block-resistant bulk source on a shared cloud IP. Override/extend via | |
| # the COMPANY_ATS_BOARDS env var (JSON list) without touching code. If empty, the | |
| # scraper uses its built-in seed list (src/scrapers/company_ats.DEFAULT_BOARDS). | |
| def _load_company_ats_boards(): | |
| raw = os.getenv("COMPANY_ATS_BOARDS", "").strip() | |
| if not raw: | |
| return [] | |
| try: | |
| import json as _json | |
| boards = _json.loads(raw) | |
| return boards if isinstance(boards, list) else [] | |
| except Exception: | |
| return [] | |
| COMPANY_ATS_BOARDS = _load_company_ats_boards() | |
| # Output | |
| OUTPUT = { | |
| "excel_path": "data/output/reports/job_report.xlsx", | |
| "resumes_dir": "data/output/resumes/", | |
| "report_dir": "data/output/reports/", | |
| } | |
| # Resume | |
| RESUME = { | |
| "pdf_path": os.getenv("RESUME_PATH", "data/resume/resume.pdf"), | |
| } | |