Upload folder using huggingface_hub
Browse files- .gradio/certificate.pem +31 -0
- .vercel/README.txt +11 -0
- .vercel/project.json +1 -0
- README.md +16 -16
- agent_brain.py +129 -0
- app.py +242 -140
- call_agent.py +85 -0
- deepgram_tts.py +46 -0
- langgraph_agent.py +169 -146
- nvidia_ocr.py +152 -104
- orchestrator.py +155 -0
- pipeline_engine.py +263 -0
- rag_engine.py +70 -70
- requirements.txt +9 -8
- test_audio.mp3 +0 -0
- utils.py +100 -61
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
.vercel/README.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
> Why do I have a folder named ".vercel" in my project?
|
| 2 |
+
The ".vercel" folder is created when you link a directory to a Vercel project.
|
| 3 |
+
|
| 4 |
+
> What does the "project.json" file contain?
|
| 5 |
+
The "project.json" file contains:
|
| 6 |
+
- The ID of the Vercel project that you linked ("projectId")
|
| 7 |
+
- The ID of the user or team your Vercel project is owned by ("orgId")
|
| 8 |
+
|
| 9 |
+
> Should I commit the ".vercel" folder?
|
| 10 |
+
No, you should not share the ".vercel" folder with anyone.
|
| 11 |
+
Upon creation, it will be automatically added to your ".gitignore" file.
|
.vercel/project.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"projectId":"prj_wJm9O84sruowjNqTPWeotnscTSzm","orgId":"team_AhmnrQylOHZrxQHMUNoHjloE","projectName":"project"}
|
README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: AI Resume Analyzer RAG LangGraph
|
| 3 |
-
emoji: 📄
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: gradio
|
| 7 |
-
app_file: app.py
|
| 8 |
-
pinned: false
|
| 9 |
-
license: mit
|
| 10 |
-
short_description: AI Resume Analyzer (RAG + LangGraph) by abersabil
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
# 📄 AI Resume Analyzer (RAG + LangGraph)
|
| 14 |
-
Created & Maintained by **abersabil** (`@abersbail`) | User ID: `69b2ede7cec72416131a3260`
|
| 15 |
-
|
| 16 |
-
Powered by **NVIDIA Nemotron OCR v2/v1**, **RAG Vector Search**, & **Groq LLM ATS Auditor**.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Resume Analyzer RAG LangGraph
|
| 3 |
+
emoji: 📄
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: AI Resume Analyzer (RAG + LangGraph) by abersabil
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# 📄 AI Resume Analyzer (RAG + LangGraph)
|
| 14 |
+
Created & Maintained by **abersabil** (`@abersbail`) | User ID: `69b2ede7cec72416131a3260`
|
| 15 |
+
|
| 16 |
+
Powered by **NVIDIA Nemotron OCR v2/v1**, **RAG Vector Search**, & **Groq LLM ATS Auditor**.
|
agent_brain.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from groq import Groq
|
| 5 |
+
|
| 6 |
+
class NagaMLOpsAgent:
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
api_key: str = "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",
|
| 10 |
+
model: str = "llama-3.3-70b-versatile"
|
| 11 |
+
):
|
| 12 |
+
self.api_key = api_key
|
| 13 |
+
self.primary_model = model
|
| 14 |
+
self.fallback_models = [
|
| 15 |
+
"llama-3.3-70b-versatile",
|
| 16 |
+
"llama-3.1-8b-instant",
|
| 17 |
+
"deepseek-r1-distill-llama-70b",
|
| 18 |
+
"mixtral-8x7b-32768"
|
| 19 |
+
]
|
| 20 |
+
self.client = Groq(api_key=self.api_key)
|
| 21 |
+
|
| 22 |
+
def diagnose_and_heal(
|
| 23 |
+
self,
|
| 24 |
+
script_code: str,
|
| 25 |
+
execution_logs: str,
|
| 26 |
+
telemetry: dict,
|
| 27 |
+
fault_name: str = None
|
| 28 |
+
) -> dict:
|
| 29 |
+
"""
|
| 30 |
+
Sends error logs, broken python code, and telemetry context to Groq ultra-fast API.
|
| 31 |
+
Returns a structured dictionary containing root cause diagnosis and executable patched code.
|
| 32 |
+
"""
|
| 33 |
+
system_prompt = (
|
| 34 |
+
"You are an expert Autonomous MLOps & AI Infrastructure Diagnostic Agent.\n"
|
| 35 |
+
"Your task is to analyze failing machine learning pipelines, identify root causes, "
|
| 36 |
+
"and generate production-grade, fully working Python code patches to fix the issue.\n\n"
|
| 37 |
+
"CRITICAL INSTRUCTION: You MUST return your answer in valid JSON format matching this EXACT schema:\n"
|
| 38 |
+
"{\n"
|
| 39 |
+
' "fault_category": "DATA_DRIFT | CODE_RUNTIME_ERROR | NAN_LOSS | OOM_SPIKE | MODEL_ACCURACY_DROP",\n'
|
| 40 |
+
' "severity": "CRITICAL | HIGH | MEDIUM",\n'
|
| 41 |
+
' "root_cause_analysis": "Detailed explanation of why the crash/degradation happened.",\n'
|
| 42 |
+
' "explanation_for_engineers": "Actionable summary for MLOps dashboard.",\n'
|
| 43 |
+
' "patch_code": "FULL valid Python script replacing the broken code completely without placeholder comments.",\n'
|
| 44 |
+
' "verification_checklist": ["Check 1", "Check 2"]\n'
|
| 45 |
+
"}"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
user_content = (
|
| 49 |
+
f"=== REPORTED FAULT SCENARIO ===\n{fault_name or 'Auto-Detected Anomaly'}\n\n"
|
| 50 |
+
f"=== PIPELINE TELEMETRY ===\n{json.dumps(telemetry, indent=2)}\n\n"
|
| 51 |
+
f"=== BROKEN PIPELINE CODE ===\n```python\n{script_code}\n```\n\n"
|
| 52 |
+
f"=== EXECUTION LOGS & STACKTRACE ===\n{execution_logs}\n\n"
|
| 53 |
+
"Perform root cause analysis and produce the fixed `patch_code` Python script. "
|
| 54 |
+
"Ensure the patch is self-contained, syntax-correct, and completely resolves the error."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
response_text = ""
|
| 58 |
+
last_error = None
|
| 59 |
+
|
| 60 |
+
models_to_try = [self.primary_model] + [m for m in self.fallback_models if m != self.primary_model]
|
| 61 |
+
|
| 62 |
+
for m in models_to_try:
|
| 63 |
+
try:
|
| 64 |
+
completion = self.client.chat.completions.create(
|
| 65 |
+
model=m,
|
| 66 |
+
messages=[
|
| 67 |
+
{"role": "system", "content": system_prompt},
|
| 68 |
+
{"role": "user", "content": user_content}
|
| 69 |
+
],
|
| 70 |
+
temperature=0.1,
|
| 71 |
+
response_format={"type": "json_object"}
|
| 72 |
+
)
|
| 73 |
+
response_text = completion.choices[0].message.content.strip()
|
| 74 |
+
if response_text:
|
| 75 |
+
print(f"[AgentBrain] Super-fast Groq diagnosis generated using model: {m}")
|
| 76 |
+
break
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"[AgentBrain] Model {m} failed: {e}. Trying next fallback...")
|
| 79 |
+
last_error = e
|
| 80 |
+
|
| 81 |
+
if not response_text:
|
| 82 |
+
return self._generate_fallback_diagnosis(script_code, str(last_error))
|
| 83 |
+
|
| 84 |
+
return self._parse_agent_response(response_text, script_code)
|
| 85 |
+
|
| 86 |
+
def _parse_agent_response(self, raw_text: str, original_code: str) -> dict:
|
| 87 |
+
clean_text = raw_text
|
| 88 |
+
if "```json" in clean_text:
|
| 89 |
+
clean_text = clean_text.split("```json")[1].split("```")[0].strip()
|
| 90 |
+
elif "```" in clean_text and clean_text.strip().startswith("```"):
|
| 91 |
+
clean_text = clean_text.split("```")[1].split("```")[0].strip()
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
parsed = json.loads(clean_text)
|
| 95 |
+
patch = parsed.get("patch_code", original_code)
|
| 96 |
+
if isinstance(patch, dict):
|
| 97 |
+
patch = patch.get("code", patch.get("script", str(patch)))
|
| 98 |
+
elif not isinstance(patch, str):
|
| 99 |
+
patch = str(patch)
|
| 100 |
+
|
| 101 |
+
if "```python" in patch:
|
| 102 |
+
patch = patch.split("```python")[1].split("```")[0].strip()
|
| 103 |
+
elif "```" in patch:
|
| 104 |
+
patch = patch.split("```")[1].split("```")[0].strip()
|
| 105 |
+
parsed["patch_code"] = str(patch)
|
| 106 |
+
return parsed
|
| 107 |
+
except Exception as json_err:
|
| 108 |
+
print(f"[AgentBrain] JSON parsing failed: {json_err}. Extracting code via regex fallback...")
|
| 109 |
+
code_match = re.search(r"```python(.*?)```", raw_text, re.DOTALL)
|
| 110 |
+
patch_code = code_match.group(1).strip() if code_match else original_code
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"fault_category": "CODE_RUNTIME_ERROR",
|
| 114 |
+
"severity": "HIGH",
|
| 115 |
+
"root_cause_analysis": raw_text[:300] + "...",
|
| 116 |
+
"explanation_for_engineers": "Agent generated fix. Extracted code patch successfully.",
|
| 117 |
+
"patch_code": patch_code,
|
| 118 |
+
"verification_checklist": ["Execute patched script", "Verify pipeline telemetry"]
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
def _generate_fallback_diagnosis(self, original_code: str, error_msg: str) -> dict:
|
| 122 |
+
return {
|
| 123 |
+
"fault_category": "CODE_RUNTIME_ERROR",
|
| 124 |
+
"severity": "HIGH",
|
| 125 |
+
"root_cause_analysis": f"Local Fallback Diagnosis: Pipeline exception detected ({error_msg}).",
|
| 126 |
+
"explanation_for_engineers": "Network issue reaching LLM API. Initiated local safety patch.",
|
| 127 |
+
"patch_code": original_code.replace("/ 0", "/ 1.0").replace("np.nan", "0.0"),
|
| 128 |
+
"verification_checklist": ["Local syntax check", "Re-run safety sandbox"]
|
| 129 |
+
}
|
app.py
CHANGED
|
@@ -1,140 +1,242 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import json
|
| 3 |
-
import pandas as pd
|
| 4 |
-
import gradio as gr
|
| 5 |
-
from langgraph_agent import LangGraphResumeAnalyzer
|
| 6 |
-
import utils
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
""
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import gradio as gr
|
| 5 |
+
from langgraph_agent import LangGraphResumeAnalyzer
|
| 6 |
+
import utils
|
| 7 |
+
|
| 8 |
+
# Instantiate LangGraph Agent
|
| 9 |
+
analyzer = LangGraphResumeAnalyzer()
|
| 10 |
+
|
| 11 |
+
# FIXED CSS preventing vibrating / trembling layout bug by locking vertical scrollbar gutter permanently
|
| 12 |
+
CUSTOM_CSS = """
|
| 13 |
+
html {
|
| 14 |
+
overflow-y: scroll !important;
|
| 15 |
+
scroll-behavior: smooth !important;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
background-color: #090d16 !important;
|
| 20 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
|
| 21 |
+
color: #e2e8f0 !important;
|
| 22 |
+
margin: 0 !important;
|
| 23 |
+
padding: 0 !important;
|
| 24 |
+
min-height: 100vh !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.gradio-container {
|
| 28 |
+
background-color: #090d16 !important;
|
| 29 |
+
max-width: 1400px !important;
|
| 30 |
+
margin: 0 auto !important;
|
| 31 |
+
padding: 20px !important;
|
| 32 |
+
width: 100% !important;
|
| 33 |
+
box-sizing: border-box !important;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.card-panel {
|
| 37 |
+
background: linear-gradient(145deg, #131b2e, #0f172a);
|
| 38 |
+
border: 1px solid rgba(56, 189, 248, 0.2);
|
| 39 |
+
border-radius: 16px;
|
| 40 |
+
padding: 20px;
|
| 41 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
| 42 |
+
height: auto !important;
|
| 43 |
+
contain: content;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.btn-primary-audit {
|
| 47 |
+
background: linear-gradient(135deg, #0284c7, #4f46e5) !important;
|
| 48 |
+
color: white !important;
|
| 49 |
+
font-weight: 800 !important;
|
| 50 |
+
border-radius: 12px !important;
|
| 51 |
+
font-size: 1.1rem !important;
|
| 52 |
+
box-shadow: 0 4px 14px rgba(2, 132, 199, 0.4) !important;
|
| 53 |
+
}
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def handle_run_audit(file_obj, text_resume, job_desc):
|
| 57 |
+
"""
|
| 58 |
+
Executes LangGraph pipeline for PDF documents or images via NVIDIA Nemotron OCR & PyPDF.
|
| 59 |
+
"""
|
| 60 |
+
res = analyzer.run_langgraph_pipeline(
|
| 61 |
+
file_input=file_obj,
|
| 62 |
+
text_input=text_resume,
|
| 63 |
+
job_description=job_desc
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
analysis = res["analysis"]
|
| 67 |
+
ocr_info = res["ocr_result"]
|
| 68 |
+
|
| 69 |
+
overall_score = analysis.get("overall_ats_score_pct", 85)
|
| 70 |
+
ats_gauge_html = utils.generate_ats_score_html(overall_score)
|
| 71 |
+
|
| 72 |
+
subscores_html = utils.generate_subscores_html(
|
| 73 |
+
keyword_pct=analysis.get("keyword_match_pct", 82),
|
| 74 |
+
skills_pct=analysis.get("skills_match_pct", 88),
|
| 75 |
+
experience_pct=analysis.get("experience_fit_pct", 85),
|
| 76 |
+
format_pct=analysis.get("format_quality_pct", 90)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
skill_badges_html = utils.format_skill_badges(
|
| 80 |
+
analysis.get("matched_skills", []),
|
| 81 |
+
analysis.get("missing_skills", [])
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
contact = analysis.get("contact_info", {})
|
| 85 |
+
summary_md = f"""
|
| 86 |
+
### 👤 Candidate Profile & Key Info
|
| 87 |
+
- **Full Name**: `{analysis.get('candidate_name', 'Alex Chen')}`
|
| 88 |
+
- **Estimated Experience**: `{analysis.get('estimated_years_experience', '6+ Years')}`
|
| 89 |
+
- **Email**: `{contact.get('email', 'N/A')}` | **Location**: `{contact.get('location', 'N/A')}`
|
| 90 |
+
|
| 91 |
+
#### 📝 Executive Recruiter Assessment
|
| 92 |
+
{analysis.get('executive_summary', 'No summary available.')}
|
| 93 |
+
|
| 94 |
+
#### 🎯 Key Candidate Strengths
|
| 95 |
+
"""
|
| 96 |
+
for strg in analysis.get("key_strengths", []):
|
| 97 |
+
summary_md += f"- **{strg}**\n"
|
| 98 |
+
|
| 99 |
+
summary_md += "\n#### 💡 Actionable Recommendations to Boost ATS Score to 98%+\n"
|
| 100 |
+
for tip in analysis.get("improvement_tips", []):
|
| 101 |
+
summary_md += f"- {tip}\n"
|
| 102 |
+
|
| 103 |
+
# AI Tailored Resume Rewriter Bullets
|
| 104 |
+
bullets_md = "### ✍️ AI Tailored Resume Bullet Point Rewriter\n*Copy & paste these optimized bullets into your resume to maximize ATS callback rates:*\n\n"
|
| 105 |
+
for idx, bullet in enumerate(analysis.get("optimized_resume_bullets", [])):
|
| 106 |
+
bullets_md += f"**{idx+1}.** `{bullet}`\n\n"
|
| 107 |
+
|
| 108 |
+
# OCR Info
|
| 109 |
+
ocr_meta_md = f"""
|
| 110 |
+
### 👁️ NVIDIA Nemotron OCR & PDF Detection Engine
|
| 111 |
+
- **Engine Used**: `{ocr_info.get('model_used', 'NVIDIA Nemotron OCR v2')}`
|
| 112 |
+
- **Total Lines Extracted**: `{ocr_info.get('line_count', 0)}`
|
| 113 |
+
- **Extraction Status**: `{ocr_info.get('status', 'SUCCESS')}`
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
detections_list = ocr_info.get("detections", [])
|
| 117 |
+
df_det = pd.DataFrame(detections_list) if detections_list else pd.DataFrame(columns=["text", "confidence"])
|
| 118 |
+
|
| 119 |
+
full_report_json = json.dumps({
|
| 120 |
+
"candidate": analysis.get('candidate_name'),
|
| 121 |
+
"overall_ats_score_pct": overall_score,
|
| 122 |
+
"subscores": {
|
| 123 |
+
"keywords": analysis.get("keyword_match_pct"),
|
| 124 |
+
"skills": analysis.get("skills_match_pct"),
|
| 125 |
+
"experience": analysis.get("experience_fit_pct"),
|
| 126 |
+
"formatting": analysis.get("format_quality_pct")
|
| 127 |
+
},
|
| 128 |
+
"ocr_engine": ocr_info.get('model_used'),
|
| 129 |
+
"analysis": analysis
|
| 130 |
+
}, indent=2)
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
ats_gauge_html,
|
| 134 |
+
subscores_html,
|
| 135 |
+
skill_badges_html,
|
| 136 |
+
summary_md,
|
| 137 |
+
bullets_md,
|
| 138 |
+
res["timeline"],
|
| 139 |
+
ocr_meta_md,
|
| 140 |
+
df_det,
|
| 141 |
+
res["resume_text"],
|
| 142 |
+
full_report_json
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def handle_rag_chat(user_question: str):
|
| 146 |
+
return analyzer.answer_rag_question(user_question)
|
| 147 |
+
|
| 148 |
+
with gr.Blocks(title="AI Resume Analyzer (RAG + LangGraph)") as demo:
|
| 149 |
+
gr.Markdown(
|
| 150 |
+
"""
|
| 151 |
+
# 📄 Advanced AI Resume Analyzer (RAG + LangGraph)
|
| 152 |
+
### Multimodal PDF & Image OCR via NVIDIA Nemotron OCR v2/v1 & Groq LLM ATS Auditor
|
| 153 |
+
**Engineer / Creator:** `abersabil` (`@abersbail`) | **User ID:** `69b2ede7cec72416131a3260`
|
| 154 |
+
"""
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
with gr.Tabs():
|
| 158 |
+
# TAB 1: LangGraph ATS Audit & Matching
|
| 159 |
+
with gr.TabItem("📊 LangGraph ATS Audit & Matching"):
|
| 160 |
+
with gr.Row():
|
| 161 |
+
with gr.Column(scale=1, elem_classes=["card-panel"]):
|
| 162 |
+
gr.Markdown("### 📥 Upload PDF / Image Resume & Job Description")
|
| 163 |
+
|
| 164 |
+
resume_file_input = gr.File(
|
| 165 |
+
label="Upload Resume File (.pdf, .png, .jpg, .jpeg, .webp)",
|
| 166 |
+
file_types=[".pdf", ".png", ".jpg", ".jpeg", ".webp"]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
resume_text_area = gr.Textbox(
|
| 170 |
+
value=utils.DEFAULT_SAMPLE_RESUME,
|
| 171 |
+
label="OR Paste Resume Text directly",
|
| 172 |
+
lines=7
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
job_desc_area = gr.Textbox(
|
| 176 |
+
value=utils.DEFAULT_JOB_DESCRIPTION,
|
| 177 |
+
label="Target Job Description (JD)",
|
| 178 |
+
lines=5
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
btn_audit = gr.Button("🚀 Run LangGraph RAG Audit", elem_classes=["btn-primary-audit"])
|
| 182 |
+
langgraph_timeline = gr.Textbox(label="LangGraph State Machine Stream", lines=6, interactive=False)
|
| 183 |
+
|
| 184 |
+
with gr.Column(scale=1):
|
| 185 |
+
ats_score_gauge = gr.HTML(utils.generate_ats_score_html(88))
|
| 186 |
+
ats_subscores_box = gr.HTML(utils.generate_subscores_html(85, 90, 85, 95))
|
| 187 |
+
skill_badges_box = gr.HTML("Click 'Run LangGraph RAG Audit' to analyze candidate skills.")
|
| 188 |
+
executive_summary_box = gr.Markdown("Candidate evaluation summary will appear here.")
|
| 189 |
+
tailored_bullets_box = gr.Markdown("Optimized resume bullets will appear here.")
|
| 190 |
+
|
| 191 |
+
# TAB 2: NVIDIA Nemotron OCR Scanner View
|
| 192 |
+
with gr.TabItem("👁️ NVIDIA Nemotron OCR Inspector"):
|
| 193 |
+
with gr.Row():
|
| 194 |
+
with gr.Column(scale=1):
|
| 195 |
+
ocr_metadata_box = gr.Markdown("Run audit to view NVIDIA Nemotron OCR v2/v1 detection metrics.")
|
| 196 |
+
ocr_detections_df = gr.Dataframe(label="Detected Lines & Confidence Scores")
|
| 197 |
+
with gr.Column(scale=1):
|
| 198 |
+
gr.Markdown("### 📄 Extracted Resume Raw Text")
|
| 199 |
+
ocr_raw_text_box = gr.Textbox(lines=18, interactive=False)
|
| 200 |
+
|
| 201 |
+
# TAB 3: LangGraph RAG Candidate Chatbot
|
| 202 |
+
with gr.TabItem("💬 Candidate RAG Chatbot"):
|
| 203 |
+
gr.Markdown("### 🤖 Ask RAG Questions About Candidate Qualifications")
|
| 204 |
+
with gr.Row():
|
| 205 |
+
with gr.Column(scale=1):
|
| 206 |
+
rag_question_input = gr.Textbox(
|
| 207 |
+
label="Type Question about Candidate",
|
| 208 |
+
placeholder="e.g., What PyTorch & RAG experience does the candidate have?",
|
| 209 |
+
lines=2
|
| 210 |
+
)
|
| 211 |
+
btn_ask_rag = gr.Button("🔍 Query RAG Vector Index", variant="primary")
|
| 212 |
+
with gr.Column(scale=1):
|
| 213 |
+
rag_answer_output = gr.Textbox(label="Grounded RAG Answer", lines=8, interactive=False)
|
| 214 |
+
|
| 215 |
+
# TAB 4: Full Audit Report Export
|
| 216 |
+
with gr.TabItem("📝 Candidate Report Export"):
|
| 217 |
+
gr.Markdown("### 📑 Downloadable Candidate Evaluation JSON Report")
|
| 218 |
+
full_report_code = gr.Code(language="json", label="JSON Candidate Audit Report")
|
| 219 |
+
|
| 220 |
+
# Event Bindings
|
| 221 |
+
btn_audit.click(
|
| 222 |
+
fn=handle_run_audit,
|
| 223 |
+
inputs=[resume_file_input, resume_text_area, job_desc_area],
|
| 224 |
+
outputs=[
|
| 225 |
+
ats_score_gauge, ats_subscores_box, skill_badges_box,
|
| 226 |
+
executive_summary_box, tailored_bullets_box, langgraph_timeline,
|
| 227 |
+
ocr_metadata_box, ocr_detections_df, ocr_raw_text_box, full_report_code
|
| 228 |
+
]
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
btn_ask_rag.click(
|
| 232 |
+
fn=handle_rag_chat,
|
| 233 |
+
inputs=[rag_question_input],
|
| 234 |
+
outputs=[rag_answer_output]
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
demo.queue()
|
| 239 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, css=CUSTOM_CSS)
|
| 240 |
+
"""
|
| 241 |
+
with open("app.py", "w", encoding="utf-8") as f:
|
| 242 |
+
f.write(CodeContent)
|
call_agent.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
from groq import Groq
|
| 4 |
+
import deepgram_tts
|
| 5 |
+
|
| 6 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "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")
|
| 7 |
+
|
| 8 |
+
PERSONA_PROMPTS = {
|
| 9 |
+
"Medical Telehealth Assistant": (
|
| 10 |
+
"You are Dr. Thalia, an empathetic and professional AI Telehealth Calling Assistant calling a patient. "
|
| 11 |
+
"Your goal is to communicate lab results, answer patient medical concerns clearly, and schedule follow-ups. "
|
| 12 |
+
"CRITICAL INSTRUCTIONS FOR VOICE SYNTHESIS:\n"
|
| 13 |
+
"1. Speak naturally as if on a live phone call.\n"
|
| 14 |
+
"2. Keep responses brief (1 to 3 short spoken sentences).\n"
|
| 15 |
+
"3. Do NOT use bullet points, markdown bold, lists, or symbols like # or *.\n"
|
| 16 |
+
"4. Spell out medical numbers clearly (e.g. 'two hundred forty milligrams per deciliter')."
|
| 17 |
+
),
|
| 18 |
+
"Customer Support Specialist": (
|
| 19 |
+
"You are Alex, a helpful AI Customer Service Calling Agent assisting a customer over the phone. "
|
| 20 |
+
"Keep your tone polite, natural, and conversational. Keep responses to 1-3 spoken sentences without markdown formatting."
|
| 21 |
+
),
|
| 22 |
+
"Outbound Sales & Qualification": (
|
| 23 |
+
"You are Jordan, a friendly AI Outbound Account Manager calling a potential business client. "
|
| 24 |
+
"Speak concisely, ask engaging follow-up questions, and maintain a warm phone persona."
|
| 25 |
+
),
|
| 26 |
+
"Custom Assistant": (
|
| 27 |
+
"You are an AI Voice Calling Agent on a live phone call. Speak naturally, warmly, and concisely."
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
class AICallingAgent:
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self.client = Groq(api_key=GROQ_API_KEY)
|
| 34 |
+
self.model = "llama-3.3-70b-versatile"
|
| 35 |
+
|
| 36 |
+
def process_call_turn(
|
| 37 |
+
self,
|
| 38 |
+
user_input: str,
|
| 39 |
+
conversation_history: list,
|
| 40 |
+
persona: str = "Medical Telehealth Assistant",
|
| 41 |
+
voice_model: str = "aura-2-thalia-en"
|
| 42 |
+
) -> dict:
|
| 43 |
+
"""
|
| 44 |
+
Processes a phone call conversational turn:
|
| 45 |
+
1. Generates conversational LLM text response via Groq API.
|
| 46 |
+
2. Synthesizes voice audio via Deepgram Aura-2 API.
|
| 47 |
+
Returns dictionary with text, audio file path, latency, and updated history.
|
| 48 |
+
"""
|
| 49 |
+
start_time = time.time()
|
| 50 |
+
|
| 51 |
+
system_prompt = PERSONA_PROMPTS.get(persona, PERSONA_PROMPTS["Medical Telehealth Assistant"])
|
| 52 |
+
|
| 53 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 54 |
+
for msg in conversation_history:
|
| 55 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 56 |
+
|
| 57 |
+
messages.append({"role": "user", "content": user_input})
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
completion = self.client.chat.completions.create(
|
| 61 |
+
model=self.model,
|
| 62 |
+
messages=messages,
|
| 63 |
+
temperature=0.7,
|
| 64 |
+
max_tokens=250
|
| 65 |
+
)
|
| 66 |
+
agent_text = completion.choices[0].message.content.strip()
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"[AICallingAgent] Groq API Error: {e}")
|
| 69 |
+
agent_text = "I apologize, I am experiencing a brief connection drop. Could you please repeat that?"
|
| 70 |
+
|
| 71 |
+
# Generate Voice Audio via Deepgram
|
| 72 |
+
audio_filepath = deepgram_tts.generate_voice_audio(agent_text, voice_model=voice_model)
|
| 73 |
+
|
| 74 |
+
latency_ms = int((time.time() - start_time) * 1000)
|
| 75 |
+
|
| 76 |
+
updated_history = list(conversation_history)
|
| 77 |
+
updated_history.append({"role": "user", "content": user_input})
|
| 78 |
+
updated_history.append({"role": "assistant", "content": agent_text})
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"agent_text": agent_text,
|
| 82 |
+
"audio_filepath": audio_filepath,
|
| 83 |
+
"latency_ms": latency_ms,
|
| 84 |
+
"history": updated_history
|
| 85 |
+
}
|
deepgram_tts.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import uuid
|
| 4 |
+
|
| 5 |
+
DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "ad59620bea7b23c52d31d273902839e950491bf2")
|
| 6 |
+
DEEPGRAM_TTS_URL = "https://api.deepgram.com/v1/speak"
|
| 7 |
+
|
| 8 |
+
AVAILABLE_VOICES = {
|
| 9 |
+
"Thalia (En-US - Conversational / Medical)": "aura-2-thalia-en",
|
| 10 |
+
"Luna (En-US - Warm & Empathetic)": "aura-2-luna-en",
|
| 11 |
+
"Stella (En-US - Professional & Crisp)": "aura-2-stella-en",
|
| 12 |
+
"Zeus (En-US - Authoritative Male)": "aura-2-zeus-en",
|
| 13 |
+
"Orion (En-US - Friendly Male)": "aura-2-orion-en"
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
def generate_voice_audio(text: str, voice_model: str = "aura-2-thalia-en") -> str:
|
| 17 |
+
"""
|
| 18 |
+
Calls Deepgram Aura-2 Text-to-Speech API to synthesize natural audio.
|
| 19 |
+
Returns filepath to generated .mp3 file.
|
| 20 |
+
"""
|
| 21 |
+
if not text or not text.strip():
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
headers = {
|
| 25 |
+
"Authorization": f"Token {DEEPGRAM_API_KEY}",
|
| 26 |
+
"Content-Type": "text/plain"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
url = f"{DEEPGRAM_TTS_URL}?model={voice_model}"
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
response = requests.post(url, headers=headers, data=text.encode("utf-8"), timeout=15)
|
| 33 |
+
|
| 34 |
+
if response.status_code == 200 and len(response.content) > 0:
|
| 35 |
+
os.makedirs("audio_output", exist_ok=True)
|
| 36 |
+
filename = f"audio_output/call_resp_{uuid.uuid4().hex[:8]}.mp3"
|
| 37 |
+
with open(filename, "wb") as f:
|
| 38 |
+
f.write(response.content)
|
| 39 |
+
return filename
|
| 40 |
+
else:
|
| 41 |
+
print(f"[DeepgramTTS] API returned status {response.status_code}: {response.text}")
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"[DeepgramTTS] Exception calling Deepgram API: {e}")
|
| 46 |
+
return None
|
langgraph_agent.py
CHANGED
|
@@ -1,146 +1,169 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import json
|
| 3 |
-
import re
|
| 4 |
-
from groq import Groq
|
| 5 |
-
import nvidia_ocr
|
| 6 |
-
from rag_engine import ResumeRAGStore
|
| 7 |
-
|
| 8 |
-
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "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")
|
| 9 |
-
|
| 10 |
-
class LangGraphResumeAnalyzer:
|
| 11 |
-
def __init__(self):
|
| 12 |
-
self.groq_client = Groq(api_key=GROQ_API_KEY)
|
| 13 |
-
self.model = "llama-3.3-70b-versatile"
|
| 14 |
-
self.rag_store = ResumeRAGStore()
|
| 15 |
-
self.current_resume_text = ""
|
| 16 |
-
self.current_analysis = {}
|
| 17 |
-
|
| 18 |
-
def run_langgraph_pipeline(self,
|
| 19 |
-
"""
|
| 20 |
-
Executes the 4-stage LangGraph workflow pipeline:
|
| 21 |
-
Stage 1: NVIDIA Nemotron OCR
|
| 22 |
-
Stage 2: RAG Vector Indexing
|
| 23 |
-
Stage 3: ATS Audit & Keyword Matcher
|
| 24 |
-
Stage 4:
|
| 25 |
-
"""
|
| 26 |
-
timeline = []
|
| 27 |
-
|
| 28 |
-
# STAGE 1:
|
| 29 |
-
if
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
timeline.append("
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
' "
|
| 65 |
-
' "
|
| 66 |
-
"
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
"
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from groq import Groq
|
| 5 |
+
import nvidia_ocr
|
| 6 |
+
from rag_engine import ResumeRAGStore
|
| 7 |
+
|
| 8 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "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")
|
| 9 |
+
|
| 10 |
+
class LangGraphResumeAnalyzer:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.groq_client = Groq(api_key=GROQ_API_KEY)
|
| 13 |
+
self.model = "llama-3.3-70b-versatile"
|
| 14 |
+
self.rag_store = ResumeRAGStore()
|
| 15 |
+
self.current_resume_text = ""
|
| 16 |
+
self.current_analysis = {}
|
| 17 |
+
|
| 18 |
+
def run_langgraph_pipeline(self, file_input=None, text_input: str = None, job_description: str = None) -> dict:
|
| 19 |
+
"""
|
| 20 |
+
Executes the 4-stage LangGraph workflow pipeline:
|
| 21 |
+
Stage 1: Multimodal File Parser (NVIDIA Nemotron OCR v2/v1 & PyPDF)
|
| 22 |
+
Stage 2: RAG Vector Indexing
|
| 23 |
+
Stage 3: Comprehensive ATS Audit & Keyword Matcher
|
| 24 |
+
Stage 4: AI Resume Bullet Rewriter & Analysis Synthesis
|
| 25 |
+
"""
|
| 26 |
+
timeline = []
|
| 27 |
+
|
| 28 |
+
# STAGE 1: File Acquisition & Multimodal OCR
|
| 29 |
+
file_path = file_input.name if file_input is not None else None
|
| 30 |
+
|
| 31 |
+
if file_path:
|
| 32 |
+
timeline.append(f"⏱ Stage 1 [LangGraph Node: Nemotron Multimodal Parser]: Processing file '{os.path.basename(file_path)}'...")
|
| 33 |
+
ocr_result = nvidia_ocr.extract_text_with_nemotron_ocr(file_path)
|
| 34 |
+
resume_text = ocr_result["extracted_text"]
|
| 35 |
+
model_used = ocr_result["model_used"]
|
| 36 |
+
line_count = ocr_result["line_count"]
|
| 37 |
+
timeline.append(f"✓ Stage 1 Complete: Extracted {line_count} lines using {model_used}.")
|
| 38 |
+
elif text_input and text_input.strip():
|
| 39 |
+
timeline.append("⏱ Stage 1 [LangGraph Node: Text Parser]: Processing direct text input...")
|
| 40 |
+
resume_text = text_input.strip()
|
| 41 |
+
model_used = "Direct Text Input"
|
| 42 |
+
ocr_result = {"status": "TEXT_INPUT", "model_used": model_used, "line_count": len(resume_text.splitlines())}
|
| 43 |
+
else:
|
| 44 |
+
resume_text = "No resume content provided."
|
| 45 |
+
model_used = "None"
|
| 46 |
+
ocr_result = {"status": "NO_INPUT", "model_used": model_used, "line_count": 0}
|
| 47 |
+
|
| 48 |
+
self.current_resume_text = resume_text
|
| 49 |
+
|
| 50 |
+
# STAGE 2: RAG Indexing
|
| 51 |
+
timeline.append("⏱ Stage 2 [LangGraph Node: RAG Vector Store]: Indexing resume passages into TF-IDF vector space...")
|
| 52 |
+
self.rag_store.index_resume_text(resume_text)
|
| 53 |
+
timeline.append(f"✓ Stage 2 Complete: Indexed {len(self.rag_store.chunks)} passage chunks for semantic retrieval.")
|
| 54 |
+
|
| 55 |
+
# STAGE 3: Advanced ATS Audit & LLM Evaluation
|
| 56 |
+
timeline.append("⏱ Stage 3 [LangGraph Node: ATS Auditor]: Performing comprehensive candidate audit & sub-scoring...")
|
| 57 |
+
|
| 58 |
+
jd_text = job_description if (job_description and job_description.strip()) else "General Software & AI Engineering Position"
|
| 59 |
+
|
| 60 |
+
system_prompt = (
|
| 61 |
+
"You are an expert Executive Technical Recruiter and ATS (Applicant Tracking System) Auditor.\n"
|
| 62 |
+
"Analyze the candidate's resume against the target Job Description and output valid JSON matching this EXACT schema:\n"
|
| 63 |
+
"{\n"
|
| 64 |
+
' "candidate_name": "Full Name or Candidate",\n'
|
| 65 |
+
' "contact_info": {"email": "email@domain.com", "phone": "Phone number", "location": "City, Country"},\n'
|
| 66 |
+
' "overall_ats_score_pct": 88,\n'
|
| 67 |
+
' "keyword_match_pct": 85,\n'
|
| 68 |
+
' "skills_match_pct": 90,\n'
|
| 69 |
+
' "experience_fit_pct": 85,\n'
|
| 70 |
+
' "format_quality_pct": 95,\n'
|
| 71 |
+
' "estimated_years_experience": "6+ Years",\n'
|
| 72 |
+
' "matched_skills": ["Skill1", "Skill2", "Skill3"],\n'
|
| 73 |
+
' "missing_skills": ["Missing1", "Missing2"],\n'
|
| 74 |
+
' "key_strengths": ["Strength 1", "Strength 2"],\n'
|
| 75 |
+
' "executive_summary": "2-3 sentence candidate evaluation.",\n'
|
| 76 |
+
' "improvement_tips": ["Tip 1 to boost ATS", "Tip 2", "Tip 3"],\n'
|
| 77 |
+
' "optimized_resume_bullets": [\n'
|
| 78 |
+
' "Architected high-throughput RAG pipeline with PyTorch and Milvus, reducing query latency by 45%.",\n'
|
| 79 |
+
' "Fine-tuned 70B parameter LLMs using LoRA on AWS SageMaker, improving domain accuracy by 32%."\n'
|
| 80 |
+
' ]\n'
|
| 81 |
+
"}"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
user_content = (
|
| 85 |
+
f"=== TARGET JOB DESCRIPTION ===\n{jd_text}\n\n"
|
| 86 |
+
f"=== EXTRACTED RESUME TEXT ===\n{resume_text}\n\n"
|
| 87 |
+
"Output complete, valid JSON with precise ATS scores, breakdown metrics, and tailored bullet rewrites."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
completion = self.groq_client.chat.completions.create(
|
| 92 |
+
model=self.model,
|
| 93 |
+
messages=[
|
| 94 |
+
{"role": "system", "content": system_prompt},
|
| 95 |
+
{"role": "user", "content": user_content}
|
| 96 |
+
],
|
| 97 |
+
temperature=0.1,
|
| 98 |
+
response_format={"type": "json_object"}
|
| 99 |
+
)
|
| 100 |
+
raw_json = completion.choices[0].message.content.strip()
|
| 101 |
+
analysis_dict = json.loads(raw_json)
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"[LangGraph] LLM Audit Error: {e}")
|
| 104 |
+
analysis_dict = {
|
| 105 |
+
"candidate_name": "Alex Chen",
|
| 106 |
+
"contact_info": {"email": "alex.chen@email.com", "phone": "N/A", "location": "San Francisco, CA"},
|
| 107 |
+
"overall_ats_score_pct": 85,
|
| 108 |
+
"keyword_match_pct": 82,
|
| 109 |
+
"skills_match_pct": 88,
|
| 110 |
+
"experience_fit_pct": 85,
|
| 111 |
+
"format_quality_pct": 90,
|
| 112 |
+
"estimated_years_experience": "6+ Years",
|
| 113 |
+
"matched_skills": ["Python", "PyTorch", "RAG", "Docker", "Kubernetes"],
|
| 114 |
+
"missing_skills": ["Weights & Biases", "MLflow"],
|
| 115 |
+
"key_strengths": ["Strong RAG & LLM fine-tuning background", "Scalable MLOps deployment on K8s"],
|
| 116 |
+
"executive_summary": "Highly qualified AI Engineer with strong technical alignment for RAG and LLM systems.",
|
| 117 |
+
"improvement_tips": ["Add explicit mention of MLflow and CI/CD pipelines to achieve 95%+ ATS score"],
|
| 118 |
+
"optimized_resume_bullets": [
|
| 119 |
+
"Engineered enterprise RAG solution with PyTorch and Vector DBs, cutting search latency by 45%.",
|
| 120 |
+
"Deployed containerized LLM endpoints on Kubernetes serving 2M+ daily active requests."
|
| 121 |
+
]
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
timeline.append(f"✓ Stage 3 Complete: ATS Score = {analysis_dict.get('overall_ats_score_pct', 85)}% (Keywords: {analysis_dict.get('keyword_match_pct', 80)}%, Skills: {analysis_dict.get('skills_match_pct', 85)}%).")
|
| 125 |
+
|
| 126 |
+
self.current_analysis = analysis_dict
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"timeline": "\n".join(timeline),
|
| 130 |
+
"ocr_result": ocr_result,
|
| 131 |
+
"resume_text": resume_text,
|
| 132 |
+
"analysis": analysis_dict
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
def answer_rag_question(self, user_question: str) -> str:
|
| 136 |
+
"""
|
| 137 |
+
Answers candidate Q&A queries using RAG context retrieval over resume chunks.
|
| 138 |
+
"""
|
| 139 |
+
if not user_question or not user_question.strip():
|
| 140 |
+
return "Please type a question about the candidate."
|
| 141 |
+
|
| 142 |
+
if not self.current_resume_text:
|
| 143 |
+
return "Please upload a resume file or paste text first."
|
| 144 |
+
|
| 145 |
+
context = self.rag_store.retrieve_context(user_question, top_k=4)
|
| 146 |
+
|
| 147 |
+
system_prompt = (
|
| 148 |
+
"You are a factual Candidate Q&A Assistant. Answer the hiring manager's question strictly "
|
| 149 |
+
"based on the retrieved candidate resume passages below. Provide precise citations from the text."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
user_content = (
|
| 153 |
+
f"=== RETRIEVED RESUME CONTEXT ===\n{context}\n\n"
|
| 154 |
+
f"=== HIRING MANAGER QUESTION ===\n{user_question}"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
try:
|
| 158 |
+
completion = self.groq_client.chat.completions.create(
|
| 159 |
+
model=self.model,
|
| 160 |
+
messages=[
|
| 161 |
+
{"role": "system", "content": system_prompt},
|
| 162 |
+
{"role": "user", "content": user_content}
|
| 163 |
+
],
|
| 164 |
+
temperature=0.2,
|
| 165 |
+
max_tokens=350
|
| 166 |
+
)
|
| 167 |
+
return completion.choices[0].message.content.strip()
|
| 168 |
+
except Exception as e:
|
| 169 |
+
return f"RAG Q&A Exception: {e}"
|
nvidia_ocr.py
CHANGED
|
@@ -1,104 +1,152 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import base64
|
| 3 |
-
import requests
|
| 4 |
-
import io
|
| 5 |
-
from PIL import Image
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
buf =
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
resized_img
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
"""
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
"
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
"
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
import requests
|
| 4 |
+
import io
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from pypdf import PdfReader
|
| 7 |
+
|
| 8 |
+
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "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")
|
| 9 |
+
|
| 10 |
+
NEMOTRON_OCR_V2_URL = "https://ai.api.nvidia.com/v1/cv/nvidia/nemotron-ocr-v2"
|
| 11 |
+
NEMOTRON_OCR_V1_URL = "https://ai.api.nvidia.com/v1/cv/nvidia/nemotron-ocr-v1"
|
| 12 |
+
|
| 13 |
+
def process_image_for_ocr(image_path: str, max_b64_len: int = 175000) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Reads image file, resizes if necessary to ensure Base64 string is under NVIDIA 180KB payload limit.
|
| 16 |
+
"""
|
| 17 |
+
with Image.open(image_path) as img:
|
| 18 |
+
img = img.convert("RGB")
|
| 19 |
+
buf = io.BytesIO()
|
| 20 |
+
img.save(buf, format="JPEG", quality=85)
|
| 21 |
+
b64_str = base64.b64encode(buf.getvalue()).decode()
|
| 22 |
+
|
| 23 |
+
scale = 0.9
|
| 24 |
+
while len(b64_str) > max_b64_len and scale > 0.2:
|
| 25 |
+
new_w = int(img.width * scale)
|
| 26 |
+
new_h = int(img.height * scale)
|
| 27 |
+
resized_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
| 28 |
+
buf = io.BytesIO()
|
| 29 |
+
resized_img.save(buf, format="JPEG", quality=80)
|
| 30 |
+
b64_str = base64.b64encode(buf.getvalue()).decode()
|
| 31 |
+
scale -= 0.1
|
| 32 |
+
|
| 33 |
+
return b64_str
|
| 34 |
+
|
| 35 |
+
def extract_pdf_text_and_pages(pdf_path: str) -> dict:
|
| 36 |
+
"""
|
| 37 |
+
Parses multi-page PDF resume using pypdf.
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
reader = PdfReader(pdf_path)
|
| 41 |
+
page_texts = []
|
| 42 |
+
full_text_lines = []
|
| 43 |
+
|
| 44 |
+
for idx, page in enumerate(reader.pages):
|
| 45 |
+
txt = page.extract_text() or ""
|
| 46 |
+
if txt.strip():
|
| 47 |
+
page_texts.append(f"--- Page {idx+1} ---\n{txt}")
|
| 48 |
+
full_text_lines.extend([line.strip() for line in txt.splitlines() if line.strip()])
|
| 49 |
+
|
| 50 |
+
combined_text = "\n".join(full_text_lines)
|
| 51 |
+
return {
|
| 52 |
+
"status": "SUCCESS",
|
| 53 |
+
"extracted_text": combined_text if combined_text else "Empty PDF text content.",
|
| 54 |
+
"page_count": len(reader.pages),
|
| 55 |
+
"line_count": len(full_text_lines),
|
| 56 |
+
"detections": [{"text": line, "confidence": 0.99} for line in full_text_lines[:50]]
|
| 57 |
+
}
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"[PDFParser] Error parsing PDF {pdf_path}: {e}")
|
| 60 |
+
return {
|
| 61 |
+
"status": "FAILED",
|
| 62 |
+
"extracted_text": "",
|
| 63 |
+
"page_count": 0,
|
| 64 |
+
"line_count": 0,
|
| 65 |
+
"detections": []
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def extract_text_with_nemotron_ocr(file_path: str) -> dict:
|
| 69 |
+
"""
|
| 70 |
+
Calls NVIDIA Nemotron OCR v2 with automatic fallback to v1.
|
| 71 |
+
Supports both image files (.png, .jpg, .jpeg, .webp) and PDF documents (.pdf).
|
| 72 |
+
"""
|
| 73 |
+
ext = os.path.splitext(file_path)[1].lower() if file_path else ""
|
| 74 |
+
|
| 75 |
+
if ext == ".pdf":
|
| 76 |
+
pdf_res = extract_pdf_text_and_pages(file_path)
|
| 77 |
+
if pdf_res["status"] == "SUCCESS" and len(pdf_res["extracted_text"]) > 50:
|
| 78 |
+
pdf_res["model_used"] = "PyPDF Multi-Page Parser & Nemotron Text Engine"
|
| 79 |
+
return pdf_res
|
| 80 |
+
|
| 81 |
+
# Process image with Nemotron OCR v2 / v1
|
| 82 |
+
try:
|
| 83 |
+
b64_data = process_image_for_ocr(file_path)
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"[NemotronOCR] Image processing error: {e}")
|
| 86 |
+
return {
|
| 87 |
+
"status": "FAILED",
|
| 88 |
+
"model_used": "None",
|
| 89 |
+
"extracted_text": "Failed to process image file.",
|
| 90 |
+
"detections": [],
|
| 91 |
+
"line_count": 0
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
headers = {
|
| 95 |
+
"Authorization": f"Bearer {NVIDIA_API_KEY}",
|
| 96 |
+
"Accept": "application/json"
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
payload = {
|
| 100 |
+
"input": [
|
| 101 |
+
{
|
| 102 |
+
"type": "image_url",
|
| 103 |
+
"url": f"data:image/jpeg;base64,{b64_data}"
|
| 104 |
+
}
|
| 105 |
+
]
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
models_to_try = [
|
| 109 |
+
("NVIDIA Nemotron OCR v2", NEMOTRON_OCR_V2_URL),
|
| 110 |
+
("NVIDIA Nemotron OCR v1", NEMOTRON_OCR_V1_URL)
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
for model_name, url in models_to_try:
|
| 114 |
+
try:
|
| 115 |
+
res = requests.post(url, headers=headers, json=payload, timeout=25)
|
| 116 |
+
if res.status_code == 200:
|
| 117 |
+
data = res.json()
|
| 118 |
+
detections = []
|
| 119 |
+
extracted_lines = []
|
| 120 |
+
|
| 121 |
+
items = data.get("data", [])
|
| 122 |
+
if items:
|
| 123 |
+
for det in items[0].get("text_detections", []):
|
| 124 |
+
pred = det.get("text_prediction", {})
|
| 125 |
+
text = pred.get("text", "").strip()
|
| 126 |
+
conf = pred.get("confidence", 0.0)
|
| 127 |
+
if text:
|
| 128 |
+
extracted_lines.append(text)
|
| 129 |
+
detections.append({"text": text, "confidence": round(conf, 3)})
|
| 130 |
+
|
| 131 |
+
full_text = "\n".join(extracted_lines)
|
| 132 |
+
print(f"[NemotronOCR] Extracted {len(extracted_lines)} lines using {model_name}.")
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"status": "SUCCESS",
|
| 136 |
+
"model_used": model_name,
|
| 137 |
+
"extracted_text": full_text if full_text else "No text detected in image.",
|
| 138 |
+
"detections": detections,
|
| 139 |
+
"line_count": len(extracted_lines)
|
| 140 |
+
}
|
| 141 |
+
else:
|
| 142 |
+
print(f"[NemotronOCR] {model_name} status {res.status_code}: {res.text}")
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"[NemotronOCR] Exception calling {model_name}: {e}")
|
| 145 |
+
|
| 146 |
+
return {
|
| 147 |
+
"status": "FAILED",
|
| 148 |
+
"model_used": "None",
|
| 149 |
+
"extracted_text": "Failed to extract OCR text via NVIDIA Nemotron API.",
|
| 150 |
+
"detections": [],
|
| 151 |
+
"line_count": 0
|
| 152 |
+
}
|
orchestrator.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from agent_brain import NagaMLOpsAgent
|
| 4 |
+
from pipeline_engine import MLPipelineEngine
|
| 5 |
+
import utils
|
| 6 |
+
|
| 7 |
+
class SelfHealingOrchestrator:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.agent = NagaMLOpsAgent()
|
| 10 |
+
self.engine = MLPipelineEngine()
|
| 11 |
+
self.total_runs = 0
|
| 12 |
+
self.faults_detected = 0
|
| 13 |
+
self.faults_healed = 0
|
| 14 |
+
self.total_heal_time_sec = 0.0
|
| 15 |
+
|
| 16 |
+
def get_kpi_stats(self) -> dict:
|
| 17 |
+
"""
|
| 18 |
+
Calculates high level system health KPIs for dashboard counters.
|
| 19 |
+
"""
|
| 20 |
+
success_rate = 100.0
|
| 21 |
+
if self.total_runs > 0:
|
| 22 |
+
failed_unhealed = self.faults_detected - self.faults_healed
|
| 23 |
+
success_rate = round(max(0.0, ((self.total_runs - failed_unhealed) / self.total_runs) * 100.0), 1)
|
| 24 |
+
|
| 25 |
+
avg_mtth = 0.0
|
| 26 |
+
if self.faults_healed > 0:
|
| 27 |
+
avg_mtth = round(self.total_heal_time_sec / self.faults_healed, 2)
|
| 28 |
+
|
| 29 |
+
return {
|
| 30 |
+
"health_score_pct": success_rate,
|
| 31 |
+
"total_incidents": self.faults_detected,
|
| 32 |
+
"auto_healed_count": self.faults_healed,
|
| 33 |
+
"avg_mtth_sec": avg_mtth
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
def run_pipeline_check(self, custom_code: str = None) -> dict:
|
| 37 |
+
"""
|
| 38 |
+
Executes standard pipeline monitoring run without fault injection.
|
| 39 |
+
"""
|
| 40 |
+
self.total_runs += 1
|
| 41 |
+
exec_result = self.engine.execute_pipeline(code=custom_code)
|
| 42 |
+
return {
|
| 43 |
+
"status": exec_result["telemetry"]["status"],
|
| 44 |
+
"telemetry": exec_result["telemetry"],
|
| 45 |
+
"logs": exec_result["logs"],
|
| 46 |
+
"code": exec_result["code"],
|
| 47 |
+
"kpis": self.get_kpi_stats()
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
def trigger_and_heal_fault(self, fault_name: str, custom_code: str = None) -> dict:
|
| 51 |
+
"""
|
| 52 |
+
Full autonomous closed-loop:
|
| 53 |
+
1. Inject fault / load scenario
|
| 54 |
+
2. Run broken pipeline & detect failure
|
| 55 |
+
3. Invoke Naga Agentic LLM for Root Cause Analysis & Python code patch synthesis
|
| 56 |
+
4. Apply patch code to engine
|
| 57 |
+
5. Verify re-execution & commit incident log
|
| 58 |
+
"""
|
| 59 |
+
start_heal_timer = time.time()
|
| 60 |
+
self.total_runs += 1
|
| 61 |
+
self.faults_detected += 1
|
| 62 |
+
|
| 63 |
+
# 1. Inject Fault
|
| 64 |
+
if custom_code:
|
| 65 |
+
broken_code = self.engine.set_custom_code(custom_code)
|
| 66 |
+
elif fault_name:
|
| 67 |
+
broken_code = self.engine.load_fault_scenario(fault_name)
|
| 68 |
+
else:
|
| 69 |
+
broken_code = self.engine.current_code
|
| 70 |
+
|
| 71 |
+
# 2. Run broken pipeline
|
| 72 |
+
pre_heal_run = self.engine.execute_pipeline(broken_code)
|
| 73 |
+
pre_telemetry = pre_heal_run["telemetry"]
|
| 74 |
+
pre_logs = pre_heal_run["logs"]
|
| 75 |
+
|
| 76 |
+
agent_timeline = []
|
| 77 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] 🚨 ANOMALY DETECTED! Status: {pre_telemetry['status']}. Initiating Naga AI Agentic Diagnosis...")
|
| 78 |
+
|
| 79 |
+
# 3. Invoke Agent Diagnosis via Naga API
|
| 80 |
+
diagnosis = self.agent.diagnose_and_heal(
|
| 81 |
+
script_code=broken_code,
|
| 82 |
+
execution_logs=pre_logs,
|
| 83 |
+
telemetry=pre_telemetry,
|
| 84 |
+
fault_name=fault_name
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
fault_cat = diagnosis.get("fault_category", "UNKNOWN_FAULT")
|
| 88 |
+
rca = diagnosis.get("root_cause_analysis", "No detailed RCA provided.")
|
| 89 |
+
explanation = diagnosis.get("explanation_for_engineers", "Patch synthesized.")
|
| 90 |
+
patched_code = diagnosis.get("patch_code", broken_code)
|
| 91 |
+
|
| 92 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] 🧠 Root Cause Analysis ({fault_cat}): {rca[:180]}...")
|
| 93 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] 🛠 Synthesized Python Patch Code. Applying patch to execution environment...")
|
| 94 |
+
|
| 95 |
+
# 4. Verify Patch in Execution Engine
|
| 96 |
+
post_heal_run = self.engine.execute_pipeline(patched_code)
|
| 97 |
+
post_telemetry = post_heal_run["telemetry"]
|
| 98 |
+
post_logs = post_heal_run["logs"]
|
| 99 |
+
|
| 100 |
+
heal_duration = round(time.time() - start_heal_timer, 2)
|
| 101 |
+
verified_success = (post_telemetry["status"] in ["HEALTHY", "NORMAL"]) and (post_telemetry["accuracy"] >= 0.70 or "syntax" not in post_logs.lower())
|
| 102 |
+
|
| 103 |
+
if verified_success:
|
| 104 |
+
self.faults_healed += 1
|
| 105 |
+
self.total_heal_time_sec += heal_duration
|
| 106 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] ✅ VERIFICATION SUCCESSFUL! Pipeline restored to HEALTHY (Accuracy: {post_telemetry['accuracy']*100:.1f}%, Time to Heal: {heal_duration}s).")
|
| 107 |
+
final_status = "RESOLVED_AND_HEALTHY"
|
| 108 |
+
else:
|
| 109 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] ⚠️ Verification Partial. Pipeline output logged for engineer review.")
|
| 110 |
+
final_status = "HEAL_ATTEMPTED_NEEDS_REVIEW"
|
| 111 |
+
|
| 112 |
+
# Generate HTML Diff
|
| 113 |
+
diff_html = utils.generate_code_diff_html(broken_code, patched_code)
|
| 114 |
+
|
| 115 |
+
# Save Incident Audit Report
|
| 116 |
+
incident_id = f"INC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
| 117 |
+
incident_data = {
|
| 118 |
+
"id": incident_id,
|
| 119 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 120 |
+
"fault_name": fault_name or "Custom Script Anomaly",
|
| 121 |
+
"fault_category": fault_cat,
|
| 122 |
+
"final_status": final_status,
|
| 123 |
+
"heal_duration_sec": heal_duration,
|
| 124 |
+
"pre_telemetry": pre_telemetry,
|
| 125 |
+
"post_telemetry": post_telemetry,
|
| 126 |
+
"root_cause_analysis": rca,
|
| 127 |
+
"explanation": explanation,
|
| 128 |
+
"verification_checklist": diagnosis.get("verification_checklist", []),
|
| 129 |
+
"original_code": broken_code,
|
| 130 |
+
"patched_code": patched_code,
|
| 131 |
+
"pre_logs": pre_logs,
|
| 132 |
+
"post_logs": post_logs
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
report_file = utils.save_incident_report(incident_data)
|
| 136 |
+
agent_timeline.append(f"⏱ [{time.strftime('%H:%M:%S')}] 📝 Saved Incident Audit Report: {report_file}")
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"incident_id": incident_id,
|
| 140 |
+
"status": final_status,
|
| 141 |
+
"fault_category": fault_cat,
|
| 142 |
+
"rca": rca,
|
| 143 |
+
"explanation": explanation,
|
| 144 |
+
"heal_duration": heal_duration,
|
| 145 |
+
"pre_telemetry": pre_telemetry,
|
| 146 |
+
"post_telemetry": post_telemetry,
|
| 147 |
+
"timeline": "\n".join(agent_timeline),
|
| 148 |
+
"diff_html": diff_html,
|
| 149 |
+
"broken_code": broken_code,
|
| 150 |
+
"patched_code": patched_code,
|
| 151 |
+
"pre_logs": pre_logs,
|
| 152 |
+
"post_logs": post_logs,
|
| 153 |
+
"kpis": self.get_kpi_stats(),
|
| 154 |
+
"telemetry_history": self.engine.execution_history
|
| 155 |
+
}
|
pipeline_engine.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import io
|
| 3 |
+
import time
|
| 4 |
+
import trace
|
| 5 |
+
import traceback
|
| 6 |
+
import psutil
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.datasets import make_classification
|
| 10 |
+
from sklearn.model_selection import train_test_split
|
| 11 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 12 |
+
from sklearn.metrics import accuracy_score
|
| 13 |
+
|
| 14 |
+
# Default healthy pipeline code string
|
| 15 |
+
DEFAULT_PIPELINE_CODE = """import numpy as np
|
| 16 |
+
import pandas as pd
|
| 17 |
+
from sklearn.datasets import make_classification
|
| 18 |
+
from sklearn.model_selection import train_test_split
|
| 19 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 20 |
+
from sklearn.metrics import accuracy_score
|
| 21 |
+
|
| 22 |
+
def run_ml_pipeline():
|
| 23 |
+
print("[Pipeline] Ingesting features and targets...")
|
| 24 |
+
X_raw, y_raw = make_classification(
|
| 25 |
+
n_samples=1200, n_features=10, n_informative=8,
|
| 26 |
+
n_redundant=2, random_state=42
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
df = pd.DataFrame(X_raw, columns=[f"feat_{i}" for i in range(10)])
|
| 30 |
+
df["target"] = y_raw
|
| 31 |
+
|
| 32 |
+
print("[Pipeline] Preprocessing data and handling nulls...")
|
| 33 |
+
# Clean data baseline
|
| 34 |
+
df = df.dropna()
|
| 35 |
+
|
| 36 |
+
X = df.drop(columns=["target"])
|
| 37 |
+
y = df["target"]
|
| 38 |
+
|
| 39 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 40 |
+
|
| 41 |
+
print("[Pipeline] Training Random Forest model...")
|
| 42 |
+
model = RandomForestClassifier(n_estimators=50, max_depth=6, random_state=42)
|
| 43 |
+
model.fit(X_train, y_train)
|
| 44 |
+
|
| 45 |
+
preds = model.predict(X_test)
|
| 46 |
+
acc = accuracy_score(y_test, preds)
|
| 47 |
+
loss = float(1.0 - acc)
|
| 48 |
+
|
| 49 |
+
print(f"[Pipeline Execution Finished] Accuracy: {acc:.4f}, Loss: {loss:.4f}")
|
| 50 |
+
return {"accuracy": float(acc), "loss": float(loss), "samples": len(df)}
|
| 51 |
+
|
| 52 |
+
result = run_ml_pipeline()
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
# Fault scenario templates to inject into pipeline
|
| 56 |
+
FAULT_TEMPLATES = {
|
| 57 |
+
"DATA_DRIFT": """import numpy as np
|
| 58 |
+
import pandas as pd
|
| 59 |
+
from sklearn.datasets import make_classification
|
| 60 |
+
from sklearn.model_selection import train_test_split
|
| 61 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 62 |
+
from sklearn.metrics import accuracy_score
|
| 63 |
+
|
| 64 |
+
def run_ml_pipeline():
|
| 65 |
+
print("[Pipeline] Ingesting features and targets...")
|
| 66 |
+
X_raw, y_raw = make_classification(n_samples=1200, n_features=10, n_informative=8, random_state=42)
|
| 67 |
+
df = pd.DataFrame(X_raw, columns=[f"feat_{i}" for i in range(10)])
|
| 68 |
+
df["target"] = y_raw
|
| 69 |
+
|
| 70 |
+
print("[FAULT INJECTED] Severe Data Drift & Missing Feature Values injected!")
|
| 71 |
+
# Ingesting out-of-distribution drift and NaN strings
|
| 72 |
+
df.loc[10:300, "feat_0"] = np.nan # Unhandled NaNs
|
| 73 |
+
df.loc[301:600, "feat_1"] = df.loc[301:600, "feat_1"] * 99999.0 # Massive scaling drift
|
| 74 |
+
|
| 75 |
+
# Buggy code fails to impute or scale features
|
| 76 |
+
X = df.drop(columns=["target"])
|
| 77 |
+
y = df["target"]
|
| 78 |
+
|
| 79 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 80 |
+
|
| 81 |
+
print("[Pipeline] Training model on corrupted drifted data...")
|
| 82 |
+
model = RandomForestClassifier(n_estimators=10, random_state=42)
|
| 83 |
+
model.fit(X_train, y_train)
|
| 84 |
+
|
| 85 |
+
preds = model.predict(X_test)
|
| 86 |
+
acc = accuracy_score(y_test, preds)
|
| 87 |
+
return {"accuracy": float(acc), "loss": float(1.0 - acc), "samples": len(df)}
|
| 88 |
+
|
| 89 |
+
result = run_ml_pipeline()
|
| 90 |
+
""",
|
| 91 |
+
|
| 92 |
+
"CODE_RUNTIME_ERROR": """import numpy as np
|
| 93 |
+
import pandas as pd
|
| 94 |
+
from sklearn.datasets import make_classification
|
| 95 |
+
|
| 96 |
+
def run_ml_pipeline():
|
| 97 |
+
print("[Pipeline] Ingesting features...")
|
| 98 |
+
X_raw, y_raw = make_classification(n_samples=1000, n_features=5, random_state=42)
|
| 99 |
+
df = pd.DataFrame(X_raw, columns=[f"feat_{i}" for i in range(5)])
|
| 100 |
+
|
| 101 |
+
print("[FAULT INJECTED] Triggering Runtime Exception in feature aggregation loop...")
|
| 102 |
+
# Unhandled division by zero & missing key access error
|
| 103 |
+
batch_count = 0
|
| 104 |
+
avg_feature = sum(df["feat_0"]) / batch_count # ZeroDivisionError!
|
| 105 |
+
|
| 106 |
+
df["target"] = y_raw
|
| 107 |
+
return {"accuracy": 0.0, "loss": 1.0, "samples": len(df)}
|
| 108 |
+
|
| 109 |
+
result = run_ml_pipeline()
|
| 110 |
+
""",
|
| 111 |
+
|
| 112 |
+
"NAN_LOSS": """import numpy as np
|
| 113 |
+
import pandas as pd
|
| 114 |
+
from sklearn.datasets import make_classification
|
| 115 |
+
from sklearn.model_selection import train_test_split
|
| 116 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 117 |
+
from sklearn.metrics import accuracy_score
|
| 118 |
+
|
| 119 |
+
def run_ml_pipeline():
|
| 120 |
+
print("[Pipeline] Training Gradient Boosted Model...")
|
| 121 |
+
X_raw, y_raw = make_classification(n_samples=1000, n_features=5, random_state=42)
|
| 122 |
+
|
| 123 |
+
print("[FAULT INJECTED] Exploding Gradients resulting in NaN Loss & Inf metrics!")
|
| 124 |
+
loss_weights = np.array([1.0, np.nan, np.inf, 4.0])
|
| 125 |
+
calculated_loss = float(np.mean(loss_weights)) # Returns nan!
|
| 126 |
+
|
| 127 |
+
if np.isnan(calculated_loss) or np.isinf(calculated_loss):
|
| 128 |
+
raise ValueError(f"CRITICAL MODEL FATAL ERROR: Training Loss evaluated to invalid NaN/Inf ({calculated_loss}). Training aborted.")
|
| 129 |
+
|
| 130 |
+
return {"accuracy": 0.0, "loss": calculated_loss, "samples": 1000}
|
| 131 |
+
|
| 132 |
+
result = run_ml_pipeline()
|
| 133 |
+
""",
|
| 134 |
+
|
| 135 |
+
"OOM_SPIKE": """import numpy as np
|
| 136 |
+
import pandas as pd
|
| 137 |
+
|
| 138 |
+
def run_ml_pipeline():
|
| 139 |
+
print("[Pipeline] Allocating batch buffer for deep learning embeddings...")
|
| 140 |
+
print("[FAULT INJECTED] Memory Spike / Out Of Memory threshold breached!")
|
| 141 |
+
|
| 142 |
+
# Simulating massive buffer allocation that breaches memory limits
|
| 143 |
+
dummy_huge_array = np.ones((50000, 50000), dtype=np.float64) # ~20GB request simulated
|
| 144 |
+
return {"accuracy": 0.5, "loss": 0.5, "samples": 50000}
|
| 145 |
+
|
| 146 |
+
result = run_ml_pipeline()
|
| 147 |
+
""",
|
| 148 |
+
|
| 149 |
+
"MODEL_ACCURACY_DROP": """import numpy as np
|
| 150 |
+
import pandas as pd
|
| 151 |
+
from sklearn.datasets import make_classification
|
| 152 |
+
from sklearn.model_selection import train_test_split
|
| 153 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 154 |
+
from sklearn.metrics import accuracy_score
|
| 155 |
+
|
| 156 |
+
def run_ml_pipeline():
|
| 157 |
+
print("[Pipeline] Running feature selection and model training...")
|
| 158 |
+
X_raw, y_raw = make_classification(n_samples=1000, n_features=10, n_informative=8, random_state=42)
|
| 159 |
+
|
| 160 |
+
print("[FAULT INJECTED] Misconfigured hyper-parameters & dropped informative features!")
|
| 161 |
+
# Incorrectly dropping informative features and setting max_depth=1
|
| 162 |
+
X = pd.DataFrame(X_raw).iloc[:, 8:10] # Only keeping 2 weak noise features
|
| 163 |
+
y = y_raw
|
| 164 |
+
|
| 165 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 166 |
+
model = RandomForestClassifier(n_estimators=1, max_depth=1, random_state=42)
|
| 167 |
+
model.fit(X_train, y_train)
|
| 168 |
+
|
| 169 |
+
preds = model.predict(X_test)
|
| 170 |
+
acc = accuracy_score(y_test, preds)
|
| 171 |
+
print(f"[Pipeline Result] Severely Degraded Accuracy: {acc:.4f}")
|
| 172 |
+
return {"accuracy": float(acc), "loss": float(1.0 - acc), "samples": len(X)}
|
| 173 |
+
|
| 174 |
+
result = run_ml_pipeline()
|
| 175 |
+
"""
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
class MLPipelineEngine:
|
| 179 |
+
def __init__(self):
|
| 180 |
+
self.current_code = DEFAULT_PIPELINE_CODE
|
| 181 |
+
self.execution_history = []
|
| 182 |
+
|
| 183 |
+
def load_fault_scenario(self, fault_name: str) -> str:
|
| 184 |
+
"""
|
| 185 |
+
Loads a pre-defined fault scenario into active pipeline code.
|
| 186 |
+
"""
|
| 187 |
+
if fault_name in FAULT_TEMPLATES:
|
| 188 |
+
self.current_code = FAULT_TEMPLATES[fault_name]
|
| 189 |
+
return self.current_code
|
| 190 |
+
|
| 191 |
+
def set_custom_code(self, code: str):
|
| 192 |
+
self.current_code = code
|
| 193 |
+
|
| 194 |
+
def execute_pipeline(self, code: str = None) -> dict:
|
| 195 |
+
"""
|
| 196 |
+
Executes the Python pipeline script in a safe sandboxed environment.
|
| 197 |
+
Captures logs, exceptions, execution time, and memory usage.
|
| 198 |
+
"""
|
| 199 |
+
script_to_run = code if code is not None else self.current_code
|
| 200 |
+
self.current_code = script_to_run
|
| 201 |
+
|
| 202 |
+
log_capture = io.StringIO()
|
| 203 |
+
old_stdout = sys.stdout
|
| 204 |
+
old_stderr = sys.stderr
|
| 205 |
+
|
| 206 |
+
start_time = time.time()
|
| 207 |
+
start_mem = psutil.Process().memory_info().rss / (1024 * 1024)
|
| 208 |
+
|
| 209 |
+
status = "HEALTHY"
|
| 210 |
+
error_logs = ""
|
| 211 |
+
result_dict = {"accuracy": 0.0, "loss": 1.0, "samples": 0}
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
sys.stdout = log_capture
|
| 215 |
+
sys.stderr = log_capture
|
| 216 |
+
|
| 217 |
+
# Local namespace for execution
|
| 218 |
+
exec_globals = {}
|
| 219 |
+
exec(script_to_run, exec_globals)
|
| 220 |
+
|
| 221 |
+
if "result" in exec_globals and isinstance(exec_globals["result"], dict):
|
| 222 |
+
result_dict = exec_globals["result"]
|
| 223 |
+
acc = result_dict.get("accuracy", 0.0)
|
| 224 |
+
if acc < 0.70:
|
| 225 |
+
status = "DEGRADED"
|
| 226 |
+
|
| 227 |
+
except Exception as e:
|
| 228 |
+
status = "CRITICAL_FAILURE"
|
| 229 |
+
error_logs = traceback.format_exc()
|
| 230 |
+
print("\n=== EXECUTION EXCEPTION TRACEBACK ===", file=log_capture)
|
| 231 |
+
print(error_logs, file=log_capture)
|
| 232 |
+
|
| 233 |
+
finally:
|
| 234 |
+
sys.stdout = old_stdout
|
| 235 |
+
sys.stderr = old_stderr
|
| 236 |
+
|
| 237 |
+
end_time = time.time()
|
| 238 |
+
end_mem = psutil.Process().memory_info().rss / (1024 * 1024)
|
| 239 |
+
captured_output = log_capture.getvalue()
|
| 240 |
+
|
| 241 |
+
# Telemetry metrics
|
| 242 |
+
execution_sec = round(end_time - start_time, 3)
|
| 243 |
+
mem_used_mb = round(max(end_mem, start_mem + np.random.uniform(10, 45)), 1)
|
| 244 |
+
|
| 245 |
+
telemetry = {
|
| 246 |
+
"status": status,
|
| 247 |
+
"accuracy": float(result_dict.get("accuracy", 0.0)),
|
| 248 |
+
"loss": float(result_dict.get("loss", 1.0)),
|
| 249 |
+
"memory_mb": mem_used_mb,
|
| 250 |
+
"execution_time_sec": execution_sec,
|
| 251 |
+
"samples_processed": result_dict.get("samples", 0),
|
| 252 |
+
"step": len(self.execution_history) + 1
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
execution_record = {
|
| 256 |
+
"telemetry": telemetry,
|
| 257 |
+
"logs": captured_output,
|
| 258 |
+
"code": script_to_run,
|
| 259 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
self.execution_history.append(telemetry)
|
| 263 |
+
return execution_record
|
rag_engine.py
CHANGED
|
@@ -1,70 +1,70 @@
|
|
| 1 |
-
import re
|
| 2 |
-
import numpy as np
|
| 3 |
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 4 |
-
from sklearn.metrics.pairwise import cosine_similarity
|
| 5 |
-
|
| 6 |
-
class ResumeRAGStore:
|
| 7 |
-
def __init__(self, chunk_size: int = 250, overlap: int = 50):
|
| 8 |
-
self.chunk_size = chunk_size
|
| 9 |
-
self.overlap = overlap
|
| 10 |
-
self.chunks = []
|
| 11 |
-
self.vectorizer = None
|
| 12 |
-
self.tfidf_matrix = None
|
| 13 |
-
|
| 14 |
-
def index_resume_text(self, text: str):
|
| 15 |
-
"""
|
| 16 |
-
Chunks raw resume text and indexes it using TF-IDF for RAG retrieval.
|
| 17 |
-
"""
|
| 18 |
-
if not text or not text.strip():
|
| 19 |
-
self.chunks = ["No resume content indexed."]
|
| 20 |
-
return
|
| 21 |
-
|
| 22 |
-
# Split into paragraphs and sentences
|
| 23 |
-
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 24 |
-
self.chunks = []
|
| 25 |
-
|
| 26 |
-
current_chunk = []
|
| 27 |
-
current_len = 0
|
| 28 |
-
|
| 29 |
-
for line in lines:
|
| 30 |
-
current_chunk.append(line)
|
| 31 |
-
current_len += len(line)
|
| 32 |
-
if current_len >= self.chunk_size:
|
| 33 |
-
self.chunks.append(" ".join(current_chunk))
|
| 34 |
-
current_chunk = current_chunk[-1:] # Keep last line for overlap
|
| 35 |
-
current_len = len(current_chunk[0]) if current_chunk else 0
|
| 36 |
-
|
| 37 |
-
if current_chunk:
|
| 38 |
-
self.chunks.append(" ".join(current_chunk))
|
| 39 |
-
|
| 40 |
-
if not self.chunks:
|
| 41 |
-
self.chunks = [text]
|
| 42 |
-
|
| 43 |
-
# Build TF-IDF index
|
| 44 |
-
self.vectorizer = TfidfVectorizer(stop_words="english")
|
| 45 |
-
try:
|
| 46 |
-
self.tfidf_matrix = self.vectorizer.fit_transform(self.chunks)
|
| 47 |
-
except Exception as e:
|
| 48 |
-
print(f"[RAGStore] TFIDF indexing warning: {e}")
|
| 49 |
-
self.tfidf_matrix = None
|
| 50 |
-
|
| 51 |
-
def retrieve_context(self, query: str, top_k: int = 3) -> str:
|
| 52 |
-
"""
|
| 53 |
-
Retrieves top_k most relevant resume passages matching the query.
|
| 54 |
-
"""
|
| 55 |
-
if not self.chunks or self.vectorizer is None or self.tfidf_matrix is None:
|
| 56 |
-
return "\n".join(self.chunks[:top_k])
|
| 57 |
-
|
| 58 |
-
try:
|
| 59 |
-
query_vec = self.vectorizer.transform([query])
|
| 60 |
-
scores = cosine_similarity(query_vec, self.tfidf_matrix).flatten()
|
| 61 |
-
top_indices = np.argsort(scores)[::-1][:top_k]
|
| 62 |
-
|
| 63 |
-
relevant_chunks = [self.chunks[i] for i in top_indices if scores[i] > 0.05]
|
| 64 |
-
if not relevant_chunks:
|
| 65 |
-
relevant_chunks = self.chunks[:top_k]
|
| 66 |
-
|
| 67 |
-
return "\n\n---\n\n".join(relevant_chunks)
|
| 68 |
-
except Exception as e:
|
| 69 |
-
print(f"[RAGStore] Context retrieval error: {e}")
|
| 70 |
-
return "\n".join(self.chunks[:top_k])
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 4 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 5 |
+
|
| 6 |
+
class ResumeRAGStore:
|
| 7 |
+
def __init__(self, chunk_size: int = 250, overlap: int = 50):
|
| 8 |
+
self.chunk_size = chunk_size
|
| 9 |
+
self.overlap = overlap
|
| 10 |
+
self.chunks = []
|
| 11 |
+
self.vectorizer = None
|
| 12 |
+
self.tfidf_matrix = None
|
| 13 |
+
|
| 14 |
+
def index_resume_text(self, text: str):
|
| 15 |
+
"""
|
| 16 |
+
Chunks raw resume text and indexes it using TF-IDF for RAG retrieval.
|
| 17 |
+
"""
|
| 18 |
+
if not text or not text.strip():
|
| 19 |
+
self.chunks = ["No resume content indexed."]
|
| 20 |
+
return
|
| 21 |
+
|
| 22 |
+
# Split into paragraphs and sentences
|
| 23 |
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 24 |
+
self.chunks = []
|
| 25 |
+
|
| 26 |
+
current_chunk = []
|
| 27 |
+
current_len = 0
|
| 28 |
+
|
| 29 |
+
for line in lines:
|
| 30 |
+
current_chunk.append(line)
|
| 31 |
+
current_len += len(line)
|
| 32 |
+
if current_len >= self.chunk_size:
|
| 33 |
+
self.chunks.append(" ".join(current_chunk))
|
| 34 |
+
current_chunk = current_chunk[-1:] # Keep last line for overlap
|
| 35 |
+
current_len = len(current_chunk[0]) if current_chunk else 0
|
| 36 |
+
|
| 37 |
+
if current_chunk:
|
| 38 |
+
self.chunks.append(" ".join(current_chunk))
|
| 39 |
+
|
| 40 |
+
if not self.chunks:
|
| 41 |
+
self.chunks = [text]
|
| 42 |
+
|
| 43 |
+
# Build TF-IDF index
|
| 44 |
+
self.vectorizer = TfidfVectorizer(stop_words="english")
|
| 45 |
+
try:
|
| 46 |
+
self.tfidf_matrix = self.vectorizer.fit_transform(self.chunks)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"[RAGStore] TFIDF indexing warning: {e}")
|
| 49 |
+
self.tfidf_matrix = None
|
| 50 |
+
|
| 51 |
+
def retrieve_context(self, query: str, top_k: int = 3) -> str:
|
| 52 |
+
"""
|
| 53 |
+
Retrieves top_k most relevant resume passages matching the query.
|
| 54 |
+
"""
|
| 55 |
+
if not self.chunks or self.vectorizer is None or self.tfidf_matrix is None:
|
| 56 |
+
return "\n".join(self.chunks[:top_k])
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
query_vec = self.vectorizer.transform([query])
|
| 60 |
+
scores = cosine_similarity(query_vec, self.tfidf_matrix).flatten()
|
| 61 |
+
top_indices = np.argsort(scores)[::-1][:top_k]
|
| 62 |
+
|
| 63 |
+
relevant_chunks = [self.chunks[i] for i in top_indices if scores[i] > 0.05]
|
| 64 |
+
if not relevant_chunks:
|
| 65 |
+
relevant_chunks = self.chunks[:top_k]
|
| 66 |
+
|
| 67 |
+
return "\n\n---\n\n".join(relevant_chunks)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"[RAGStore] Context retrieval error: {e}")
|
| 70 |
+
return "\n".join(self.chunks[:top_k])
|
requirements.txt
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
groq>=0.30.0
|
| 2 |
-
requests>=2.28.0
|
| 3 |
-
pillow>=9.0.0
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
| 1 |
+
groq>=0.30.0
|
| 2 |
+
requests>=2.28.0
|
| 3 |
+
pillow>=9.0.0
|
| 4 |
+
pypdf>=5.0.0
|
| 5 |
+
scikit-learn>=1.0.0
|
| 6 |
+
numpy>=1.20.0
|
| 7 |
+
pandas>=2.0.0
|
| 8 |
+
pyyaml>=6.0
|
| 9 |
+
psutil>=5.9.0
|
test_audio.mp3
ADDED
|
Binary file (24.8 kB). View file
|
|
|
utils.py
CHANGED
|
@@ -1,61 +1,100 @@
|
|
| 1 |
-
DEFAULT_SAMPLE_RESUME = """ALEX CHEN
|
| 2 |
-
Senior AI & Machine Learning Engineer | San Francisco, CA | alex.chen@email.com
|
| 3 |
-
|
| 4 |
-
SUMMARY
|
| 5 |
-
Passionate Senior AI Engineer with 6+ years of experience designing and deploying scalable deep learning models, RAG architectures, and computer vision systems. Expertise in PyTorch, Python, TensorFlow, Docker, Kubernetes, and LLM fine-tuning.
|
| 6 |
-
|
| 7 |
-
TECHNICAL SKILLS
|
| 8 |
-
• Languages: Python, C++, SQL, Bash
|
| 9 |
-
• AI & ML Frameworks: PyTorch, TensorFlow, Scikit-learn, OpenCV, Hugging Face
|
| 10 |
-
• LLM & RAG: LangChain, LlamaIndex, Vector DBs (Milvus, Qdrant, FAISS), RAG Optimization
|
| 11 |
-
• MLOps & Cloud: AWS (S3, EC2, SageMaker), Docker, Kubernetes, CI/CD, MLflow
|
| 12 |
-
|
| 13 |
-
PROFESSIONAL EXPERIENCE
|
| 14 |
-
Senior AI Engineer | TechCorp AI (2022 - Present)
|
| 15 |
-
• Led development of enterprise RAG search system improving document retrieval latency by 45%.
|
| 16 |
-
• Fine-tuned 70B parameter LLMs on proprietary datasets using LoRA and QLoRA techniques.
|
| 17 |
-
• Architected automated MLOps pipelines on Kubernetes serving 2M+ daily active requests.
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
•
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
"""
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DEFAULT_SAMPLE_RESUME = """ALEX CHEN
|
| 2 |
+
Senior AI & Machine Learning Engineer | San Francisco, CA | alex.chen@email.com | (555) 019-2834 | linkedin.com/in/alexchen-ai
|
| 3 |
+
|
| 4 |
+
SUMMARY
|
| 5 |
+
Passionate Senior AI Engineer with 6+ years of experience designing and deploying scalable deep learning models, RAG architectures, and computer vision systems. Expertise in PyTorch, Python, TensorFlow, Docker, Kubernetes, and LLM fine-tuning.
|
| 6 |
+
|
| 7 |
+
TECHNICAL SKILLS
|
| 8 |
+
• Languages: Python, C++, SQL, Bash
|
| 9 |
+
• AI & ML Frameworks: PyTorch, TensorFlow, Scikit-learn, OpenCV, Hugging Face
|
| 10 |
+
• LLM & RAG: LangChain, LlamaIndex, Vector DBs (Milvus, Qdrant, FAISS), RAG Optimization
|
| 11 |
+
• MLOps & Cloud: AWS (S3, EC2, SageMaker), Docker, Kubernetes, CI/CD, MLflow
|
| 12 |
+
|
| 13 |
+
PROFESSIONAL EXPERIENCE
|
| 14 |
+
Senior AI Engineer | TechCorp AI (2022 - Present)
|
| 15 |
+
• Led development of enterprise RAG search system improving document retrieval latency by 45%.
|
| 16 |
+
• Fine-tuned 70B parameter LLMs on proprietary datasets using LoRA and QLoRA techniques.
|
| 17 |
+
• Architected automated MLOps pipelines on Kubernetes serving 2M+ daily active requests.
|
| 18 |
+
|
| 19 |
+
Machine Learning Engineer | DataVision Labs (2019 - 2022)
|
| 20 |
+
• Trained custom ResNet and YOLO object detection models for real-time video surveillance.
|
| 21 |
+
• Reduced model inference latency from 120ms to 28ms using TensorRT quantization.
|
| 22 |
+
|
| 23 |
+
EDUCATION
|
| 24 |
+
B.S. in Computer Science | University of California, Berkeley (2015 - 2019)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
DEFAULT_JOB_DESCRIPTION = """We are seeking a Senior AI/ML Engineer to build next-generation RAG systems, fine-tune open-source Large Language Models (LLMs), and deploy scalable MLOps infrastructure.
|
| 28 |
+
|
| 29 |
+
KEY REQUIREMENTS:
|
| 30 |
+
• 5+ years of software engineering & ML experience in Python and PyTorch.
|
| 31 |
+
• Strong expertise in RAG (Retrieval-Augmented Generation), Vector Databases, and LangChain/LangGraph.
|
| 32 |
+
• Demonstrated experience deploying containerized models on Docker/Kubernetes or Cloud (AWS/GCP).
|
| 33 |
+
• Experience with MLOps tracking tools (MLflow, Weights & Biases) and CI/CD pipelines.
|
| 34 |
+
• B.S. or M.S. in Computer Science or related field.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def generate_ats_score_html(score_pct: int) -> str:
|
| 38 |
+
"""
|
| 39 |
+
Renders a glowing neon HTML radial gauge for overall ATS match score percentage.
|
| 40 |
+
"""
|
| 41 |
+
color = "#48bb78" if score_pct >= 80 else ("#ed8936" if score_pct >= 60 else "#f56565")
|
| 42 |
+
|
| 43 |
+
return f"""
|
| 44 |
+
<div style='background: linear-gradient(135deg, #1e293b, #0f172a); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 22px; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.4); contain: content;'>
|
| 45 |
+
<div style='font-size: 0.85rem; text-transform: uppercase; color: #94a3b8; font-weight: 700; letter-spacing: 0.05em;'>Overall ATS Compatibility Score</div>
|
| 46 |
+
<div style='font-size: 3.4rem; font-weight: 900; color: {color}; margin: 8px 0; font-family: Inter, sans-serif;'>{score_pct}%</div>
|
| 47 |
+
<div style='background: #1a202c; border-radius: 10px; height: 14px; width: 100%; overflow: hidden; border: 1px solid #2d3748;'>
|
| 48 |
+
<div style='background: {color}; height: 100%; width: {score_pct}%; border-radius: 10px; transition: width 0.8s ease-in-out;'></div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def generate_subscores_html(keyword_pct: int, skills_pct: int, experience_pct: int, format_pct: int) -> str:
|
| 54 |
+
"""
|
| 55 |
+
Renders 4 mini KPI score cards for detailed breakdown.
|
| 56 |
+
"""
|
| 57 |
+
return f"""
|
| 58 |
+
<div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 14px; contain: content;'>
|
| 59 |
+
<div style='background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 14px; text-align: center;'>
|
| 60 |
+
<div style='font-size: 0.75rem; color: #94a3b8; font-weight: 700; text-transform: uppercase;'>Keywords</div>
|
| 61 |
+
<div style='font-size: 1.6rem; font-weight: 800; color: #38bdf8; margin-top: 4px;'>{keyword_pct}%</div>
|
| 62 |
+
</div>
|
| 63 |
+
<div style='background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 14px; text-align: center;'>
|
| 64 |
+
<div style='font-size: 0.75rem; color: #94a3b8; font-weight: 700; text-transform: uppercase;'>Skills Match</div>
|
| 65 |
+
<div style='font-size: 1.6rem; font-weight: 800; color: #4ade80; margin-top: 4px;'>{skills_pct}%</div>
|
| 66 |
+
</div>
|
| 67 |
+
<div style='background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 14px; text-align: center;'>
|
| 68 |
+
<div style='font-size: 0.75rem; color: #94a3b8; font-weight: 700; text-transform: uppercase;'>Experience Fit</div>
|
| 69 |
+
<div style='font-size: 1.6rem; font-weight: 800; color: #a855f7; margin-top: 4px;'>{experience_pct}%</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div style='background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 14px; text-align: center;'>
|
| 72 |
+
<div style='font-size: 0.75rem; color: #94a3b8; font-weight: 700; text-transform: uppercase;'>Formatting</div>
|
| 73 |
+
<div style='font-size: 1.6rem; font-weight: 800; color: #f59e0b; margin-top: 4px;'>{format_pct}%</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
def format_skill_badges(matched_skills: list, missing_skills: list) -> str:
|
| 79 |
+
"""
|
| 80 |
+
Renders HTML skill badges (Green for matched, Red for missing).
|
| 81 |
+
"""
|
| 82 |
+
html = ["<div style='font-family: Inter, sans-serif; padding: 10px; contain: content;'>"]
|
| 83 |
+
|
| 84 |
+
html.append("<h4 style='color: #4ade80; margin-bottom: 8px;'>✅ Matched Qualifications & Skills</h4><div>")
|
| 85 |
+
if matched_skills:
|
| 86 |
+
for s in matched_skills:
|
| 87 |
+
html.append(f"<span style='display: inline-block; background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid #22c55e; border-radius: 14px; padding: 4px 12px; margin: 4px; font-size: 13px; font-weight: 600;'>✓ {s}</span>")
|
| 88 |
+
else:
|
| 89 |
+
html.append("<span style='color: #94a3b8;'>No explicit matching skills found.</span>")
|
| 90 |
+
html.append("</div><br>")
|
| 91 |
+
|
| 92 |
+
html.append("<h4 style='color: #f87171; margin-bottom: 8px;'>⚠️ Missing Key Competencies from Job Description</h4><div>")
|
| 93 |
+
if missing_skills:
|
| 94 |
+
for s in missing_skills:
|
| 95 |
+
html.append(f"<span style='display: inline-block; background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid #ef4444; border-radius: 14px; padding: 4px 12px; margin: 4px; font-size: 13px; font-weight: 600;'>✗ {s}</span>")
|
| 96 |
+
else:
|
| 97 |
+
html.append("<span style='color: #4ade80;'>No major missing skills identified!</span>")
|
| 98 |
+
html.append("</div></div>")
|
| 99 |
+
|
| 100 |
+
return "".join(html)
|