Anish Dahiya commited on
Commit
e5cb314
·
1 Parent(s): 144f9ca

Clean up nested package directory structure

Browse files
tech_radar/cli.py CHANGED
@@ -13,11 +13,31 @@ def cli():
13
  pass
14
 
15
  @cli.command()
16
- def seed():
17
- """Seed the SQLite database and Vector Store with universal tech job postings."""
18
- console.print("[bold green]🌱 Seeding TechRadar Database & Vector Engine...[/bold green]")
 
19
  from tech_radar.scrapers.seeder import seed_database
20
- seed_database()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  @cli.command()
23
  @click.option("--transport", default="stdio", type=click.Choice(["stdio", "sse"]), help="MCP transport protocol")
 
13
  pass
14
 
15
  @cli.command()
16
+ @click.option("--live/--no-live", default=True, help="Fetch real-time live internet jobs from RemoteOK, Jobicy, WWR")
17
+ def seed(live):
18
+ """Seed SQLite database & Vector Engine with curated + live internet job postings."""
19
+ console.print(f"[bold green]🌱 Seeding TechRadar Database (Live Scraping: {live})...[/bold green]")
20
  from tech_radar.scrapers.seeder import seed_database
21
+ seed_database(fetch_live=live)
22
+
23
+ @cli.command()
24
+ def scrape_live():
25
+ """Trigger real-time live internet job scraping pipeline."""
26
+ console.print("[bold cyan]🌐 Executing Live Internet Job Scraper Pipeline...[/bold cyan]")
27
+ from tech_radar.scrapers.live_scraper import LiveInternetScraper
28
+ from tech_radar.db.database import DatabaseManager
29
+ from tech_radar.db.vector_store import SemanticVectorStore
30
+
31
+ scraper = LiveInternetScraper()
32
+ live_jobs = scraper.fetch_all_live_jobs()
33
+ db = DatabaseManager()
34
+ for job in live_jobs:
35
+ db.save_job_posting(job)
36
+
37
+ vector_store = SemanticVectorStore()
38
+ all_jobs = db.get_all_jobs()
39
+ vector_store.index_jobs(all_jobs)
40
+ console.print(f"[bold green]✅ Ingested {len(live_jobs)} live jobs! Total jobs in database: {len(all_jobs)}[/bold green]")
41
 
42
  @cli.command()
43
  @click.option("--transport", default="stdio", type=click.Choice(["stdio", "sse"]), help="MCP transport protocol")
tech_radar/scrapers/live_scraper.py CHANGED
@@ -1,11 +1,20 @@
1
  import requests
2
  from bs4 import BeautifulSoup
3
  import re
4
- from typing import Optional, Dict, Any, List
 
 
 
 
5
  from tech_radar.db.models import JobPosting
6
 
7
- class LiveJobScraper:
8
- """Extensible Web Scraper Engine for extracting Tech & Software Engineering jobs."""
 
 
 
 
 
9
 
10
  def __init__(self):
11
  self.headers = {
@@ -16,50 +25,248 @@ class LiveJobScraper:
16
  )
17
  }
18
 
19
- def scrape_url(self, url: str) -> Optional[JobPosting]:
 
 
 
 
 
20
  try:
21
- resp = requests.get(url, headers=self.headers, timeout=10)
22
- if resp.status_code != 200:
23
- return None
24
-
25
- soup = BeautifulSoup(resp.text, "html.parser")
26
- title = soup.find("h1") or soup.find("title")
27
- title_text = title.get_text(strip=True) if title else "Senior Software Engineer"
28
-
29
- body_text = soup.get_text()
30
- extracted_stack = self.extract_tech_keywords(body_text)
31
-
32
- return JobPosting(
33
- id=f"LIVE-SCRAPE-{abs(hash(url)) % 10000}",
34
- title=title_text[:60],
35
- company="Tech Enterprise",
36
- tech_domain="Software Engineering",
37
- city="Bengaluru",
38
- area="Indiranagar",
39
- salary_min_lpa=25.0,
40
- salary_max_lpa=45.0,
41
- experience_min_years=3,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  experience_max_years=7,
43
- tech_stack=extracted_stack or ["Python", "Docker", "PostgreSQL"],
44
- requirements=body_text[:1000],
45
- work_mode="Hybrid",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  company_tier="Tech Firm",
47
- url=url
48
- )
49
- except Exception as e:
50
- print(f"[LiveJobScraper] Error scraping {url}: {e}")
51
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def extract_tech_keywords(self, text: str) -> List[str]:
54
- known_keywords = [
55
  "Go", "Java", "Python", "TypeScript", "React", "Next.js", "Node.js",
56
  "FastAPI", "Spring Boot", "Docker", "Kubernetes", "AWS", "Terraform",
57
  "PostgreSQL", "Redis", "Kafka", "Apache Spark", "Snowflake",
58
  "FastMCP", "MCP", "LangGraph", "PyTorch", "CUDA", "vLLM", "Qdrant",
59
- "Kotlin", "Flutter", "Swift"
60
  ]
61
  found = []
62
- for kw in known_keywords:
63
  if re.search(r'\b' + re.escape(kw) + r'\b', text, re.IGNORECASE):
64
  found.append(kw)
65
- return found
 
 
 
 
 
1
  import requests
2
  from bs4 import BeautifulSoup
3
  import re
4
+ import json
5
+ import warnings
6
+ from typing import List, Dict, Any, Optional
7
+ from datetime import datetime
8
+
9
  from tech_radar.db.models import JobPosting
10
 
11
+ warnings.filterwarnings("ignore")
12
+
13
+ class LiveInternetScraper:
14
+ """
15
+ Autonomous Live Internet Job Scraper Engine for TechRadar-MCP.
16
+ Aggregates live, real-time tech postings from RemoteOK, Jobicy, and WeWorkRemotely feeds.
17
+ """
18
 
19
  def __init__(self):
20
  self.headers = {
 
25
  )
26
  }
27
 
28
+ def fetch_all_live_jobs(self) -> List[JobPosting]:
29
+ """Fetch real-time live jobs from all active internet APIs & feeds."""
30
+ live_jobs: List[JobPosting] = []
31
+
32
+ # 1. Fetch RemoteOK Live API
33
+ print("[LiveScraper] Ingesting live jobs from RemoteOK API...")
34
  try:
35
+ remote_ok_jobs = self._scrape_remoteok()
36
+ live_jobs.extend(remote_ok_jobs)
37
+ print(f" -> Fetched {len(remote_ok_jobs)} live jobs from RemoteOK.")
38
+ except Exception as e:
39
+ print(f"[LiveScraper] RemoteOK error: {e}")
40
+
41
+ # 2. Fetch Jobicy Live API
42
+ print("[LiveScraper] Ingesting live jobs from Jobicy API...")
43
+ try:
44
+ jobicy_jobs = self._scrape_jobicy()
45
+ live_jobs.extend(jobicy_jobs)
46
+ print(f" -> Fetched {len(jobicy_jobs)} live jobs from Jobicy.")
47
+ except Exception as e:
48
+ print(f"[LiveScraper] Jobicy error: {e}")
49
+
50
+ # 3. Fetch WeWorkRemotely RSS
51
+ print("[LiveScraper] Ingesting live jobs from WeWorkRemotely RSS...")
52
+ try:
53
+ wwr_jobs = self._scrape_weworkremotely()
54
+ live_jobs.extend(wwr_jobs)
55
+ print(f" -> Fetched {len(wwr_jobs)} live jobs from WeWorkRemotely.")
56
+ except Exception as e:
57
+ print(f"[LiveScraper] WeWorkRemotely error: {e}")
58
+
59
+ print(f"✨ Total Live Internet Jobs Ingested: {len(live_jobs)}")
60
+ return live_jobs
61
+
62
+ def _scrape_remoteok(self) -> List[JobPosting]:
63
+ url = "https://remoteok.com/api"
64
+ resp = requests.get(url, headers=self.headers, timeout=12)
65
+ if resp.status_code != 200:
66
+ return []
67
+
68
+ raw_data = resp.json()
69
+ jobs = []
70
+
71
+ # RemoteOK first item is metadata
72
+ items = raw_data[1:] if isinstance(raw_data, list) and len(raw_data) > 1 else []
73
+
74
+ for item in items:
75
+ if not isinstance(item, dict) or "position" not in item:
76
+ continue
77
+
78
+ title = item.get("position", "Software Engineer")
79
+ company = item.get("company", "Tech Company")
80
+ tags = item.get("tags", [])
81
+ location = item.get("location", "Remote")
82
+ desc = item.get("description", title)
83
+ job_url = item.get("url", "https://remoteok.com")
84
+ raw_id = item.get("id", str(abs(hash(title + company)) % 100000))
85
+
86
+ domain = self.detect_domain(title, tags)
87
+ city = self.detect_city(location)
88
+ salary_min, salary_max = self.extract_salary_lpa(item.get("salary_min"), item.get("salary_max"), desc)
89
+
90
+ jobs.append(JobPosting(
91
+ id=f"LIVE-ROK-{raw_id}",
92
+ title=title[:70],
93
+ company=company[:50],
94
+ tech_domain=domain,
95
+ city=city,
96
+ area=location[:30] if location else "Remote Hub",
97
+ salary_min_lpa=salary_min,
98
+ salary_max_lpa=salary_max,
99
+ experience_min_years=2,
100
  experience_max_years=7,
101
+ tech_stack=tags[:8] if tags else ["Python", "Docker", "API"],
102
+ requirements=self.clean_html(desc)[:1000],
103
+ work_mode="Remote" if "remote" in location.lower() or not location else "Hybrid",
104
+ company_tier="Global Remote / Tech Enterprise",
105
+ posted_date=datetime.now().strftime("%Y-%m-%d"),
106
+ url=job_url
107
+ ))
108
+
109
+ return jobs
110
+
111
+ def _scrape_jobicy(self) -> List[JobPosting]:
112
+ url = "https://jobicy.com/api/v2/remote-jobs"
113
+ resp = requests.get(url, headers=self.headers, timeout=12)
114
+ if resp.status_code != 200:
115
+ return []
116
+
117
+ raw_data = resp.json().get("jobs", [])
118
+ jobs = []
119
+
120
+ for item in raw_data:
121
+ title = item.get("jobTitle", "Software Engineer")
122
+ company = item.get("companyName", "Tech Firm")
123
+ geo = item.get("jobGeo", "Remote")
124
+ desc = item.get("jobDescription", title)
125
+ job_url = item.get("url", "https://jobicy.com")
126
+ raw_id = item.get("id", str(abs(hash(title + company)) % 100000))
127
+
128
+ domain = self.detect_domain(title, [item.get("jobCategory", "")])
129
+ city = self.detect_city(geo)
130
+ salary_min, salary_max = self.extract_salary_lpa(None, None, desc)
131
+
132
+ jobs.append(JobPosting(
133
+ id=f"LIVE-JBC-{raw_id}",
134
+ title=title[:70],
135
+ company=company[:50],
136
+ tech_domain=domain,
137
+ city=city,
138
+ area=geo[:30] if geo else "Global Remote",
139
+ salary_min_lpa=salary_min,
140
+ salary_max_lpa=salary_max,
141
+ experience_min_years=3,
142
+ experience_max_years=8,
143
+ tech_stack=self.extract_tech_keywords(title + " " + desc)[:7],
144
+ requirements=self.clean_html(desc)[:1000],
145
+ work_mode="Remote",
146
  company_tier="Tech Firm",
147
+ posted_date=datetime.now().strftime("%Y-%m-%d"),
148
+ url=job_url
149
+ ))
150
+
151
+ return jobs
152
+
153
+ def _scrape_weworkremotely(self) -> List[JobPosting]:
154
+ url = "https://weworkremotely.com/categories/remote-programming-jobs.rss"
155
+ resp = requests.get(url, headers=self.headers, timeout=12)
156
+ if resp.status_code != 200:
157
+ return []
158
+
159
+ soup = BeautifulSoup(resp.text, "html.parser")
160
+ items = soup.find_all("item")
161
+ jobs = []
162
+
163
+ for idx, item in enumerate(items):
164
+ title_node = item.find("title")
165
+ link_node = item.find("link")
166
+ desc_node = item.find("description")
167
+
168
+ full_title = title_node.get_text() if title_node else "Senior Engineer"
169
+ job_url = link_node.get_text() if link_node else "https://weworkremotely.com"
170
+ desc = desc_node.get_text() if desc_node else full_title
171
+
172
+ parts = full_title.split(":")
173
+ if len(parts) > 1:
174
+ company = parts[0].strip()
175
+ title = parts[1].strip()
176
+ else:
177
+ company = "WeWorkRemotely Tech"
178
+ title = full_title
179
+
180
+ domain = self.detect_domain(title, [])
181
+ salary_min, salary_max = self.extract_salary_lpa(None, None, desc)
182
+
183
+ jobs.append(JobPosting(
184
+ id=f"LIVE-WWR-{idx + 100}",
185
+ title=title[:70],
186
+ company=company[:50],
187
+ tech_domain=domain,
188
+ city="Remote",
189
+ area="Global Remote Hub",
190
+ salary_min_lpa=salary_min,
191
+ salary_max_lpa=salary_max,
192
+ experience_min_years=3,
193
+ experience_max_years=8,
194
+ tech_stack=self.extract_tech_keywords(title + " " + desc)[:7],
195
+ requirements=self.clean_html(desc)[:1000],
196
+ work_mode="Remote",
197
+ company_tier="Product Tech Leader",
198
+ posted_date=datetime.now().strftime("%Y-%m-%d"),
199
+ url=job_url
200
+ ))
201
+
202
+ return jobs
203
+
204
+ def detect_domain(self, title: str, tags: List[str]) -> str:
205
+ text = (title + " " + " ".join(tags)).lower()
206
+ if any(w in text for w in ["backend", "go", "java", "spring", "microservice", "python"]):
207
+ return "Backend Engineering"
208
+ elif any(w in text for w in ["frontend", "react", "next.js", "vue", "typescript", "ui"]):
209
+ return "Frontend Engineering"
210
+ elif any(w in text for w in ["full stack", "fullstack", "full-stack"]):
211
+ return "Full Stack Engineering"
212
+ elif any(w in text for w in ["devops", "cloud", "aws", "kubernetes", "k8s", "terraform", "sre"]):
213
+ return "Cloud & DevOps"
214
+ elif any(w in text for w in ["data", "spark", "snowflake", "kafka", "pipeline", "sql"]):
215
+ return "Data Engineering"
216
+ elif any(w in text for w in ["ai", "genai", "llm", "machine learning", "pytorch", "mcp", "cuda"]):
217
+ return "AI/ML & GenAI"
218
+ elif any(w in text for w in ["android", "ios", "flutter", "kotlin", "mobile", "swift"]):
219
+ return "Mobile Engineering"
220
+ return "Software Engineering"
221
+
222
+ def detect_city(self, location: str) -> str:
223
+ loc = (location or "").lower()
224
+ if "bengaluru" in loc or "bangalore" in loc:
225
+ return "Bengaluru"
226
+ elif "pune" in loc:
227
+ return "Pune"
228
+ elif "hyderabad" in loc:
229
+ return "Hyderabad"
230
+ elif "gurgaon" in loc or "delhi" in loc or "ncr" in loc:
231
+ return "Gurgaon"
232
+ elif "mumbai" in loc:
233
+ return "Mumbai"
234
+ elif "chennai" in loc:
235
+ return "Chennai"
236
+ return "Remote"
237
+
238
+ def extract_salary_lpa(self, sal_min: Optional[float], sal_max: Optional[float], text: str) -> tuple[float, float]:
239
+ if sal_min and sal_max and sal_min > 1000:
240
+ # Convert USD to INR LPA (e.g. $100K = ~85 LPA)
241
+ min_lpa = round((sal_min * 83.5) / 100000.0, 1)
242
+ max_lpa = round((sal_max * 83.5) / 100000.0, 1)
243
+ return max(18.0, min_lpa), max(28.0, max_lpa)
244
+
245
+ # Regex search for salary numbers in description
246
+ match = re.search(r'\$(\d{2,3})k?\s*-\s*\$?(\d{2,3})k', text, re.IGNORECASE)
247
+ if match:
248
+ s1 = float(match.group(1)) * 1000
249
+ s2 = float(match.group(2)) * 1000
250
+ min_lpa = round((s1 * 83.5) / 100000.0, 1)
251
+ max_lpa = round((s2 * 83.5) / 100000.0, 1)
252
+ return max(20.0, min_lpa), max(32.0, max_lpa)
253
+
254
+ return 26.0, 44.0
255
 
256
  def extract_tech_keywords(self, text: str) -> List[str]:
257
+ known = [
258
  "Go", "Java", "Python", "TypeScript", "React", "Next.js", "Node.js",
259
  "FastAPI", "Spring Boot", "Docker", "Kubernetes", "AWS", "Terraform",
260
  "PostgreSQL", "Redis", "Kafka", "Apache Spark", "Snowflake",
261
  "FastMCP", "MCP", "LangGraph", "PyTorch", "CUDA", "vLLM", "Qdrant",
262
+ "Kotlin", "Flutter", "Swift", "GraphQL", "gRPC"
263
  ]
264
  found = []
265
+ for kw in known:
266
  if re.search(r'\b' + re.escape(kw) + r'\b', text, re.IGNORECASE):
267
  found.append(kw)
268
+ return found or ["Python", "Docker", "REST API"]
269
+
270
+ def clean_html(self, raw_html: str) -> str:
271
+ soup = BeautifulSoup(raw_html, "html.parser")
272
+ return soup.get_text(separator=" ", strip=True)
tech_radar/scrapers/seeder.py CHANGED
@@ -1,23 +1,36 @@
1
  from tech_radar.db.database import DatabaseManager
2
  from tech_radar.db.vector_store import SemanticVectorStore
3
  from tech_radar.scrapers.mock_data import UNIVERSAL_TECH_JOBS
 
4
 
5
- def seed_database(db_path: str = "tech_radar.db") -> tuple[DatabaseManager, SemanticVectorStore]:
6
- """Seed SQLite database and vector store with universal tech jobs across India & Remote."""
7
  db = DatabaseManager(db_path=db_path)
8
  vector_store = SemanticVectorStore()
9
 
10
  count = 0
 
11
  for job in UNIVERSAL_TECH_JOBS:
12
  db.save_job_posting(job)
13
  count += 1
14
 
 
 
 
 
 
 
 
 
 
 
 
15
  all_jobs = db.get_all_jobs()
16
  vector_store.index_jobs(all_jobs)
17
 
18
- print(f" Successfully seeded {count} tech job postings into {db_path}.")
19
  print(f" Indexed {len(all_jobs)} jobs across all domains in Vector Engine.")
20
  return db, vector_store
21
 
22
  if __name__ == "__main__":
23
- seed_database()
 
1
  from tech_radar.db.database import DatabaseManager
2
  from tech_radar.db.vector_store import SemanticVectorStore
3
  from tech_radar.scrapers.mock_data import UNIVERSAL_TECH_JOBS
4
+ from tech_radar.scrapers.live_scraper import LiveInternetScraper
5
 
6
+ def seed_database(db_path: str = "tech_radar.db", fetch_live: bool = True) -> tuple[DatabaseManager, SemanticVectorStore]:
7
+ """Seed SQLite database and vector store with curated + live real-time internet job postings."""
8
  db = DatabaseManager(db_path=db_path)
9
  vector_store = SemanticVectorStore()
10
 
11
  count = 0
12
+ # 1. Seed base curated tech jobs
13
  for job in UNIVERSAL_TECH_JOBS:
14
  db.save_job_posting(job)
15
  count += 1
16
 
17
+ # 2. Fetch live real-time internet jobs
18
+ if fetch_live:
19
+ try:
20
+ scraper = LiveInternetScraper()
21
+ live_jobs = scraper.fetch_all_live_jobs()
22
+ for l_job in live_jobs:
23
+ db.save_job_posting(l_job)
24
+ count += 1
25
+ except Exception as e:
26
+ print(f"[Seeder] Live internet scraping warning: {e}")
27
+
28
  all_jobs = db.get_all_jobs()
29
  vector_store.index_jobs(all_jobs)
30
 
31
+ print(f" Successfully seeded {count} total job postings into {db_path}.")
32
  print(f" Indexed {len(all_jobs)} jobs across all domains in Vector Engine.")
33
  return db, vector_store
34
 
35
  if __name__ == "__main__":
36
+ seed_database(fetch_live=True)
tech_radar/tech_radar/__init__.py DELETED
@@ -1,6 +0,0 @@
1
- """
2
- TechRadar-MCP: Universal Tech Hiring Intelligence & Model Context Protocol Ecosystem
3
- """
4
-
5
- __version__ = "1.0.0"
6
- __author__ = "Tech Radar Team"
 
 
 
 
 
 
 
tech_radar/tech_radar/agents/evaluator_agent.py DELETED
@@ -1,160 +0,0 @@
1
- from typing import List, Dict, Any
2
- from tech_radar.db.models import JobPosting, SkillGapReport, ResumePatch, InterviewPrepKit, InterviewQuestion
3
- from tech_radar.agents.skill_extractor import SkillExtractorAgent
4
-
5
- class EvaluatorAgent:
6
- """Evaluates candidate profiles against target tech job descriptions, producing ATS patches & interview prep."""
7
-
8
- def __init__(self):
9
- self.skill_extractor = SkillExtractorAgent()
10
-
11
- def evaluate_skill_gap(self, resume_text: str, candidate_skills: List[str], job: JobPosting) -> SkillGapReport:
12
- resume_lower = resume_text.lower()
13
- matched = []
14
- missing = []
15
-
16
- for req_skill in job.tech_stack:
17
- if any(req_skill.lower() == s.lower() for s in candidate_skills) or req_skill.lower() in resume_lower:
18
- matched.append(req_skill)
19
- else:
20
- missing.append(req_skill)
21
-
22
- total_req = len(job.tech_stack) or 1
23
- match_pct = round((len(matched) / total_req) * 100, 1)
24
-
25
- high_priority = missing[:3]
26
- action_plan = self.skill_extractor.generate_learning_roadmap(missing)
27
-
28
- return SkillGapReport(
29
- target_job_id=job.id,
30
- job_title=job.title,
31
- company=job.company,
32
- city=job.city,
33
- tech_domain=job.tech_domain,
34
- match_percentage=match_pct,
35
- matched_skills=matched,
36
- missing_skills=missing,
37
- high_priority_gaps=high_priority,
38
- recommended_action_plan=action_plan,
39
- estimated_learning_hours=len(missing) * 6
40
- )
41
-
42
- def generate_resume_patch(self, resume_text: str, job: JobPosting) -> ResumePatch:
43
- diffs = []
44
- added_keywords = []
45
-
46
- for skill in job.tech_stack:
47
- if skill.lower() not in resume_text.lower():
48
- added_keywords.append(skill)
49
-
50
- domain = job.tech_domain.lower()
51
-
52
- if "backend" in domain or "go" in [s.lower() for s in job.tech_stack]:
53
- diffs.append({
54
- "original": "Wrote backend APIs and managed database queries.",
55
- "tailored": f"Engineered high-concurrency microservices in {job.tech_stack[0] if job.tech_stack else 'Go'}, optimizing low-latency data pipelines at {job.company}.",
56
- "rationale": f"Highlights core backend technology ({job.tech_stack[0] if job.tech_stack else 'Go'}) requested in JD."
57
- })
58
-
59
- if "cloud" in domain or "devops" in domain or "kubernetes" in [s.lower() for s in job.tech_stack]:
60
- diffs.append({
61
- "original": "Managed cloud deployments and Docker scripts.",
62
- "tailored": "Automated multi-region Kubernetes cluster deployment using Terraform and GitOps (ArgoCD), reducing deployment cycle time by 45%.",
63
- "rationale": "Emphasizes infrastructure automation & GitOps patterns."
64
- })
65
-
66
- if "frontend" in domain or "react" in [s.lower() for s in job.tech_stack]:
67
- diffs.append({
68
- "original": "Built UI components in React.",
69
- "tailored": "Architected performant React/Next.js design system components with server-side rendering (SSR), improving Core Web Vitals score to 95+.",
70
- "rationale": "Showcases frontend performance metrics and SSR."
71
- })
72
-
73
- if not diffs:
74
- diffs.append({
75
- "original": "Worked on software development tasks and team deliverables.",
76
- "tailored": f"Architected scalable solution utilizing {', '.join(job.tech_stack[:3])} to deliver high-availability software features at {job.company}.",
77
- "rationale": "Injects critical tech stack keywords into resume."
78
- })
79
-
80
- ats_score = min(98.0, 72.0 + (len(added_keywords) * 3))
81
-
82
- return ResumePatch(
83
- target_job_id=job.id,
84
- job_title=job.title,
85
- company=job.company,
86
- ats_compatibility_score=ats_score,
87
- tailored_bullets=diffs,
88
- added_keywords=added_keywords
89
- )
90
-
91
- def generate_interview_prep(self, job: JobPosting) -> InterviewPrepKit:
92
- questions = []
93
- domain = job.tech_domain.lower()
94
-
95
- if "backend" in domain or "go" in [s.lower() for s in job.tech_stack]:
96
- questions.append(InterviewQuestion(
97
- question="How do you handle memory allocation and garbage collection tuning in high-concurrency Go / Java microservices?",
98
- category="Backend & Systems",
99
- difficulty="Hard",
100
- ideal_answer_points=[
101
- "Explain stack vs heap allocation and escape analysis.",
102
- "Discuss sync.Pool for buffer reuse to minimize GC pauses under high QPS."
103
- ],
104
- company_context=f"Commonly evaluated at {job.company} ({job.city})."
105
- ))
106
-
107
- if "cloud" in domain or "devops" in domain:
108
- questions.append(InterviewQuestion(
109
- question="Explain zero-downtime blue/green deployment strategy in Kubernetes using ArgoCD / Helm.",
110
- category="Cloud & DevOps",
111
- difficulty="Intermediate",
112
- ideal_answer_points=[
113
- "Use ingress controller routing rules to switch traffic between blue and green deployments.",
114
- "Automate health check probes and automated rollback on 5xx spike."
115
- ]
116
- ))
117
-
118
- if "frontend" in domain:
119
- questions.append(InterviewQuestion(
120
- question="What is the difference between React Server Components (RSC) and traditional Client Side Rendering (CSR)?",
121
- category="Frontend Engineering",
122
- difficulty="Intermediate",
123
- ideal_answer_points=[
124
- "RSC renders components on the server without sending JS bundle code to client.",
125
- "Reduces bundle size and improves initial page load (LCP)."
126
- ]
127
- ))
128
-
129
- questions.append(InterviewQuestion(
130
- question=f"Design a scalable system for {job.company} handling 50,000 requests/second using {', '.join(job.tech_stack[:3])}.",
131
- category="System Design",
132
- difficulty="Hard",
133
- ideal_answer_points=[
134
- "Implement API Gateway with rate limiting & OAuth token validation.",
135
- "Partition microservices with message queues (Kafka) for asynchronous processing.",
136
- "Use distributed caching (Redis) for database load reduction."
137
- ]
138
- ))
139
-
140
- sys_design = {
141
- "title": f"High-Scale System Architecture Challenge for {job.company} ({job.city})",
142
- "domain": job.tech_domain,
143
- "target_scale": "50K+ QPS",
144
- "recommended_stack": f"{', '.join(job.tech_stack)}"
145
- }
146
-
147
- return InterviewPrepKit(
148
- job_id=job.id,
149
- job_title=job.title,
150
- company=job.company,
151
- city=job.city,
152
- tech_domain=job.tech_domain,
153
- technical_questions=questions,
154
- system_design_challenge=sys_design,
155
- prep_tips=[
156
- f"Review {job.company}'s engineering blog and stack ({', '.join(job.tech_stack)}).",
157
- "Prepare 2 system design diagrams detailing microservice boundaries & caching strategy.",
158
- "Be ready to live-code algorithmic problem solving & concurrency patterns."
159
- ]
160
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/agents/market_analyst.py DELETED
@@ -1,42 +0,0 @@
1
- from typing import Dict, Any, List
2
- from tech_radar.db.database import DatabaseManager
3
- from tech_radar.db.models import CityMarketInsights
4
-
5
- class MarketAnalystAgent:
6
- """Generates universal market trend analytics across tech domains & cities."""
7
-
8
- def __init__(self, db: DatabaseManager):
9
- self.db = db
10
-
11
- def generate_market_report(self, city: str = "All", domain: str = "All") -> CityMarketInsights:
12
- data = self.db.get_market_analytics(city=city, domain=domain)
13
-
14
- return CityMarketInsights(
15
- city=data["city"],
16
- tech_domain=data["domain"],
17
- total_active_jobs=data["total_jobs"],
18
- avg_salary_lpa=data["avg_salary_lpa"],
19
- salary_range=data["salary_range"],
20
- top_demanded_frameworks=data["top_frameworks"],
21
- top_hiring_hubs=data["top_hubs"],
22
- top_employers=data["top_companies"],
23
- growth_trend=f"🚀 Tech hiring for {domain} in {city} shows robust growth with competitive salary packages."
24
- )
25
-
26
- def generate_markdown_summary(self, city: str = "All", domain: str = "All") -> str:
27
- insights = self.generate_market_report(city=city, domain=domain)
28
-
29
- md = f"# 📊 Tech Hiring Market Report — {insights.city.upper()} ({insights.tech_domain.upper()})\n\n"
30
- md += f"**Total Active Roles Tracked**: {insights.total_active_jobs}\n"
31
- md += f"**Average Salary Band**: {insights.avg_salary_lpa} LPA (Range: {insights.salary_range})\n"
32
- md += f"**Market Sentiment**: {insights.growth_trend}\n\n"
33
-
34
- md += "## 🔥 Top Demanded Tech Skills & Frameworks\n"
35
- for item in insights.top_demanded_frameworks:
36
- md += f"- **{item['skill']}**: Present in `{item['percentage']}%` of JDs ({item['count']} roles)\n"
37
-
38
- md += "\n## 🏢 Leading Employers Hiring Tech Talent\n"
39
- for company in insights.top_employers:
40
- md += f"- {company}\n"
41
-
42
- return md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/agents/skill_extractor.py DELETED
@@ -1,70 +0,0 @@
1
- from typing import List, Dict, Any
2
-
3
- class SkillExtractorAgent:
4
- """Categorizes tech skills into universal engineering taxonomy buckets."""
5
-
6
- SKILL_TAXONOMY = {
7
- "Backend & Microservices": ["Go", "Java", "Python", "C++", "Spring Boot", "FastAPI", "gRPC", "Node.js"],
8
- "Frontend & UI": ["React", "Next.js", "TypeScript", "TailwindCSS", "Vue", "Zustand", "GraphQL", "HTML/CSS"],
9
- "Cloud, DevOps & Infra": ["AWS", "Kubernetes", "Docker", "Terraform", "Helm", "ArgoCD", "CI/CD", "Prometheus"],
10
- "Data & Databases": ["PostgreSQL", "Apache Kafka", "Apache Spark", "Snowflake", "Cassandra", "Redis", "Airflow", "SQL"],
11
- "AI/ML & GenAI": ["FastMCP", "MCP", "LangGraph", "PyTorch", "vLLM", "CUDA", "Unsloth", "Qdrant", "DeepSpeed"],
12
- "Mobile & Client": ["Kotlin", "Jetpack Compose", "Flutter", "Android SDK", "Swift", "iOS SDK"]
13
- }
14
-
15
- def categorize_skills(self, skills: List[str]) -> Dict[str, List[str]]:
16
- result = {category: [] for category in self.SKILL_TAXONOMY}
17
- result["Other Tech"] = []
18
-
19
- for skill in skills:
20
- matched = False
21
- for category, taxonomy_skills in self.SKILL_TAXONOMY.items():
22
- if any(skill.lower() == t.lower() for t in taxonomy_skills):
23
- result[category].append(skill)
24
- matched = True
25
- break
26
- if not matched:
27
- result["Other Tech"].append(skill)
28
-
29
- return {k: v for k, v in result.items() if v}
30
-
31
- def generate_learning_roadmap(self, missing_skills: List[str]) -> List[Dict[str, str]]:
32
- roadmap = []
33
- for skill in missing_skills:
34
- s_lower = skill.lower()
35
- if s_lower in ["go", "grpc"]:
36
- roadmap.append({
37
- "skill": skill,
38
- "estimated_hours": "8 Hours",
39
- "micro_project": "Build a high-concurrency microservice in Go using gRPC, Protocol Buffers, and worker pools.",
40
- "resource": "https://go.dev/doc/tutorial/"
41
- })
42
- elif s_lower in ["kubernetes", "docker", "terraform"]:
43
- roadmap.append({
44
- "skill": skill,
45
- "estimated_hours": "10 Hours",
46
- "micro_project": "Provision an EKS/GKE cluster with Terraform, configure Helm charts, and set up GitOps deployment.",
47
- "resource": "https://kubernetes.io/docs/tutorials/"
48
- })
49
- elif s_lower in ["react", "next.js", "typescript"]:
50
- roadmap.append({
51
- "skill": skill,
52
- "estimated_hours": "6 Hours",
53
- "micro_project": "Create a modern Next.js 14 app featuring App Router, Server Components, and TypeScript state management.",
54
- "resource": "https://nextjs.org/docs"
55
- })
56
- elif s_lower in ["fastmcp", "mcp"]:
57
- roadmap.append({
58
- "skill": skill,
59
- "estimated_hours": "6 Hours",
60
- "micro_project": "Build a custom Python FastMCP server exposing stdio/SSE tools for AI agents.",
61
- "resource": "https://modelcontextprotocol.io"
62
- })
63
- else:
64
- roadmap.append({
65
- "skill": skill,
66
- "estimated_hours": "5 Hours",
67
- "micro_project": f"Build a practical micro-project integrating {skill} into a production pipeline.",
68
- "resource": "https://github.com/topics/software-engineering"
69
- })
70
- return roadmap
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/cli.py DELETED
@@ -1,108 +0,0 @@
1
- import click
2
- import sys
3
- import subprocess
4
- import os
5
- from rich.console import Console
6
- from rich.panel import Panel
7
-
8
- console = Console()
9
-
10
- @click.group()
11
- def cli():
12
- """TechRadar MCP — Universal Tech Hiring Intelligence & MCP Ecosystem across India & Remote."""
13
- pass
14
-
15
- @cli.command()
16
- @click.option("--live/--no-live", default=True, help="Fetch real-time live internet jobs from RemoteOK, Jobicy, WWR")
17
- def seed(live):
18
- """Seed SQLite database & Vector Engine with curated + live internet job postings."""
19
- console.print(f"[bold green]🌱 Seeding TechRadar Database (Live Scraping: {live})...[/bold green]")
20
- from tech_radar.scrapers.seeder import seed_database
21
- seed_database(fetch_live=live)
22
-
23
- @cli.command()
24
- def scrape_live():
25
- """Trigger real-time live internet job scraping pipeline."""
26
- console.print("[bold cyan]🌐 Executing Live Internet Job Scraper Pipeline...[/bold cyan]")
27
- from tech_radar.scrapers.live_scraper import LiveInternetScraper
28
- from tech_radar.db.database import DatabaseManager
29
- from tech_radar.db.vector_store import SemanticVectorStore
30
-
31
- scraper = LiveInternetScraper()
32
- live_jobs = scraper.fetch_all_live_jobs()
33
- db = DatabaseManager()
34
- for job in live_jobs:
35
- db.save_job_posting(job)
36
-
37
- vector_store = SemanticVectorStore()
38
- all_jobs = db.get_all_jobs()
39
- vector_store.index_jobs(all_jobs)
40
- console.print(f"[bold green]✅ Ingested {len(live_jobs)} live jobs! Total jobs in database: {len(all_jobs)}[/bold green]")
41
-
42
- @cli.command()
43
- @click.option("--transport", default="stdio", type=click.Choice(["stdio", "sse"]), help="MCP transport protocol")
44
- def serve(transport):
45
- """Start the FastMCP Protocol Server for Claude Desktop or Cursor IDE."""
46
- console.print(Panel.fit(
47
- f"[bold magenta]⚡ Starting TechRadar FastMCP Server ({transport} mode)...[/bold magenta]\n"
48
- "[dim]Exposing tools: search_tech_jobs, analyze_skill_gap, generate_tailored_resume_patch, get_market_insights, generate_interview_prep_kit[/dim]",
49
- title="TechRadar MCP"
50
- ))
51
- from tech_radar.mcp.server import mcp
52
- if transport == "stdio":
53
- mcp.run(transport="stdio")
54
- else:
55
- mcp.run(transport="sse")
56
-
57
- @cli.command()
58
- @click.option("--port", default=8000, help="Port to run 3D Web App frontend on")
59
- def web(port):
60
- """Launch the 3D Cyber-Glassmorphic Web App frontend."""
61
- console.print(f"[bold cyan]🌐 Launching 3D Cyber-Glassmorphic TechRadar Web App on http://localhost:{port}...[/bold cyan]")
62
- import uvicorn
63
- uvicorn.run("tech_radar.ui.static_server:app", host="0.0.0.0", port=port, reload=True)
64
-
65
- @cli.command()
66
- @click.option("--port", default=8501, help="Port to run Streamlit dashboard on")
67
- def ui(port):
68
- """Launch the Interactive Streamlit Dashboard."""
69
- console.print(f"[bold cyan]🚀 Launching TechRadar Streamlit Dashboard on http://localhost:{port}...[/bold cyan]")
70
- app_path = os.path.join(os.path.dirname(__file__), "ui", "app.py")
71
- subprocess.run([sys.executable, "-m", "streamlit", "run", app_path, "--server.port", str(port)])
72
-
73
- @cli.command()
74
- def test_mcp():
75
- """Run automated end-to-end verification of all FastMCP tools."""
76
- console.print("[bold yellow]🧪 Running Automated FastMCP Server Verification...[/bold yellow]")
77
- from tech_radar.mcp.tools import (
78
- tool_search_tech_jobs,
79
- tool_analyze_skill_gap,
80
- tool_generate_resume_patch,
81
- tool_get_market_insights,
82
- tool_generate_interview_prep_kit
83
- )
84
-
85
- console.print("\n1. Testing 'search_tech_jobs' (Backend in Bengaluru)...")
86
- res1 = tool_search_tech_jobs(domain="Backend Engineering", city="Bengaluru", query="Go")
87
- console.print(f" Success! Retreived response size: {len(res1)} bytes")
88
-
89
- console.print("\n2. Testing 'get_market_insights' (Cloud in Hyderabad)...")
90
- res2 = tool_get_market_insights(domain="Cloud & DevOps", city="Hyderabad")
91
- console.print(f" Success! Retreived response size: {len(res2)} bytes")
92
-
93
- console.print("\n3. Testing 'analyze_skill_gap' (BLR-BACKEND-101)...")
94
- res3 = tool_analyze_skill_gap(resume_text="Go & Docker developer", target_job_id="BLR-BACKEND-101")
95
- console.print(f" Success! Retreived response size: {len(res3)} bytes")
96
-
97
- console.print("\n4. Testing 'generate_tailored_resume_patch' (PUNE-FULLSTACK-202)...")
98
- res4 = tool_generate_resume_patch(resume_text="Python React dev", target_job_id="PUNE-FULLSTACK-202")
99
- console.print(f" Success! Retreived response size: {len(res4)} bytes")
100
-
101
- console.print("\n5. Testing 'generate_interview_prep_kit' (BLR-FRONTEND-201)...")
102
- res5 = tool_generate_interview_prep_kit(target_job_id="BLR-FRONTEND-201")
103
- console.print(f" Success! Retreived response size: {len(res5)} bytes")
104
-
105
- console.print("\n[bold green]✅ All 5 FastMCP Tools verified successfully![/bold green]")
106
-
107
- if __name__ == "__main__":
108
- cli()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/db/database.py DELETED
@@ -1,218 +0,0 @@
1
- import sqlite3
2
- import json
3
- import os
4
- from typing import List, Optional, Dict, Any
5
- from tech_radar.db.models import JobPosting
6
-
7
- class DatabaseManager:
8
- """Thread-safe SQLite database manager for TechRadar-MCP across all domains & cities."""
9
-
10
- def __init__(self, db_path: str = "tech_radar.db"):
11
- self.db_path = db_path
12
- self._init_db()
13
-
14
- def _get_connection(self) -> sqlite3.Connection:
15
- conn = sqlite3.connect(self.db_path)
16
- conn.row_factory = sqlite3.Row
17
- return conn
18
-
19
- def _init_db(self):
20
- with self._get_connection() as conn:
21
- cursor = conn.cursor()
22
- cursor.execute("""
23
- CREATE TABLE IF NOT EXISTS job_postings (
24
- id TEXT PRIMARY KEY,
25
- title TEXT NOT NULL,
26
- company TEXT NOT NULL,
27
- tech_domain TEXT NOT NULL,
28
- city TEXT NOT NULL,
29
- area TEXT NOT NULL,
30
- salary_min_lpa REAL NOT NULL,
31
- salary_max_lpa REAL NOT NULL,
32
- experience_min_years INTEGER NOT NULL,
33
- experience_max_years INTEGER NOT NULL,
34
- tech_stack TEXT NOT NULL, -- JSON List
35
- requirements TEXT NOT NULL,
36
- work_mode TEXT NOT NULL,
37
- company_tier TEXT NOT NULL,
38
- posted_date TEXT NOT NULL,
39
- url TEXT
40
- )
41
- """)
42
- conn.commit()
43
-
44
- def save_job_posting(self, job: JobPosting) -> bool:
45
- with self._get_connection() as conn:
46
- cursor = conn.cursor()
47
- cursor.execute("""
48
- INSERT OR REPLACE INTO job_postings (
49
- id, title, company, tech_domain, city, area, salary_min_lpa, salary_max_lpa,
50
- experience_min_years, experience_max_years, tech_stack, requirements,
51
- work_mode, company_tier, posted_date, url
52
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
- """, (
54
- job.id, job.title, job.company, job.tech_domain, job.city, job.area,
55
- job.salary_min_lpa, job.salary_max_lpa,
56
- job.experience_min_years, job.experience_max_years,
57
- json.dumps(job.tech_stack), job.requirements,
58
- job.work_mode, job.company_tier, job.posted_date, job.url
59
- ))
60
- conn.commit()
61
- return True
62
-
63
- def get_job_by_id(self, job_id: str) -> Optional[JobPosting]:
64
- with self._get_connection() as conn:
65
- cursor = conn.cursor()
66
- cursor.execute("SELECT * FROM job_postings WHERE id = ?", (job_id,))
67
- row = cursor.fetchone()
68
- if not row:
69
- return None
70
- return self._row_to_job(row)
71
-
72
- def search_jobs(
73
- self,
74
- domain: Optional[str] = None,
75
- city: Optional[str] = None,
76
- query: Optional[str] = None,
77
- experience_level: Optional[int] = None,
78
- min_salary_lpa: Optional[float] = None,
79
- tech_stack_filter: Optional[List[str]] = None,
80
- limit: int = 100
81
- ) -> List[JobPosting]:
82
- with self._get_connection() as conn:
83
- cursor = conn.cursor()
84
- sql = "SELECT * FROM job_postings WHERE 1=1"
85
- params = []
86
-
87
- if domain and domain.lower() != "all":
88
- sql += " AND LOWER(tech_domain) = LOWER(?)"
89
- params.append(domain)
90
-
91
- if city and city.lower() != "all":
92
- sql += " AND LOWER(city) = LOWER(?)"
93
- params.append(city)
94
-
95
- if experience_level is not None:
96
- sql += " AND experience_min_years <= ? AND experience_max_years >= ?"
97
- params.extend([experience_level, experience_level])
98
-
99
- if min_salary_lpa is not None:
100
- sql += " AND salary_max_lpa >= ?"
101
- params.append(min_salary_lpa)
102
-
103
- if query:
104
- sql += " AND (LOWER(title) LIKE LOWER(?) OR LOWER(company) LIKE LOWER(?) OR LOWER(requirements) LIKE LOWER(?) OR LOWER(area) LIKE LOWER(?))"
105
- q = f"%{query}%"
106
- params.extend([q, q, q, q])
107
-
108
- sql += " ORDER BY salary_max_lpa DESC LIMIT ?"
109
- params.append(limit)
110
-
111
- cursor.execute(sql, params)
112
- rows = cursor.fetchall()
113
- jobs = [self._row_to_job(r) for r in rows]
114
-
115
- if tech_stack_filter:
116
- filter_set = {s.lower() for s in tech_stack_filter}
117
- jobs = [
118
- j for j in jobs
119
- if any(ts.lower() in filter_set for ts in j.tech_stack)
120
- ]
121
-
122
- return jobs
123
-
124
- def get_all_jobs(self) -> List[JobPosting]:
125
- return self.search_jobs(limit=500)
126
-
127
- def get_market_analytics(self, city: str = "All", domain: str = "All") -> Dict[str, Any]:
128
- with self._get_connection() as conn:
129
- cursor = conn.cursor()
130
- sql = "SELECT COUNT(*), AVG((salary_min_lpa + salary_max_lpa) / 2.0), MIN(salary_min_lpa), MAX(salary_max_lpa) FROM job_postings WHERE 1=1"
131
- params = []
132
-
133
- if city and city.lower() != "all":
134
- sql += " AND LOWER(city) = LOWER(?)"
135
- params.append(city)
136
-
137
- if domain and domain.lower() != "all":
138
- sql += " AND LOWER(tech_domain) = LOWER(?)"
139
- params.append(domain)
140
-
141
- cursor.execute(sql, params)
142
- count, avg_sal, min_sal, max_sal = cursor.fetchone()
143
-
144
- if not count or count == 0:
145
- return {
146
- "city": city,
147
- "domain": domain,
148
- "total_jobs": 0,
149
- "avg_salary_lpa": 0,
150
- "salary_range": "N/A",
151
- "top_frameworks": [],
152
- "top_hubs": [],
153
- "top_companies": []
154
- }
155
-
156
- sql_details = "SELECT tech_stack, area, company FROM job_postings WHERE 1=1"
157
- params_details = []
158
- if city and city.lower() != "all":
159
- sql_details += " AND LOWER(city) = LOWER(?)"
160
- params_details.append(city)
161
- if domain and domain.lower() != "all":
162
- sql_details += " AND LOWER(tech_domain) = LOWER(?)"
163
- params_details.append(domain)
164
-
165
- cursor.execute(sql_details, params_details)
166
- rows = cursor.fetchall()
167
-
168
- skill_counts = {}
169
- hub_counts = {}
170
- company_set = set()
171
-
172
- for r in rows:
173
- stacks = json.loads(r["tech_stack"])
174
- for s in stacks:
175
- skill_counts[s] = skill_counts.get(s, 0) + 1
176
- area = r["area"]
177
- hub_counts[area] = hub_counts.get(area, 0) + 1
178
- company_set.add(r["company"])
179
-
180
- sorted_skills = [
181
- {"skill": k, "count": v, "percentage": round((v / count) * 100, 1)}
182
- for k, v in sorted(skill_counts.items(), key=lambda x: x[1], reverse=True)[:10]
183
- ]
184
- sorted_hubs = [
185
- {"hub": k, "count": v}
186
- for k, v in sorted(hub_counts.items(), key=lambda x: x[1], reverse=True)[:5]
187
- ]
188
-
189
- return {
190
- "city": city,
191
- "domain": domain,
192
- "total_jobs": count,
193
- "avg_salary_lpa": round(avg_sal or 0, 1),
194
- "salary_range": f"₹{int(min_sal or 0)}L - ₹{int(max_sal or 0)}L PA",
195
- "top_frameworks": sorted_skills,
196
- "top_hubs": sorted_hubs,
197
- "top_companies": list(company_set)[:10]
198
- }
199
-
200
- def _row_to_job(self, row: sqlite3.Row) -> JobPosting:
201
- return JobPosting(
202
- id=row["id"],
203
- title=row["title"],
204
- company=row["company"],
205
- tech_domain=row["tech_domain"],
206
- city=row["city"],
207
- area=row["area"],
208
- salary_min_lpa=row["salary_min_lpa"],
209
- salary_max_lpa=row["salary_max_lpa"],
210
- experience_min_years=row["experience_min_years"],
211
- experience_max_years=row["experience_max_years"],
212
- tech_stack=json.loads(row["tech_stack"]),
213
- requirements=row["requirements"],
214
- work_mode=row["work_mode"],
215
- company_tier=row["company_tier"],
216
- posted_date=row["posted_date"],
217
- url=row["url"]
218
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/db/models.py DELETED
@@ -1,74 +0,0 @@
1
- from typing import List, Dict, Optional, Any
2
- from pydantic import BaseModel, Field
3
- from datetime import datetime
4
-
5
- class JobPosting(BaseModel):
6
- id: str = Field(..., description="Unique job identifier, e.g. JOB-BLR-101")
7
- title: str = Field(..., description="Job title, e.g., Senior Backend Engineer (Go/Distributed Systems)")
8
- company: str = Field(..., description="Company name")
9
- tech_domain: str = Field(default="Backend Engineering", description="Domain: Backend, Frontend, Full Stack, DevOps/Cloud, Data Engineering, AI/ML & GenAI, Mobile, Cybersecurity")
10
- city: str = Field(default="Bengaluru", description="City location: Bengaluru, Pune, Hyderabad, Gurgaon, Mumbai, Chennai, Remote")
11
- area: str = Field(default="Indiranagar", description="Locality or Tech Park, e.g. Outer Ring Road, Hinjawadi, HITEC City, Cyber City")
12
- salary_min_lpa: float = Field(..., description="Minimum salary in Lakhs Per Annum (LPA)")
13
- salary_max_lpa: float = Field(..., description="Maximum salary in Lakhs Per Annum (LPA)")
14
- experience_min_years: int = Field(..., description="Minimum experience required in years")
15
- experience_max_years: int = Field(..., description="Maximum experience requested in years")
16
- tech_stack: List[str] = Field(default_factory=list, description="Extracted tech stack, e.g., ['Go', 'Kubernetes', 'gRPC', 'PostgreSQL', 'Redis']")
17
- requirements: str = Field(..., description="Full text job description and responsibilities")
18
- work_mode: str = Field(default="Hybrid", description="Hybrid, On-site, or Remote")
19
- company_tier: str = Field(default="Product Unicorn / Enterprise", description="Company tier")
20
- posted_date: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
21
- url: Optional[str] = Field(default="https://techradar.ai/jobs", description="Job posting URL")
22
- semantic_score: Optional[float] = Field(default=None, description="Relevance score from semantic vector search")
23
-
24
- class SkillGapReport(BaseModel):
25
- target_job_id: str
26
- job_title: str
27
- company: str
28
- city: str
29
- tech_domain: str
30
- match_percentage: float = Field(..., description="Overall candidate match percentage (0-100%)")
31
- matched_skills: List[str] = Field(default_factory=list, description="Skills present in candidate profile & JD")
32
- missing_skills: List[str] = Field(default_factory=list, description="Critical skills in JD missing from candidate profile")
33
- high_priority_gaps: List[str] = Field(default_factory=list, description="Top deal-breaker missing skills for this role")
34
- recommended_action_plan: List[Dict[str, str]] = Field(default_factory=list, description="Actionable micro-projects to bridge gaps")
35
- estimated_learning_hours: int = Field(default=20, description="Estimated effort to reach 90%+ match")
36
-
37
- class ResumePatch(BaseModel):
38
- target_job_id: str
39
- job_title: str
40
- company: str
41
- ats_compatibility_score: float = Field(..., description="Score out of 100 for ATS parsing")
42
- tailored_bullets: List[Dict[str, str]] = Field(
43
- ...,
44
- description="List of dicts with 'original', 'tailored', and 'rationale'"
45
- )
46
- added_keywords: List[str] = Field(default_factory=list, description="Keywords injected for ATS optimization")
47
-
48
- class CityMarketInsights(BaseModel):
49
- city: str
50
- tech_domain: str
51
- total_active_jobs: int
52
- avg_salary_lpa: float
53
- salary_range: str
54
- top_demanded_frameworks: List[Dict[str, Any]]
55
- top_hiring_hubs: List[Dict[str, Any]]
56
- top_employers: List[str]
57
- growth_trend: str
58
-
59
- class InterviewQuestion(BaseModel):
60
- question: str
61
- category: str
62
- difficulty: str
63
- ideal_answer_points: List[str]
64
- company_context: Optional[str] = None
65
-
66
- class InterviewPrepKit(BaseModel):
67
- job_id: str
68
- job_title: str
69
- company: str
70
- city: str
71
- tech_domain: str
72
- technical_questions: List[InterviewQuestion]
73
- system_design_challenge: Dict[str, Any]
74
- prep_tips: List[str]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/db/vector_store.py DELETED
@@ -1,119 +0,0 @@
1
- import math
2
- import re
3
- from typing import List, Tuple, Dict, Any
4
- from tech_radar.db.models import JobPosting
5
-
6
- class SemanticVectorStore:
7
- """
8
- Lightweight, high-performance Vector & Semantic Search Engine for TechRadar-MCP across all software domains.
9
- """
10
-
11
- def __init__(self):
12
- self.jobs: List[JobPosting] = []
13
- self.doc_vectors: List[Dict[str, float]] = []
14
- self.idf: Dict[str, float] = {}
15
-
16
- def _tokenize(self, text: str) -> List[str]:
17
- words = re.findall(r'\b[a-zA-Z0-9+#\.-]+\b', text.lower())
18
- return [w for w in words if len(w) > 1]
19
-
20
- def index_jobs(self, jobs: List[JobPosting]):
21
- self.jobs = jobs
22
- self.doc_vectors = []
23
- doc_count = len(jobs)
24
- doc_freq = {}
25
-
26
- raw_docs = []
27
- for job in jobs:
28
- text = f"{job.title} {job.company} {job.tech_domain} {job.city} {job.area} {' '.join(job.tech_stack)} {job.requirements}"
29
- tokens = self._tokenize(text)
30
- raw_docs.append(tokens)
31
- unique_tokens = set(tokens)
32
- for token in unique_tokens:
33
- doc_freq[token] = doc_freq.get(token, 0) + 1
34
-
35
- self.idf = {
36
- token: math.log((doc_count + 1) / (freq + 1)) + 1.0
37
- for token, freq in doc_freq.items()
38
- }
39
-
40
- for tokens in raw_docs:
41
- tf = {}
42
- for t in tokens:
43
- tf[t] = tf.get(t, 0) + 1
44
- length = len(tokens) or 1
45
- vec = {
46
- term: (freq / length) * self.idf.get(term, 1.0)
47
- for term, freq in tf.items()
48
- }
49
- self.doc_vectors.append(vec)
50
-
51
- def _cosine_similarity(self, vec1: Dict[str, float], vec2: Dict[str, float]) -> float:
52
- intersection = set(vec1.keys()) & set(vec2.keys())
53
- numerator = sum(vec1[x] * vec2[x] for x in intersection)
54
-
55
- sum1 = sum(val ** 2 for val in vec1.values())
56
- sum2 = sum(val ** 2 for val in vec2.values())
57
- denominator = math.sqrt(sum1) * math.sqrt(sum2)
58
-
59
- if not denominator:
60
- return 0.0
61
- return float(numerator / denominator)
62
-
63
- def search_semantic(
64
- self,
65
- query: str,
66
- domain: str = None,
67
- city: str = None,
68
- top_k: int = 15
69
- ) -> List[Tuple[JobPosting, float]]:
70
- if not self.doc_vectors or not self.jobs:
71
- return []
72
-
73
- q_tokens = self._tokenize(query)
74
- tf = {}
75
- for t in q_tokens:
76
- tf[t] = tf.get(t, 0) + 1
77
- q_length = len(q_tokens) or 1
78
- q_vec = {
79
- term: (freq / q_length) * self.idf.get(term, 1.0)
80
- for term, freq in tf.items()
81
- }
82
-
83
- results = []
84
- for idx, job in enumerate(self.jobs):
85
- if city and city.lower() != "all" and job.city.lower() != city.lower():
86
- continue
87
- if domain and domain.lower() != "all" and job.tech_domain.lower() != domain.lower():
88
- continue
89
-
90
- sim = self._cosine_similarity(q_vec, self.doc_vectors[idx])
91
-
92
- tech_match_bonus = sum(
93
- 0.15 for t in q_tokens
94
- if any(t.lower() == stack.lower() for stack in job.tech_stack)
95
- )
96
- final_score = round(min(1.0, sim + tech_match_bonus), 3)
97
-
98
- if final_score > 0.01:
99
- results.append((job, final_score))
100
-
101
- results.sort(key=lambda x: x[1], reverse=True)
102
- return results[:top_k]
103
-
104
- def match_resume_to_jd(self, resume_text: str, job: JobPosting) -> float:
105
- r_tokens = self._tokenize(resume_text)
106
- jd_text = f"{job.title} {job.tech_domain} {' '.join(job.tech_stack)} {job.requirements}"
107
- jd_tokens = self._tokenize(jd_text)
108
-
109
- r_set = set(r_tokens)
110
- jd_set = set(jd_tokens)
111
-
112
- tech_matches = [t for t in job.tech_stack if any(t.lower() == r.lower() for r in r_set)]
113
- tech_ratio = len(tech_matches) / (len(job.tech_stack) or 1)
114
-
115
- common_vocab = r_set & jd_set
116
- vocab_ratio = len(common_vocab) / (len(jd_set) or 1)
117
-
118
- overall_score = (tech_ratio * 0.6) + (vocab_ratio * 0.4)
119
- return round(min(99.0, overall_score * 100.0), 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/mcp/prompts.py DELETED
@@ -1,15 +0,0 @@
1
- def prompt_tech_career_coach(candidate_target_city: str = "All", domain_interest: str = "Software Engineering", resume_summary: str = "") -> str:
2
- """Prompt template for AI assistant acting as a Universal Tech Career Coach."""
3
- return f"""You are the TechRadar Universal Career Coach specialized in Software & AI hiring across India & Remote.
4
- Target City / Hub: {candidate_target_city}
5
- Domain Interest: {domain_interest}
6
- Candidate Summary: {resume_summary or 'Software Engineer'}
7
-
8
- Instructions:
9
- 1. Use the 'search_tech_jobs' MCP tool to find matching active roles across companies in {candidate_target_city}.
10
- 2. Use 'analyze_skill_gap' to identify missing domain skills and frameworks.
11
- 3. Use 'generate_tailored_resume_patch' to suggest bullet point diffs for ATS optimization.
12
- 4. Use 'generate_interview_prep_kit' to prepare technical interview questions & system design challenges.
13
-
14
- Provide clear, encouraging, and highly technical actionable advice.
15
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/mcp/resources.py DELETED
@@ -1,13 +0,0 @@
1
- from tech_radar.mcp.tools import get_db_and_vector_store
2
- import json
3
-
4
- def resource_india_tech_market_report() -> str:
5
- """Return universal India Tech Hiring Market Intelligence Report in Markdown."""
6
- _, _, _, analyst = get_db_and_vector_store()
7
- return analyst.generate_markdown_summary(city="All", domain="All")
8
-
9
- def resource_latest_jobs_json() -> str:
10
- """Return JSON snapshot of active tech jobs across India & Remote."""
11
- db, _, _, _ = get_db_and_vector_store()
12
- jobs = [j.dict() for j in db.get_all_jobs()[:20]]
13
- return json.dumps(jobs, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/mcp/server.py DELETED
@@ -1,106 +0,0 @@
1
- import sys
2
- import os
3
- from typing import Optional, List
4
- from fastmcp import FastMCP
5
-
6
- from tech_radar.mcp.tools import (
7
- tool_search_tech_jobs,
8
- tool_analyze_skill_gap,
9
- tool_generate_resume_patch,
10
- tool_get_market_insights,
11
- tool_generate_interview_prep_kit
12
- )
13
- from tech_radar.mcp.resources import (
14
- resource_india_tech_market_report,
15
- resource_latest_jobs_json
16
- )
17
- from tech_radar.mcp.prompts import prompt_tech_career_coach
18
-
19
- # FastMCP Server Instance
20
- mcp = FastMCP(
21
- name="Tech-Radar-India",
22
- instructions=(
23
- "TechRadar is a Model Context Protocol (MCP) server providing universal job market intelligence, "
24
- "semantic search, candidate skill gap analysis, ATS resume patching, and interview prep kits for "
25
- "Software Engineering, Cloud, Data, and AI/ML roles across Indian tech hubs (Bengaluru, Pune, Hyderabad, Gurgaon, Remote)."
26
- )
27
- )
28
-
29
- @mcp.tool(
30
- name="search_tech_jobs",
31
- description="Search active software & tech job openings across domains (Backend, Frontend, Full Stack, DevOps, Data, AI/ML, Mobile) and cities."
32
- )
33
- def search_tech_jobs(
34
- domain: str = "All",
35
- city: str = "All",
36
- query: Optional[str] = None,
37
- experience_level: Optional[int] = None,
38
- min_salary_lpa: Optional[float] = None,
39
- limit: int = 15
40
- ) -> str:
41
- return tool_search_tech_jobs(
42
- domain=domain,
43
- city=city,
44
- query=query,
45
- experience_level=experience_level,
46
- min_salary_lpa=min_salary_lpa,
47
- limit=limit
48
- )
49
-
50
- @mcp.tool(
51
- name="analyze_skill_gap",
52
- description="Analyze candidate resume against target tech Job ID. Returns match score, missing skills, and micro-learning plan."
53
- )
54
- def analyze_skill_gap(
55
- resume_text: str,
56
- target_job_id: str
57
- ) -> str:
58
- return tool_analyze_skill_gap(resume_text=resume_text, target_job_id=target_job_id)
59
-
60
- @mcp.tool(
61
- name="generate_tailored_resume_patch",
62
- description="Generate customized ATS bullet point diffs and keyword injections for any target software role."
63
- )
64
- def generate_tailored_resume_patch(
65
- resume_text: str,
66
- target_job_id: str
67
- ) -> str:
68
- return tool_generate_resume_patch(resume_text=resume_text, target_job_id=target_job_id)
69
-
70
- @mcp.tool(
71
- name="get_market_insights",
72
- description="Get real-time hiring trends, salary distribution, tech hub breakdowns, and top employers for any domain and city."
73
- )
74
- def get_market_insights(
75
- domain: str = "All",
76
- city: str = "All"
77
- ) -> str:
78
- return tool_get_market_insights(domain=domain, city=city)
79
-
80
- @mcp.tool(
81
- name="generate_interview_prep_kit",
82
- description="Generate domain-specific technical interview questions, system design challenge, and answer key for a target job ID."
83
- )
84
- def generate_interview_prep_kit(
85
- target_job_id: str
86
- ) -> str:
87
- return tool_generate_interview_prep_kit(target_job_id=target_job_id)
88
-
89
- @mcp.resource(path="market://india-tech-report")
90
- def india_tech_market_report() -> str:
91
- return resource_india_tech_market_report()
92
-
93
- @mcp.resource(path="jobs://latest-tech-jobs")
94
- def latest_jobs_json() -> str:
95
- return resource_latest_jobs_json()
96
-
97
- @mcp.prompt(name="tech_career_coach")
98
- def tech_career_coach(candidate_target_city: str = "All", domain_interest: str = "Software Engineering", resume_summary: str = "") -> str:
99
- return prompt_tech_career_coach(candidate_target_city=candidate_target_city, domain_interest=domain_interest, resume_summary=resume_summary)
100
-
101
- def run():
102
- """Run FastMCP server over stdio."""
103
- mcp.run(transport="stdio")
104
-
105
- if __name__ == "__main__":
106
- run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/mcp/tools.py DELETED
@@ -1,116 +0,0 @@
1
- import json
2
- from typing import List, Optional, Dict, Any
3
- from tech_radar.db.database import DatabaseManager
4
- from tech_radar.db.vector_store import SemanticVectorStore
5
- from tech_radar.agents.evaluator_agent import EvaluatorAgent
6
- from tech_radar.agents.market_analyst import MarketAnalystAgent
7
- from tech_radar.scrapers.seeder import seed_database
8
-
9
- _db: Optional[DatabaseManager] = None
10
- _vector_store: Optional[SemanticVectorStore] = None
11
- _evaluator: Optional[EvaluatorAgent] = None
12
- _market_analyst: Optional[MarketAnalystAgent] = None
13
-
14
- def get_db_and_vector_store():
15
- global _db, _vector_store, _evaluator, _market_analyst
16
- if _db is None or _vector_store is None:
17
- _db, _vector_store = seed_database()
18
- _evaluator = EvaluatorAgent()
19
- _market_analyst = MarketAnalystAgent(_db)
20
- return _db, _vector_store, _evaluator, _market_analyst
21
-
22
- def tool_search_tech_jobs(
23
- domain: str = "All",
24
- city: str = "All",
25
- query: Optional[str] = None,
26
- experience_level: Optional[int] = None,
27
- min_salary_lpa: Optional[float] = None,
28
- tech_stack: Optional[List[str]] = None,
29
- limit: int = 15
30
- ) -> str:
31
- """
32
- Search tech job opportunities across domains (Backend, Frontend, Full Stack, DevOps, Data, AI/ML, Mobile)
33
- and cities (Bengaluru, Pune, Hyderabad, Gurgaon, Mumbai, Remote).
34
- """
35
- db, vector_store, _, _ = get_db_and_vector_store()
36
-
37
- if query:
38
- semantic_results = vector_store.search_semantic(query=query, domain=domain, city=city, top_k=limit)
39
- jobs = []
40
- for j, score in semantic_results:
41
- j.semantic_score = score
42
- jobs.append(j.dict())
43
- else:
44
- filtered = db.search_jobs(
45
- domain=domain,
46
- city=city,
47
- query=query,
48
- experience_level=experience_level,
49
- min_salary_lpa=min_salary_lpa,
50
- tech_stack_filter=tech_stack,
51
- limit=limit
52
- )
53
- jobs = [j.dict() for j in filtered]
54
-
55
- return json.dumps({
56
- "count": len(jobs),
57
- "domain": domain,
58
- "city": city,
59
- "jobs": jobs
60
- }, indent=2)
61
-
62
- def tool_analyze_skill_gap(
63
- resume_text: str,
64
- target_job_id: str
65
- ) -> str:
66
- """
67
- Perform candidate skill gap evaluation against any target tech Job ID.
68
- Returns match percentage, missing skills, and micro-learning action plan.
69
- """
70
- db, _, evaluator, _ = get_db_and_vector_store()
71
- job = db.get_job_by_id(target_job_id)
72
- if not job:
73
- return json.dumps({"error": f"Job ID '{target_job_id}' not found in database."})
74
-
75
- report = evaluator.evaluate_skill_gap(resume_text=resume_text, candidate_skills=[], job=job)
76
- return json.dumps(report.dict(), indent=2)
77
-
78
- def tool_generate_resume_patch(
79
- resume_text: str,
80
- target_job_id: str
81
- ) -> str:
82
- """
83
- Generate tailored ATS bullet point diffs and keyword recommendations for any target software role.
84
- """
85
- db, _, evaluator, _ = get_db_and_vector_store()
86
- job = db.get_job_by_id(target_job_id)
87
- if not job:
88
- return json.dumps({"error": f"Job ID '{target_job_id}' not found in database."})
89
-
90
- patch = evaluator.generate_resume_patch(resume_text=resume_text, job=job)
91
- return json.dumps(patch.dict(), indent=2)
92
-
93
- def tool_get_market_insights(
94
- domain: str = "All",
95
- city: str = "All"
96
- ) -> str:
97
- """
98
- Get real-time hiring trends, salary distribution, and top employers for any tech domain and city.
99
- """
100
- _, _, _, analyst = get_db_and_vector_store()
101
- insights = analyst.generate_market_report(city=city, domain=domain)
102
- return json.dumps(insights.dict(), indent=2)
103
-
104
- def tool_generate_interview_prep_kit(
105
- target_job_id: str
106
- ) -> str:
107
- """
108
- Generate domain-specific technical interview questions, system design challenge, and answer key for a target job ID.
109
- """
110
- db, _, evaluator, _ = get_db_and_vector_store()
111
- job = db.get_job_by_id(target_job_id)
112
- if not job:
113
- return json.dumps({"error": f"Job ID '{target_job_id}' not found in database."})
114
-
115
- prep_kit = evaluator.generate_interview_prep(job=job)
116
- return json.dumps(prep_kit.dict(), indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/scrapers/live_scraper.py DELETED
@@ -1,272 +0,0 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- import re
4
- import json
5
- import warnings
6
- from typing import List, Dict, Any, Optional
7
- from datetime import datetime
8
-
9
- from tech_radar.db.models import JobPosting
10
-
11
- warnings.filterwarnings("ignore")
12
-
13
- class LiveInternetScraper:
14
- """
15
- Autonomous Live Internet Job Scraper Engine for TechRadar-MCP.
16
- Aggregates live, real-time tech postings from RemoteOK, Jobicy, and WeWorkRemotely feeds.
17
- """
18
-
19
- def __init__(self):
20
- self.headers = {
21
- "User-Agent": (
22
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
23
- "AppleWebKit/537.36 (KHTML, like Gecko) "
24
- "Chrome/120.0.0.0 Safari/537.36"
25
- )
26
- }
27
-
28
- def fetch_all_live_jobs(self) -> List[JobPosting]:
29
- """Fetch real-time live jobs from all active internet APIs & feeds."""
30
- live_jobs: List[JobPosting] = []
31
-
32
- # 1. Fetch RemoteOK Live API
33
- print("[LiveScraper] Ingesting live jobs from RemoteOK API...")
34
- try:
35
- remote_ok_jobs = self._scrape_remoteok()
36
- live_jobs.extend(remote_ok_jobs)
37
- print(f" -> Fetched {len(remote_ok_jobs)} live jobs from RemoteOK.")
38
- except Exception as e:
39
- print(f"[LiveScraper] RemoteOK error: {e}")
40
-
41
- # 2. Fetch Jobicy Live API
42
- print("[LiveScraper] Ingesting live jobs from Jobicy API...")
43
- try:
44
- jobicy_jobs = self._scrape_jobicy()
45
- live_jobs.extend(jobicy_jobs)
46
- print(f" -> Fetched {len(jobicy_jobs)} live jobs from Jobicy.")
47
- except Exception as e:
48
- print(f"[LiveScraper] Jobicy error: {e}")
49
-
50
- # 3. Fetch WeWorkRemotely RSS
51
- print("[LiveScraper] Ingesting live jobs from WeWorkRemotely RSS...")
52
- try:
53
- wwr_jobs = self._scrape_weworkremotely()
54
- live_jobs.extend(wwr_jobs)
55
- print(f" -> Fetched {len(wwr_jobs)} live jobs from WeWorkRemotely.")
56
- except Exception as e:
57
- print(f"[LiveScraper] WeWorkRemotely error: {e}")
58
-
59
- print(f"✨ Total Live Internet Jobs Ingested: {len(live_jobs)}")
60
- return live_jobs
61
-
62
- def _scrape_remoteok(self) -> List[JobPosting]:
63
- url = "https://remoteok.com/api"
64
- resp = requests.get(url, headers=self.headers, timeout=12)
65
- if resp.status_code != 200:
66
- return []
67
-
68
- raw_data = resp.json()
69
- jobs = []
70
-
71
- # RemoteOK first item is metadata
72
- items = raw_data[1:] if isinstance(raw_data, list) and len(raw_data) > 1 else []
73
-
74
- for item in items:
75
- if not isinstance(item, dict) or "position" not in item:
76
- continue
77
-
78
- title = item.get("position", "Software Engineer")
79
- company = item.get("company", "Tech Company")
80
- tags = item.get("tags", [])
81
- location = item.get("location", "Remote")
82
- desc = item.get("description", title)
83
- job_url = item.get("url", "https://remoteok.com")
84
- raw_id = item.get("id", str(abs(hash(title + company)) % 100000))
85
-
86
- domain = self.detect_domain(title, tags)
87
- city = self.detect_city(location)
88
- salary_min, salary_max = self.extract_salary_lpa(item.get("salary_min"), item.get("salary_max"), desc)
89
-
90
- jobs.append(JobPosting(
91
- id=f"LIVE-ROK-{raw_id}",
92
- title=title[:70],
93
- company=company[:50],
94
- tech_domain=domain,
95
- city=city,
96
- area=location[:30] if location else "Remote Hub",
97
- salary_min_lpa=salary_min,
98
- salary_max_lpa=salary_max,
99
- experience_min_years=2,
100
- experience_max_years=7,
101
- tech_stack=tags[:8] if tags else ["Python", "Docker", "API"],
102
- requirements=self.clean_html(desc)[:1000],
103
- work_mode="Remote" if "remote" in location.lower() or not location else "Hybrid",
104
- company_tier="Global Remote / Tech Enterprise",
105
- posted_date=datetime.now().strftime("%Y-%m-%d"),
106
- url=job_url
107
- ))
108
-
109
- return jobs
110
-
111
- def _scrape_jobicy(self) -> List[JobPosting]:
112
- url = "https://jobicy.com/api/v2/remote-jobs"
113
- resp = requests.get(url, headers=self.headers, timeout=12)
114
- if resp.status_code != 200:
115
- return []
116
-
117
- raw_data = resp.json().get("jobs", [])
118
- jobs = []
119
-
120
- for item in raw_data:
121
- title = item.get("jobTitle", "Software Engineer")
122
- company = item.get("companyName", "Tech Firm")
123
- geo = item.get("jobGeo", "Remote")
124
- desc = item.get("jobDescription", title)
125
- job_url = item.get("url", "https://jobicy.com")
126
- raw_id = item.get("id", str(abs(hash(title + company)) % 100000))
127
-
128
- domain = self.detect_domain(title, [item.get("jobCategory", "")])
129
- city = self.detect_city(geo)
130
- salary_min, salary_max = self.extract_salary_lpa(None, None, desc)
131
-
132
- jobs.append(JobPosting(
133
- id=f"LIVE-JBC-{raw_id}",
134
- title=title[:70],
135
- company=company[:50],
136
- tech_domain=domain,
137
- city=city,
138
- area=geo[:30] if geo else "Global Remote",
139
- salary_min_lpa=salary_min,
140
- salary_max_lpa=salary_max,
141
- experience_min_years=3,
142
- experience_max_years=8,
143
- tech_stack=self.extract_tech_keywords(title + " " + desc)[:7],
144
- requirements=self.clean_html(desc)[:1000],
145
- work_mode="Remote",
146
- company_tier="Tech Firm",
147
- posted_date=datetime.now().strftime("%Y-%m-%d"),
148
- url=job_url
149
- ))
150
-
151
- return jobs
152
-
153
- def _scrape_weworkremotely(self) -> List[JobPosting]:
154
- url = "https://weworkremotely.com/categories/remote-programming-jobs.rss"
155
- resp = requests.get(url, headers=self.headers, timeout=12)
156
- if resp.status_code != 200:
157
- return []
158
-
159
- soup = BeautifulSoup(resp.text, "html.parser")
160
- items = soup.find_all("item")
161
- jobs = []
162
-
163
- for idx, item in enumerate(items):
164
- title_node = item.find("title")
165
- link_node = item.find("link")
166
- desc_node = item.find("description")
167
-
168
- full_title = title_node.get_text() if title_node else "Senior Engineer"
169
- job_url = link_node.get_text() if link_node else "https://weworkremotely.com"
170
- desc = desc_node.get_text() if desc_node else full_title
171
-
172
- parts = full_title.split(":")
173
- if len(parts) > 1:
174
- company = parts[0].strip()
175
- title = parts[1].strip()
176
- else:
177
- company = "WeWorkRemotely Tech"
178
- title = full_title
179
-
180
- domain = self.detect_domain(title, [])
181
- salary_min, salary_max = self.extract_salary_lpa(None, None, desc)
182
-
183
- jobs.append(JobPosting(
184
- id=f"LIVE-WWR-{idx + 100}",
185
- title=title[:70],
186
- company=company[:50],
187
- tech_domain=domain,
188
- city="Remote",
189
- area="Global Remote Hub",
190
- salary_min_lpa=salary_min,
191
- salary_max_lpa=salary_max,
192
- experience_min_years=3,
193
- experience_max_years=8,
194
- tech_stack=self.extract_tech_keywords(title + " " + desc)[:7],
195
- requirements=self.clean_html(desc)[:1000],
196
- work_mode="Remote",
197
- company_tier="Product Tech Leader",
198
- posted_date=datetime.now().strftime("%Y-%m-%d"),
199
- url=job_url
200
- ))
201
-
202
- return jobs
203
-
204
- def detect_domain(self, title: str, tags: List[str]) -> str:
205
- text = (title + " " + " ".join(tags)).lower()
206
- if any(w in text for w in ["backend", "go", "java", "spring", "microservice", "python"]):
207
- return "Backend Engineering"
208
- elif any(w in text for w in ["frontend", "react", "next.js", "vue", "typescript", "ui"]):
209
- return "Frontend Engineering"
210
- elif any(w in text for w in ["full stack", "fullstack", "full-stack"]):
211
- return "Full Stack Engineering"
212
- elif any(w in text for w in ["devops", "cloud", "aws", "kubernetes", "k8s", "terraform", "sre"]):
213
- return "Cloud & DevOps"
214
- elif any(w in text for w in ["data", "spark", "snowflake", "kafka", "pipeline", "sql"]):
215
- return "Data Engineering"
216
- elif any(w in text for w in ["ai", "genai", "llm", "machine learning", "pytorch", "mcp", "cuda"]):
217
- return "AI/ML & GenAI"
218
- elif any(w in text for w in ["android", "ios", "flutter", "kotlin", "mobile", "swift"]):
219
- return "Mobile Engineering"
220
- return "Software Engineering"
221
-
222
- def detect_city(self, location: str) -> str:
223
- loc = (location or "").lower()
224
- if "bengaluru" in loc or "bangalore" in loc:
225
- return "Bengaluru"
226
- elif "pune" in loc:
227
- return "Pune"
228
- elif "hyderabad" in loc:
229
- return "Hyderabad"
230
- elif "gurgaon" in loc or "delhi" in loc or "ncr" in loc:
231
- return "Gurgaon"
232
- elif "mumbai" in loc:
233
- return "Mumbai"
234
- elif "chennai" in loc:
235
- return "Chennai"
236
- return "Remote"
237
-
238
- def extract_salary_lpa(self, sal_min: Optional[float], sal_max: Optional[float], text: str) -> tuple[float, float]:
239
- if sal_min and sal_max and sal_min > 1000:
240
- # Convert USD to INR LPA (e.g. $100K = ~85 LPA)
241
- min_lpa = round((sal_min * 83.5) / 100000.0, 1)
242
- max_lpa = round((sal_max * 83.5) / 100000.0, 1)
243
- return max(18.0, min_lpa), max(28.0, max_lpa)
244
-
245
- # Regex search for salary numbers in description
246
- match = re.search(r'\$(\d{2,3})k?\s*-\s*\$?(\d{2,3})k', text, re.IGNORECASE)
247
- if match:
248
- s1 = float(match.group(1)) * 1000
249
- s2 = float(match.group(2)) * 1000
250
- min_lpa = round((s1 * 83.5) / 100000.0, 1)
251
- max_lpa = round((s2 * 83.5) / 100000.0, 1)
252
- return max(20.0, min_lpa), max(32.0, max_lpa)
253
-
254
- return 26.0, 44.0
255
-
256
- def extract_tech_keywords(self, text: str) -> List[str]:
257
- known = [
258
- "Go", "Java", "Python", "TypeScript", "React", "Next.js", "Node.js",
259
- "FastAPI", "Spring Boot", "Docker", "Kubernetes", "AWS", "Terraform",
260
- "PostgreSQL", "Redis", "Kafka", "Apache Spark", "Snowflake",
261
- "FastMCP", "MCP", "LangGraph", "PyTorch", "CUDA", "vLLM", "Qdrant",
262
- "Kotlin", "Flutter", "Swift", "GraphQL", "gRPC"
263
- ]
264
- found = []
265
- for kw in known:
266
- if re.search(r'\b' + re.escape(kw) + r'\b', text, re.IGNORECASE):
267
- found.append(kw)
268
- return found or ["Python", "Docker", "REST API"]
269
-
270
- def clean_html(self, raw_html: str) -> str:
271
- soup = BeautifulSoup(raw_html, "html.parser")
272
- return soup.get_text(separator=" ", strip=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/scrapers/mock_data.py DELETED
@@ -1,249 +0,0 @@
1
- from typing import List
2
- from tech_radar.db.models import JobPosting
3
-
4
- UNIVERSAL_TECH_JOBS: List[JobPosting] = [
5
- # BACKEND ENGINEERING
6
- JobPosting(
7
- id="BLR-BACKEND-101",
8
- title="Staff Backend Engineer (Distributed Systems / Go)",
9
- company="Uber Tech India",
10
- tech_domain="Backend Engineering",
11
- city="Bengaluru",
12
- area="Bellandur Outer Ring Rd",
13
- salary_min_lpa=42.0,
14
- salary_max_lpa=68.0,
15
- experience_min_years=5,
16
- experience_max_years=10,
17
- tech_stack=["Go", "gRPC", "Kubernetes", "Apache Kafka", "Cassandra", "Redis", "Distributed Systems"],
18
- requirements=(
19
- "Join Uber's Core Infrastructure & Dispatch Systems team in Bengaluru. Architect high-concurrency microservices handling "
20
- "100K+ QPS using Go and gRPC. Required experience with event-driven streaming (Kafka), distributed storage (Cassandra/CockroachDB), "
21
- "and low-latency system design."
22
- ),
23
- work_mode="Hybrid",
24
- company_tier="Product Giant / Tier-1 Enterprise",
25
- posted_date="2026-08-06",
26
- url="https://uber.com/careers/blr-staff-backend"
27
- ),
28
- JobPosting(
29
- id="PUNE-BACKEND-102",
30
- title="Senior Java Microservices Architect",
31
- company="Barclays Technology Centre",
32
- city="Pune",
33
- area="Kharadi EON Free Zone",
34
- tech_domain="Backend Engineering",
35
- salary_min_lpa=28.0,
36
- salary_max_lpa=44.0,
37
- experience_min_years=4,
38
- experience_max_years=9,
39
- tech_stack=["Java 21", "Spring Boot", "Kafka", "PostgreSQL", "Docker", "AWS", "OAuth2"],
40
- requirements=(
41
- "Barclays Kharadi campus is hiring a Senior Java Architect for core payment processing gateways. "
42
- "Must have hands-on expertise with Spring Boot, Java virtual threads, resilience patterns (Resilience4j), PostgreSQL optimization, "
43
- "and secure financial REST/gRPC API development."
44
- ),
45
- work_mode="Hybrid",
46
- company_tier="Global Banking Tech R&D",
47
- posted_date="2026-08-07",
48
- url="https://barclays.com/careers/pune-java-architect"
49
- ),
50
-
51
- # FRONTEND & FULL STACK
52
- JobPosting(
53
- id="BLR-FRONTEND-201",
54
- title="Lead Frontend Architect (Next.js & Performance)",
55
- company="Cred",
56
- tech_domain="Frontend Engineering",
57
- city="Bengaluru",
58
- area="Indiranagar",
59
- salary_min_lpa=38.0,
60
- salary_max_lpa=58.0,
61
- experience_min_years=4,
62
- experience_max_years=8,
63
- tech_stack=["React 19", "Next.js", "TypeScript", "TailwindCSS", "Zustand", "WebSockets", "GraphQL"],
64
- requirements=(
65
- "Cred Bengaluru is seeking a Lead Frontend Architect to drive UI performance across web and mobile web platforms. "
66
- "Experience with SSR/SSG in Next.js, micro-frontends, bundle size optimization, responsive design systems, and real-time WebSockets."
67
- ),
68
- work_mode="On-site",
69
- company_tier="Unicorn Tech",
70
- posted_date="2026-08-08",
71
- url="https://cred.club/careers/lead-frontend"
72
- ),
73
- JobPosting(
74
- id="PUNE-FULLSTACK-202",
75
- title="Senior Full Stack Engineer (React + Python FastAPI)",
76
- company="Mastercard Tech Hub",
77
- city="Pune",
78
- area="Kharadi",
79
- tech_domain="Full Stack Engineering",
80
- salary_min_lpa=26.0,
81
- salary_max_lpa=40.0,
82
- experience_min_years=3,
83
- experience_max_years=7,
84
- tech_stack=["React", "TypeScript", "Python", "FastAPI", "PostgreSQL", "Docker", "AWS"],
85
- requirements=(
86
- "Mastercard Pune is hiring a Senior Full Stack Engineer to build developer portals and payment dashboard tools. "
87
- "Requires strong React/TypeScript skills combined with Python FastAPI backend services, Docker containerization, and AWS deployment."
88
- ),
89
- work_mode="Hybrid",
90
- company_tier="Global Tech MNC",
91
- posted_date="2026-08-05",
92
- url="https://mastercard.com/careers/pune-fullstack"
93
- ),
94
- JobPosting(
95
- id="REMOTE-FULLSTACK-203",
96
- title="Staff Full Stack Developer (Next.js & Node.js)",
97
- company="GitLab (Remote India)",
98
- city="Remote",
99
- area="Remote India",
100
- tech_domain="Full Stack Engineering",
101
- salary_min_lpa=35.0,
102
- salary_max_lpa=55.0,
103
- experience_min_years=4,
104
- experience_max_years=9,
105
- tech_stack=["TypeScript", "React", "Next.js", "Node.js", "GraphQL", "PostgreSQL", "Redis"],
106
- requirements=(
107
- "100% Remote opportunity for Indian engineers. Lead full-stack product development on developer collaboration features. "
108
- "Requires mastery of TypeScript, Next.js server components, Node.js GraphQL APIs, and asynchronous message queues."
109
- ),
110
- work_mode="Remote",
111
- company_tier="Global Remote Leader",
112
- posted_date="2026-08-08",
113
- url="https://gitlab.com/jobs/remote-fullstack"
114
- ),
115
-
116
- # CLOUD & DEVOPS
117
- JobPosting(
118
- id="HYD-DEVOPS-301",
119
- title="Principal Cloud & DevOps Infrastructure Engineer",
120
- company="Salesforce India R&D",
121
- city="Hyderabad",
122
- area="HITEC City",
123
- tech_domain="Cloud & DevOps",
124
- salary_min_lpa=40.0,
125
- salary_max_lpa=65.0,
126
- experience_min_years=5,
127
- experience_max_years=11,
128
- tech_stack=["AWS", "Kubernetes", "Terraform", "Helm", "ArgoCD", "Python", "Prometheus", "Grafana"],
129
- requirements=(
130
- "Salesforce Hyderabad is seeking a Cloud Infrastructure Leader. Drive Kubernetes cluster automation across multi-region AWS environments. "
131
- "Must have deep hands-on expertise in Infrastructure-as-Code (Terraform), GitOps with ArgoCD, cluster security, and observability (Prometheus/Jaeger)."
132
- ),
133
- work_mode="Hybrid",
134
- company_tier="Global Enterprise SaaS",
135
- posted_date="2026-08-07",
136
- url="https://salesforce.com/careers/hyd-devops-principal"
137
- ),
138
- JobPosting(
139
- id="PUNE-DEVOPS-302",
140
- title="Senior Site Reliability & Cloud Engineer",
141
- company="PTC Software R&D",
142
- city="Pune",
143
- area="Hinjawadi Phase 1",
144
- tech_domain="Cloud & DevOps",
145
- salary_min_lpa=24.0,
146
- salary_max_lpa=38.0,
147
- experience_min_years=3,
148
- experience_max_years=7,
149
- tech_stack=["Docker", "Kubernetes", "AWS", "Terraform", "Python", "CI/CD GitHub Actions"],
150
- requirements=(
151
- "PTC Hinjawadi Pune is hiring an SRE / Cloud Engineer. Manage SaaS platform uptime, automate multi-tenant deployment pipelines, "
152
- "and maintain infrastructure provisioning using Terraform and Docker Kubernetes stacks."
153
- ),
154
- work_mode="Hybrid",
155
- company_tier="Enterprise CAD/PLM Giant",
156
- posted_date="2026-08-04",
157
- url="https://ptc.com/careers/pune-sre"
158
- ),
159
-
160
- # DATA ENGINEERING & ANALYTICS
161
- JobPosting(
162
- id="NCR-DATA-401",
163
- title="Lead Data Engineer (Spark & Snowflake Platform)",
164
- company="Zomato Tech",
165
- city="Gurgaon",
166
- area="DLF Cyber City",
167
- tech_domain="Data Engineering",
168
- salary_min_lpa=32.0,
169
- salary_max_lpa=50.0,
170
- experience_min_years=4,
171
- experience_max_years=8,
172
- tech_stack=["Apache Spark", "PySpark", "Snowflake", "Kafka", "Airflow", "Python", "SQL"],
173
- requirements=(
174
- "Zomato Gurgaon R&D is hiring a Data Platform Lead. Build petabyte-scale streaming & batch data pipelines for real-time order dispatch analytics. "
175
- "Key tech: Apache Spark, Snowflake, Airflow DAG orchestration, and Kafka stream processing."
176
- ),
177
- work_mode="Hybrid",
178
- company_tier="Indian Consumer Tech Leader",
179
- posted_date="2026-08-06",
180
- url="https://zomato.com/careers/gurgaon-data-lead"
181
- ),
182
-
183
- # AI/ML & GENAI
184
- JobPosting(
185
- id="PUNE-AIML-501",
186
- title="Senior GenAI & FastMCP Systems Engineer",
187
- company="NVIDIA India R&D",
188
- city="Pune",
189
- area="Baner-Pashan",
190
- tech_domain="AI/ML & GenAI",
191
- salary_min_lpa=35.0,
192
- salary_max_lpa=55.0,
193
- experience_min_years=3,
194
- experience_max_years=7,
195
- tech_stack=["FastMCP", "vLLM", "CUDA", "PyTorch", "LangGraph", "Qdrant", "Python"],
196
- requirements=(
197
- "NVIDIA R&D Pune is hiring an AI Systems Engineer. Build high-throughput LLM serving infrastructure using vLLM "
198
- "and expose GPU tool-calling capabilities via Model Context Protocol (MCP). Requires strong Python, PyTorch, CUDA, and RAG vector store experience."
199
- ),
200
- work_mode="Hybrid",
201
- company_tier="Global AI Pioneer",
202
- posted_date="2026-08-05",
203
- url="https://nvidia.com/careers/pune-genai-mcp"
204
- ),
205
- JobPosting(
206
- id="BLR-AIML-502",
207
- title="Staff LLM & Agentic AI Specialist",
208
- company="Flipkart AI Labs",
209
- city="Bengaluru",
210
- area="Electronic City",
211
- tech_domain="AI/ML & GenAI",
212
- salary_min_lpa=42.0,
213
- salary_max_lpa=66.0,
214
- experience_min_years=4,
215
- experience_max_years=9,
216
- tech_stack=["LangGraph", "FastMCP", "Unsloth", "DeepSpeed", "Qdrant", "FastAPI"],
217
- requirements=(
218
- "Flipkart AI Labs Bengaluru is seeking a Staff Agentic AI Engineer. Build autonomous shopping assistant agents using LangGraph, "
219
- "fine-tune Llama-3 models with PEFT/Unsloth, and integrate tool calling via FastMCP protocols."
220
- ),
221
- work_mode="Hybrid",
222
- company_tier="E-Commerce Leader",
223
- posted_date="2026-08-08",
224
- url="https://flipkart.com/careers/blr-agentic-ai"
225
- ),
226
-
227
- # MOBILE ENGINEERING
228
- JobPosting(
229
- id="CHN-MOBILE-601",
230
- title="Lead Android & Mobile Architect (Kotlin/Flutter)",
231
- company="Zoho Corporation",
232
- city="Chennai",
233
- area="Estancia IT Park",
234
- tech_domain="Mobile Engineering",
235
- salary_min_lpa=25.0,
236
- salary_max_lpa=42.0,
237
- experience_min_years=4,
238
- experience_max_years=8,
239
- tech_stack=["Kotlin", "Jetpack Compose", "Flutter", "Android SDK", "Coroutines", "Clean Architecture"],
240
- requirements=(
241
- "Zoho Chennai is hiring a Mobile Systems Architect. Lead the development of enterprise mobile applications downloaded by millions worldwide. "
242
- "Expertise in Kotlin, Jetpack Compose, state management (MVI/MVVM), offline-first architecture, and cross-platform Flutter."
243
- ),
244
- work_mode="On-site",
245
- company_tier="SaaS Pioneer",
246
- posted_date="2026-08-03",
247
- url="https://zoho.com/careers/chennai-mobile-lead"
248
- )
249
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/scrapers/seeder.py DELETED
@@ -1,36 +0,0 @@
1
- from tech_radar.db.database import DatabaseManager
2
- from tech_radar.db.vector_store import SemanticVectorStore
3
- from tech_radar.scrapers.mock_data import UNIVERSAL_TECH_JOBS
4
- from tech_radar.scrapers.live_scraper import LiveInternetScraper
5
-
6
- def seed_database(db_path: str = "tech_radar.db", fetch_live: bool = True) -> tuple[DatabaseManager, SemanticVectorStore]:
7
- """Seed SQLite database and vector store with curated + live real-time internet job postings."""
8
- db = DatabaseManager(db_path=db_path)
9
- vector_store = SemanticVectorStore()
10
-
11
- count = 0
12
- # 1. Seed base curated tech jobs
13
- for job in UNIVERSAL_TECH_JOBS:
14
- db.save_job_posting(job)
15
- count += 1
16
-
17
- # 2. Fetch live real-time internet jobs
18
- if fetch_live:
19
- try:
20
- scraper = LiveInternetScraper()
21
- live_jobs = scraper.fetch_all_live_jobs()
22
- for l_job in live_jobs:
23
- db.save_job_posting(l_job)
24
- count += 1
25
- except Exception as e:
26
- print(f"[Seeder] Live internet scraping warning: {e}")
27
-
28
- all_jobs = db.get_all_jobs()
29
- vector_store.index_jobs(all_jobs)
30
-
31
- print(f" Successfully seeded {count} total job postings into {db_path}.")
32
- print(f" Indexed {len(all_jobs)} jobs across all domains in Vector Engine.")
33
- return db, vector_store
34
-
35
- if __name__ == "__main__":
36
- seed_database(fetch_live=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/ui/app.py DELETED
@@ -1,340 +0,0 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import plotly.express as px
4
- import json
5
- import sys
6
- import os
7
-
8
- sys.path.insert(0, os.path.abspath("."))
9
-
10
- from tech_radar.db.database import DatabaseManager
11
- from tech_radar.db.vector_store import SemanticVectorStore
12
- from tech_radar.agents.evaluator_agent import EvaluatorAgent
13
- from tech_radar.agents.market_analyst import MarketAnalystAgent
14
- from tech_radar.scrapers.seeder import seed_database
15
- from tech_radar.mcp.tools import (
16
- tool_search_tech_jobs,
17
- tool_analyze_skill_gap,
18
- tool_generate_resume_patch,
19
- tool_get_market_insights,
20
- tool_generate_interview_prep_kit
21
- )
22
-
23
- st.set_page_config(
24
- page_title="TechRadar MCP | 3D Cyber Hiring Intelligence",
25
- page_icon="⚡",
26
- layout="wide",
27
- initial_sidebar_state="expanded"
28
- )
29
-
30
- # Custom CSS matching 5718062.jpg reference & Fonts.txt (Bebas Neue + Poppins)
31
- st.markdown("""
32
- <style>
33
- @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:wght@300;400;500;600;700;800&display=swap');
34
-
35
- html, body, [class*="css"] {
36
- font-family: 'Poppins', sans-serif;
37
- }
38
-
39
- .main-header {
40
- font-family: 'Bebas Neue', sans-serif;
41
- font-size: 3.2rem;
42
- letter-spacing: 3px;
43
- background: linear-gradient(90deg, #00e5ff, #9d4edd, #e94057);
44
- -webkit-background-clip: text;
45
- -webkit-text-fill-color: transparent;
46
- margin-bottom: 0.1rem;
47
- text-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
48
- }
49
-
50
- .sub-header {
51
- font-size: 1.05rem;
52
- color: #a0aec0;
53
- margin-bottom: 1.8rem;
54
- }
55
-
56
- /* 3D Glassmorphic Job Cards */
57
- .job-card-3d {
58
- background: rgba(20, 14, 45, 0.7);
59
- backdrop-filter: blur(20px);
60
- border: 1px solid rgba(0, 229, 255, 0.25);
61
- border-radius: 18px;
62
- padding: 24px;
63
- margin-bottom: 20px;
64
- box-shadow: 0 10px 30px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255, 255, 255, 0.1);
65
- transition: all 0.3s ease;
66
- }
67
-
68
- .job-card-3d:hover {
69
- border-color: #00e5ff;
70
- box-shadow: 0 15px 35px rgba(0, 229, 255, 0.3);
71
- }
72
-
73
- .badge-cyan {
74
- background: rgba(0, 229, 255, 0.15);
75
- border: 1px solid rgba(0, 229, 255, 0.4);
76
- color: #00e5ff;
77
- padding: 4px 12px;
78
- border-radius: 50px;
79
- font-size: 0.8rem;
80
- font-weight: 600;
81
- }
82
-
83
- .badge-purple {
84
- background: rgba(157, 78, 221, 0.2);
85
- border: 1px solid rgba(157, 78, 221, 0.5);
86
- color: #d8b4fe;
87
- padding: 4px 12px;
88
- border-radius: 50px;
89
- font-size: 0.8rem;
90
- font-weight: 600;
91
- }
92
-
93
- .badge-stack {
94
- background: rgba(255, 255, 255, 0.06);
95
- border: 1px solid rgba(255, 255, 255, 0.12);
96
- color: #e2e8f0;
97
- padding: 3px 9px;
98
- border-radius: 6px;
99
- font-size: 0.75rem;
100
- margin-right: 6px;
101
- }
102
-
103
- .stButton>button {
104
- background: linear-gradient(90deg, #00e5ff, #9d4edd);
105
- color: white;
106
- font-family: 'Poppins', sans-serif;
107
- font-weight: 700;
108
- border-radius: 50px;
109
- border: none;
110
- padding: 10px 28px;
111
- box-shadow: 0 0 20px rgba(0, 229, 255, 0.4);
112
- transition: all 0.3s ease;
113
- }
114
- </style>
115
- """, unsafe_allow_html=True)
116
-
117
- @st.cache_resource
118
- def load_data_and_engines():
119
- return seed_database()
120
-
121
- db, vector_store = load_data_and_engines()
122
- evaluator = EvaluatorAgent()
123
- market_analyst = MarketAnalystAgent(db)
124
-
125
- # Sidebar
126
- st.sidebar.image("https://raw.githubusercontent.com/modelcontextprotocol/mcp/main/docs/assets/mcp-logo.png", width=120)
127
- st.sidebar.title("⚡ TechRadar MCP")
128
- st.sidebar.markdown("**Universal Hiring Intelligence**")
129
-
130
- target_domain = st.sidebar.selectbox(
131
- "Filter Tech Domain",
132
- ["All", "Backend Engineering", "Frontend Engineering", "Full Stack Engineering", "Cloud & DevOps", "Data Engineering", "AI/ML & GenAI", "Mobile Engineering"]
133
- )
134
-
135
- target_city = st.sidebar.selectbox(
136
- "Filter City / Region",
137
- ["All", "Bengaluru", "Pune", "Hyderabad", "Gurgaon", "Mumbai", "Chennai", "Remote"]
138
- )
139
-
140
- selected_tab = st.sidebar.radio(
141
- "Navigation",
142
- [
143
- "📡 Universal Tech Job Radar",
144
- "📊 Market & Salary Analytics",
145
- "🎯 ATS Resume & Skill Gap Analyzer",
146
- "🧪 Interactive MCP Server Tester"
147
- ]
148
- )
149
-
150
- # Header
151
- st.markdown('<div class="main-header">UNIVERSAL TECH HIRING INTELLIGENCE</div>', unsafe_allow_html=True)
152
- st.markdown('<div class="sub-header">3D Cyber-Glassmorphic Model Context Protocol Ecosystem across All Tech Domains & Cities</div>', unsafe_allow_html=True)
153
-
154
- # TAB 1: TECH JOB RADAR
155
- if selected_tab == "📡 Universal Tech Job Radar":
156
- all_jobs = db.search_jobs(domain=target_domain, city=target_city, limit=300)
157
-
158
- col1, col2, col3, col4 = st.columns(4)
159
- with col1:
160
- st.metric("Total Active Jobs", len(all_jobs))
161
- with col2:
162
- remote_count = len([j for j in all_jobs if j.work_mode.lower() == "remote"])
163
- st.metric("Remote Roles", remote_count)
164
- with col3:
165
- avg_max_sal = round(sum(j.salary_max_lpa for j in all_jobs) / (len(all_jobs) or 1), 1)
166
- st.metric("Avg Max Salary", f"₹{avg_max_sal} LPA")
167
- with col4:
168
- top_sal = max([j.salary_max_lpa for j in all_jobs] or [0])
169
- st.metric("Highest Package", f"₹{top_sal} LPA")
170
-
171
- st.divider()
172
-
173
- search_col1, search_col2 = st.columns([3, 1])
174
- with search_col1:
175
- query = st.text_input("🔍 Semantic Tech Search (e.g., 'Go Distributed Systems Bellandur', 'React Next.js Remote')", "")
176
- with search_col2:
177
- exp_filter = st.slider("Max Experience (Years)", 0, 12, 10)
178
-
179
- if query:
180
- semantic_results = vector_store.search_semantic(query=query, domain=target_domain, city=target_city, top_k=25)
181
- jobs_to_display = [j for j, score in semantic_results]
182
- else:
183
- jobs_to_display = [j for j in all_jobs if j.experience_min_years <= exp_filter]
184
-
185
- st.subheader(f"Showing {len(jobs_to_display)} Matching Roles")
186
-
187
- for job in jobs_to_display:
188
- with st.container():
189
- st.markdown(f"""
190
- <div class="job-card-3d">
191
- <div style="display: flex; justify-content: space-between; align-items: center;">
192
- <h3 style="margin: 0; color: #ffffff; font-weight: 700;">{job.title}</h3>
193
- <div>
194
- <span class="badge-cyan">📍 {job.city} ({job.area})</span>
195
- <span class="badge-purple">💻 {job.tech_domain}</span>
196
- </div>
197
- </div>
198
- <h4 style="margin: 6px 0; color: #cbd5e0;">🏢 {job.company} &nbsp;|&nbsp; 💼 {job.company_tier}</h4>
199
- <p style="color: #00ff9d; font-weight: bold; margin: 6px 0;">💰 ₹{job.salary_min_lpa}L - ₹{job.salary_max_lpa}L PA &nbsp;|&nbsp; ⏳ {job.experience_min_years}-{job.experience_max_years} Yrs Exp &nbsp;|&nbsp; 🌐 {job.work_mode}</p>
200
- <p style="color: #a0aec0; font-size: 0.9rem; margin-bottom: 12px;">{job.requirements[:280]}...</p>
201
- <div style="margin-top: 10px;">
202
- {' '.join([f'<span class="badge-stack">{stack}</span>' for stack in job.tech_stack])}
203
- </div>
204
- <div style="margin-top: 12px; font-size: 0.8rem; color: #718096;">
205
- <code>Job ID: {job.id}</code> &nbsp;|&nbsp; Posted: {job.posted_date}
206
- </div>
207
- </div>
208
- """, unsafe_allow_html=True)
209
-
210
- # TAB 2: MARKET ANALYTICS
211
- elif selected_tab == "📊 Market & Salary Analytics":
212
- st.subheader(f"Market Intelligence Report — {target_domain} ({target_city})")
213
- insights = market_analyst.generate_market_report(city=target_city, domain=target_domain)
214
-
215
- col1, col2 = st.columns(2)
216
- with col1:
217
- st.markdown("### 🚀 Hiring Summary")
218
- st.write(f"- **Active Roles Tracked**: `{insights.total_active_jobs}`")
219
- st.write(f"- **Average Salary**: `{insights.avg_salary_lpa} LPA`")
220
- st.write(f"- **Salary Range**: `{insights.salary_range}`")
221
- st.info(insights.growth_trend)
222
-
223
- st.markdown("### 🏢 Top Employers Hiring Tech Talent")
224
- for emp in insights.top_employers:
225
- st.markdown(f"- **{emp}**")
226
-
227
- with col2:
228
- st.markdown("### 🔥 Top Demanded Tech Skills & Frameworks")
229
- df_frameworks = pd.DataFrame(insights.top_demanded_frameworks)
230
- if not df_frameworks.empty:
231
- fig = px.bar(
232
- df_frameworks,
233
- x="percentage",
234
- y="skill",
235
- orientation="h",
236
- title="Skill Demand % in Selected Market",
237
- labels={"percentage": "Job Postings Demand (%)", "skill": "Skill"},
238
- color="percentage",
239
- color_continuous_scale="Purples"
240
- )
241
- fig.update_layout(darkmode=True, yaxis={'categoryorder':'total ascending'})
242
- st.plotly_chart(fig, use_container_width=True)
243
-
244
- # TAB 3: ATS RESUME ANALYZER
245
- elif selected_tab == "🎯 ATS Resume & Skill Gap Analyzer":
246
- st.subheader("🎯 Candidate-to-JD Skill Gap & ATS Analyzer")
247
-
248
- col1, col2 = st.columns([1, 1])
249
-
250
- with col1:
251
- sample_resume = """
252
- SOFTWARE ENGINEER | BENGALURU
253
- 3+ years experience building backend REST microservices in Go and Python.
254
- Built relational databases in PostgreSQL, cached data with Redis, and deployed Docker containers on AWS.
255
- Looking for Senior Backend, Full Stack, or Cloud roles across Bengaluru, Pune, and Remote.
256
- """
257
- resume_input = st.text_area("Paste Candidate Resume Text", sample_resume, height=220)
258
-
259
- all_jobs = db.get_all_jobs()
260
- job_options = {f"{j.id} — {j.title} at {j.company} ({j.city})": j.id for j in all_jobs}
261
- selected_job_label = st.selectbox("Select Target Job Opening", list(job_options.keys()))
262
- selected_job_id = job_options[selected_job_label]
263
-
264
- analyze_btn = st.button("🚀 Analyze Skill Gap & Generate Patch", type="primary")
265
-
266
- with col2:
267
- if analyze_btn:
268
- job = db.get_job_by_id(selected_job_id)
269
- report = evaluator.evaluate_skill_gap(resume_text=resume_input, candidate_skills=["Go", "Python", "PostgreSQL", "Redis", "Docker", "AWS"], job=job)
270
- patch = evaluator.generate_resume_patch(resume_text=resume_input, job=job)
271
- prep = evaluator.generate_interview_prep(job=job)
272
-
273
- st.markdown(f"### Match Score: `{report.match_percentage}%`")
274
- st.progress(report.match_percentage / 100.0)
275
-
276
- st.markdown("#### ✅ Matched Skills")
277
- st.write(", ".join([f"`{s}`" for s in report.matched_skills]) or "None")
278
-
279
- st.markdown("#### ❌ Missing Skills (Dealbreakers)")
280
- st.write(", ".join([f"`{s}`" for s in report.missing_skills]) or "None")
281
-
282
- st.markdown("#### 📝 Tailored ATS Resume Bullets (Diff)")
283
- for item in patch.tailored_bullets:
284
- st.warning(f"**Original**: {item['original']}\n\n**Tailored**: {item['tailored']}\n\n*Rationale*: {item['rationale']}")
285
-
286
- st.markdown("#### ❓ Sample Technical Interview Question")
287
- if prep.technical_questions:
288
- q = prep.technical_questions[0]
289
- st.info(f"**Q**: {q.question}\n\n**Key Points**: {', '.join(q.ideal_answer_points)}")
290
-
291
- # TAB 4: INTERACTIVE MCP TESTER
292
- elif selected_tab == "🧪 Interactive MCP Server Tester":
293
- st.subheader("🧪 Live FastMCP Server Tool Execution Sandbox")
294
- st.markdown("Test the exact JSON-RPC response generated by FastMCP tools for Claude Desktop & Cursor.")
295
-
296
- mcp_tool = st.selectbox(
297
- "Select MCP Tool",
298
- [
299
- "search_tech_jobs",
300
- "analyze_skill_gap",
301
- "generate_tailored_resume_patch",
302
- "get_market_insights",
303
- "generate_interview_prep_kit"
304
- ]
305
- )
306
-
307
- if mcp_tool == "search_tech_jobs":
308
- c_dom = st.selectbox("domain", ["All", "Backend Engineering", "Frontend Engineering", "Full Stack Engineering", "Cloud & DevOps", "AI/ML & GenAI"])
309
- c_city = st.selectbox("city", ["All", "Bengaluru", "Pune", "Hyderabad", "Gurgaon", "Remote"])
310
- c_query = st.text_input("query", "Go Distributed Systems")
311
- if st.button("Execute Tool"):
312
- res = tool_search_tech_jobs(domain=c_dom, city=c_city, query=c_query)
313
- st.code(res, language="json")
314
-
315
- elif mcp_tool == "analyze_skill_gap":
316
- c_res = st.text_area("resume_text", "Backend developer experienced in Go, Docker, and PostgreSQL.")
317
- c_jid = st.text_input("target_job_id", "BLR-BACKEND-101")
318
- if st.button("Execute Tool"):
319
- res = tool_analyze_skill_gap(resume_text=c_res, target_job_id=c_jid)
320
- st.code(res, language="json")
321
-
322
- elif mcp_tool == "generate_tailored_resume_patch":
323
- c_res = st.text_area("resume_text", "Built REST APIs in Python.")
324
- c_jid = st.text_input("target_job_id", "PUNE-FULLSTACK-202")
325
- if st.button("Execute Tool"):
326
- res = tool_generate_resume_patch(resume_text=c_res, target_job_id=c_jid)
327
- st.code(res, language="json")
328
-
329
- elif mcp_tool == "get_market_insights":
330
- c_dom = st.selectbox("domain", ["All", "Backend Engineering", "Frontend Engineering", "Cloud & DevOps"])
331
- c_city = st.selectbox("city", ["All", "Bengaluru", "Pune", "Hyderabad"])
332
- if st.button("Execute Tool"):
333
- res = tool_get_market_insights(domain=c_dom, city=c_city)
334
- st.code(res, language="json")
335
-
336
- elif mcp_tool == "generate_interview_prep_kit":
337
- c_jid = st.text_input("target_job_id", "BLR-FRONTEND-201")
338
- if st.button("Execute Tool"):
339
- res = tool_generate_interview_prep_kit(target_job_id=c_jid)
340
- st.code(res, language="json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/ui/static_server.py DELETED
@@ -1,76 +0,0 @@
1
- import json
2
- import os
3
- from fastapi import FastAPI, HTTPException
4
- from fastapi.responses import HTMLResponse, JSONResponse
5
- from pydantic import BaseModel
6
- from typing import Dict, Any, Optional
7
-
8
- from tech_radar.mcp.tools import (
9
- get_db_and_vector_store,
10
- tool_search_tech_jobs,
11
- tool_analyze_skill_gap,
12
- tool_generate_resume_patch,
13
- tool_get_market_insights,
14
- tool_generate_interview_prep_kit
15
- )
16
-
17
- app = FastAPI(title="TechRadar MCP Web Server")
18
-
19
- class McpRequest(BaseModel):
20
- tool: str
21
- args: Dict[str, Any]
22
-
23
- @app.get("/", response_class=HTMLResponse)
24
- def get_web_app():
25
- html_path = os.path.join(os.path.dirname(__file__), "web_app.html")
26
- if not os.path.exists(html_path):
27
- raise HTTPException(status_code=404, detail="web_app.html not found")
28
- with open(html_path, "r", encoding="utf-8") as f:
29
- return f.read()
30
-
31
- @app.get("/api/jobs")
32
- def get_jobs(domain: Optional[str] = None, city: Optional[str] = None):
33
- db, _, _, _ = get_db_and_vector_store()
34
- jobs = [j.dict() for j in db.search_jobs(domain=domain, city=city, limit=200)]
35
- return {"count": len(jobs), "jobs": jobs}
36
-
37
- @app.get("/api/insights")
38
- def get_insights(city: str = "All", domain: str = "All"):
39
- _, _, _, analyst = get_db_and_vector_store()
40
- report = analyst.generate_market_report(city=city, domain=domain)
41
- return report.dict()
42
-
43
- @app.post("/api/mcp/execute")
44
- def execute_mcp_tool(req: McpRequest):
45
- tool_name = req.tool
46
- args = req.args
47
-
48
- if tool_name == "search_tech_jobs":
49
- res = tool_search_tech_jobs(
50
- domain=args.get("domain", "All"),
51
- city=args.get("city", "All"),
52
- query=args.get("query")
53
- )
54
- elif tool_name == "analyze_skill_gap":
55
- res = tool_analyze_skill_gap(
56
- resume_text=args.get("resume_text", ""),
57
- target_job_id=args.get("target_job_id", "")
58
- )
59
- elif tool_name == "generate_tailored_resume_patch":
60
- res = tool_generate_resume_patch(
61
- resume_text=args.get("resume_text", ""),
62
- target_job_id=args.get("target_job_id", "")
63
- )
64
- elif tool_name == "get_market_insights":
65
- res = tool_get_market_insights(
66
- domain=args.get("domain", "All"),
67
- city=args.get("city", "All")
68
- )
69
- elif tool_name == "generate_interview_prep_kit":
70
- res = tool_generate_interview_prep_kit(
71
- target_job_id=args.get("target_job_id", "")
72
- )
73
- else:
74
- raise HTTPException(status_code=400, detail=f"Unknown tool: {tool_name}")
75
-
76
- return {"status": "success", "tool": tool_name, "result": res}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_radar/tech_radar/ui/web_app.html DELETED
@@ -1,966 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TechRadar MCP — Universal Tech Hiring Intelligence</title>
7
- <!-- Google Fonts: Bebas Neue & Poppins -->
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
11
- <!-- Chart.js for Market Analytics -->
12
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
- <style>
14
- :root {
15
- --bg-dark: #090514;
16
- --bg-card: rgba(20, 14, 45, 0.65);
17
- --border-glass: rgba(0, 229, 255, 0.25);
18
- --border-glow: #00e5ff;
19
- --accent-cyan: #00e5ff;
20
- --accent-purple: #9d4edd;
21
- --accent-pink: #e94057;
22
- --text-main: #f0f4f8;
23
- --text-muted: #a0aec0;
24
- }
25
-
26
- * {
27
- box-sizing: border-box;
28
- margin: 0;
29
- padding: 0;
30
- }
31
-
32
- body {
33
- background-color: var(--bg-dark);
34
- color: var(--text-main);
35
- font-family: 'Poppins', sans-serif;
36
- overflow-x: hidden;
37
- min-height: 100vh;
38
- }
39
-
40
- /* Canvas 3D Cyber Wave Grid Background */
41
- #cyber-canvas {
42
- position: fixed;
43
- top: 0;
44
- left: 0;
45
- width: 100vw;
46
- height: 100vh;
47
- z-index: -1;
48
- pointer-events: none;
49
- }
50
-
51
- /* Main Wrapper */
52
- .app-container {
53
- max-width: 1350px;
54
- margin: 0 auto;
55
- padding: 20px 30px 80px 30px;
56
- }
57
-
58
- /* Navbar */
59
- .navbar {
60
- display: flex;
61
- justify-content: space-between;
62
- align-items: center;
63
- padding: 20px 0;
64
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
65
- margin-bottom: 30px;
66
- }
67
-
68
- .brand-logo {
69
- display: flex;
70
- align-items: center;
71
- gap: 12px;
72
- font-family: 'Bebas Neue', sans-serif;
73
- font-size: 2.2rem;
74
- letter-spacing: 2px;
75
- background: linear-gradient(90deg, #00e5ff, #9d4edd, #e94057);
76
- -webkit-background-clip: text;
77
- -webkit-text-fill-color: transparent;
78
- }
79
-
80
- .mcp-status-pill {
81
- background: rgba(0, 229, 255, 0.1);
82
- border: 1px solid var(--accent-cyan);
83
- color: var(--accent-cyan);
84
- padding: 6px 16px;
85
- border-radius: 50px;
86
- font-size: 0.85rem;
87
- font-weight: 600;
88
- display: flex;
89
- align-items: center;
90
- gap: 8px;
91
- box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
92
- }
93
-
94
- .status-dot {
95
- width: 8px;
96
- height: 8px;
97
- background-color: var(--accent-cyan);
98
- border-radius: 50%;
99
- animation: pulse-dot 1.5s infinite;
100
- }
101
-
102
- @keyframes pulse-dot {
103
- 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 229, 255, 0.7); }
104
- 70% { transform: scale(1); box-shadow: 0 0 0 8px rgba(0, 229, 255, 0); }
105
- 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 229, 255, 0); }
106
- }
107
-
108
- /* Hero Banner Section (Inspired by reference 5718062.jpg) */
109
- .hero-banner {
110
- position: relative;
111
- background: linear-gradient(135deg, rgba(20, 14, 50, 0.8) 0%, rgba(10, 6, 30, 0.9) 100%);
112
- border: 1px solid var(--border-glass);
113
- backdrop-filter: blur(20px);
114
- border-radius: 24px;
115
- padding: 45px 50px;
116
- display: flex;
117
- align-items: center;
118
- justify-content: space-between;
119
- margin-bottom: 40px;
120
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.15);
121
- overflow: hidden;
122
- }
123
-
124
- .hero-banner::before {
125
- content: '';
126
- position: absolute;
127
- top: -50%;
128
- right: -10%;
129
- width: 400px;
130
- height: 400px;
131
- background: radial-gradient(circle, rgba(157, 78, 221, 0.3) 0%, transparent 70%);
132
- pointer-events: none;
133
- }
134
-
135
- .hero-text {
136
- max-width: 650px;
137
- z-index: 2;
138
- }
139
-
140
- .hero-tag {
141
- text-transform: uppercase;
142
- font-size: 0.9rem;
143
- letter-spacing: 3px;
144
- color: var(--accent-cyan);
145
- font-weight: 700;
146
- margin-bottom: 10px;
147
- }
148
-
149
- .hero-title {
150
- font-family: 'Bebas Neue', sans-serif;
151
- font-size: 4rem;
152
- line-height: 1;
153
- letter-spacing: 3px;
154
- margin-bottom: 15px;
155
- background: linear-gradient(90deg, #ffffff, #e2e8f0);
156
- -webkit-background-clip: text;
157
- -webkit-text-fill-color: transparent;
158
- text-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
159
- }
160
-
161
- .hero-subtitle {
162
- font-size: 1.05rem;
163
- color: var(--text-muted);
164
- margin-bottom: 25px;
165
- line-height: 1.6;
166
- }
167
-
168
- .btn-gradient {
169
- background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
170
- color: white;
171
- padding: 14px 34px;
172
- border-radius: 50px;
173
- font-size: 0.95rem;
174
- font-weight: 700;
175
- border: none;
176
- cursor: pointer;
177
- text-transform: uppercase;
178
- letter-spacing: 1px;
179
- transition: all 0.3s ease;
180
- box-shadow: 0 0 25px rgba(0, 229, 255, 0.4);
181
- display: inline-flex;
182
- align-items: center;
183
- gap: 10px;
184
- }
185
-
186
- .btn-gradient:hover {
187
- transform: translateY(-3px) scale(1.02);
188
- box-shadow: 0 0 35px rgba(157, 78, 221, 0.6);
189
- }
190
-
191
- /* 3D Visual Orb / Graphics */
192
- .hero-graphic {
193
- position: relative;
194
- width: 320px;
195
- height: 220px;
196
- display: flex;
197
- align-items: center;
198
- justify-content: center;
199
- z-index: 2;
200
- }
201
-
202
- .orb-ring {
203
- position: absolute;
204
- border-radius: 50%;
205
- border: 2px dashed rgba(0, 229, 255, 0.4);
206
- animation: spin 20s linear infinite;
207
- }
208
-
209
- .orb-ring-1 { width: 220px; height: 220px; border-color: rgba(0, 229, 255, 0.5); }
210
- .orb-ring-2 { width: 170px; height: 170px; border-color: rgba(157, 78, 221, 0.6); animation-direction: reverse; animation-duration: 15s; }
211
-
212
- .orb-core {
213
- width: 110px;
214
- height: 110px;
215
- border-radius: 50%;
216
- background: radial-gradient(circle at 30% 30%, #00e5ff, #9d4edd 70%, #090514);
217
- box-shadow: 0 0 50px rgba(0, 229, 255, 0.6);
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- font-family: 'Bebas Neue', sans-serif;
222
- font-size: 2rem;
223
- color: white;
224
- text-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
225
- }
226
-
227
- @keyframes spin {
228
- from { transform: rotate(0deg); }
229
- to { transform: rotate(360deg); }
230
- }
231
-
232
- /* Controls & Filter Bar */
233
- .filter-section {
234
- background: var(--bg-card);
235
- backdrop-filter: blur(15px);
236
- border: 1px solid var(--border-glass);
237
- border-radius: 16px;
238
- padding: 20px 25px;
239
- margin-bottom: 30px;
240
- display: flex;
241
- flex-wrap: wrap;
242
- gap: 20px;
243
- align-items: center;
244
- justify-content: space-between;
245
- }
246
-
247
- .filter-group {
248
- display: flex;
249
- align-items: center;
250
- gap: 10px;
251
- flex-wrap: wrap;
252
- }
253
-
254
- .filter-label {
255
- font-size: 0.85rem;
256
- font-weight: 600;
257
- color: var(--text-muted);
258
- text-transform: uppercase;
259
- letter-spacing: 1px;
260
- }
261
-
262
- .pill-btn {
263
- background: rgba(255, 255, 255, 0.05);
264
- border: 1px solid rgba(255, 255, 255, 0.1);
265
- color: var(--text-main);
266
- padding: 8px 16px;
267
- border-radius: 30px;
268
- font-size: 0.85rem;
269
- font-weight: 500;
270
- cursor: pointer;
271
- transition: all 0.25s ease;
272
- }
273
-
274
- .pill-btn:hover, .pill-btn.active {
275
- background: linear-gradient(90deg, rgba(0, 229, 255, 0.2), rgba(157, 78, 221, 0.2));
276
- border-color: var(--accent-cyan);
277
- color: white;
278
- box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
279
- }
280
-
281
- .search-input {
282
- background: rgba(10, 6, 25, 0.8);
283
- border: 1px solid var(--border-glass);
284
- color: white;
285
- padding: 10px 20px;
286
- border-radius: 30px;
287
- font-size: 0.9rem;
288
- width: 280px;
289
- outline: none;
290
- transition: all 0.3s ease;
291
- }
292
-
293
- .search-input:focus {
294
- border-color: var(--accent-cyan);
295
- box-shadow: 0 0 20px rgba(0, 229, 255, 0.4);
296
- }
297
-
298
- /* Navigation Tabs */
299
- .tab-menu {
300
- display: flex;
301
- gap: 15px;
302
- margin-bottom: 30px;
303
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
304
- padding-bottom: 10px;
305
- }
306
-
307
- .tab-item {
308
- font-family: 'Bebas Neue', sans-serif;
309
- font-size: 1.5rem;
310
- letter-spacing: 1.5px;
311
- color: var(--text-muted);
312
- cursor: pointer;
313
- padding: 8px 16px;
314
- border-radius: 8px;
315
- transition: all 0.3s ease;
316
- }
317
-
318
- .tab-item:hover, .tab-item.active {
319
- color: var(--accent-cyan);
320
- background: rgba(0, 229, 255, 0.08);
321
- text-shadow: 0 0 10px rgba(0, 229, 255, 0.5);
322
- }
323
-
324
- /* Tab Content Panes */
325
- .tab-pane {
326
- display: none;
327
- }
328
-
329
- .tab-pane.active {
330
- display: block;
331
- }
332
-
333
- /* Job Grid & 3D Glass Cards */
334
- .job-grid {
335
- display: grid;
336
- grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
337
- gap: 25px;
338
- }
339
-
340
- .job-card-3d {
341
- background: var(--bg-card);
342
- backdrop-filter: blur(20px);
343
- border: 1px solid var(--border-glass);
344
- border-radius: 20px;
345
- padding: 25px;
346
- transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
347
- position: relative;
348
- overflow: hidden;
349
- transform-style: preserve-3d;
350
- perspective: 1000px;
351
- }
352
-
353
- .job-card-3d:hover {
354
- transform: translateY(-8px) rotateX(2deg) rotateY(-2deg);
355
- border-color: var(--accent-cyan);
356
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(0, 229, 255, 0.3);
357
- }
358
-
359
- .job-card-header {
360
- display: flex;
361
- justify-content: space-between;
362
- align-items: flex-start;
363
- margin-bottom: 12px;
364
- }
365
-
366
- .job-title {
367
- font-family: 'Poppins', sans-serif;
368
- font-size: 1.2rem;
369
- font-weight: 700;
370
- color: #ffffff;
371
- line-height: 1.3;
372
- }
373
-
374
- .badge-location {
375
- background: rgba(0, 229, 255, 0.15);
376
- border: 1px solid rgba(0, 229, 255, 0.4);
377
- color: var(--accent-cyan);
378
- padding: 4px 10px;
379
- border-radius: 12px;
380
- font-size: 0.75rem;
381
- font-weight: 600;
382
- white-space: nowrap;
383
- }
384
-
385
- .job-company {
386
- font-size: 0.9rem;
387
- color: var(--text-muted);
388
- margin-bottom: 12px;
389
- display: flex;
390
- align-items: center;
391
- gap: 8px;
392
- }
393
-
394
- .job-salary {
395
- font-size: 1rem;
396
- font-weight: 700;
397
- color: #00ff9d;
398
- margin-bottom: 12px;
399
- }
400
-
401
- .job-desc {
402
- font-size: 0.85rem;
403
- color: #cbd5e0;
404
- line-height: 1.5;
405
- margin-bottom: 15px;
406
- display: -webkit-box;
407
- -webkit-line-clamp: 3;
408
- -webkit-box-orient: vertical;
409
- overflow: hidden;
410
- }
411
-
412
- .stack-tags {
413
- display: flex;
414
- flex-wrap: wrap;
415
- gap: 6px;
416
- margin-bottom: 15px;
417
- }
418
-
419
- .stack-tag {
420
- background: rgba(255, 255, 255, 0.06);
421
- border: 1px solid rgba(255, 255, 255, 0.12);
422
- color: var(--text-main);
423
- padding: 3px 8px;
424
- border-radius: 6px;
425
- font-size: 0.75rem;
426
- }
427
-
428
- .job-card-footer {
429
- display: flex;
430
- justify-content: space-between;
431
- align-items: center;
432
- font-size: 0.75rem;
433
- color: var(--text-muted);
434
- border-top: 1px solid rgba(255, 255, 255, 0.08);
435
- padding-top: 12px;
436
- }
437
-
438
- /* ATS & Skill Gap Section */
439
- .analyzer-container {
440
- display: grid;
441
- grid-template-columns: 1fr 1fr;
442
- gap: 30px;
443
- }
444
-
445
- .analyzer-box {
446
- background: var(--bg-card);
447
- backdrop-filter: blur(20px);
448
- border: 1px solid var(--border-glass);
449
- border-radius: 20px;
450
- padding: 30px;
451
- }
452
-
453
- .form-label {
454
- font-size: 0.9rem;
455
- font-weight: 600;
456
- color: var(--text-muted);
457
- margin-bottom: 8px;
458
- display: block;
459
- }
460
-
461
- .form-textarea {
462
- width: 100%;
463
- height: 180px;
464
- background: rgba(10, 6, 25, 0.8);
465
- border: 1px solid var(--border-glass);
466
- border-radius: 12px;
467
- padding: 15px;
468
- color: white;
469
- font-family: 'Poppins', sans-serif;
470
- font-size: 0.85rem;
471
- outline: none;
472
- resize: vertical;
473
- margin-bottom: 15px;
474
- }
475
-
476
- .form-select {
477
- width: 100%;
478
- background: rgba(10, 6, 25, 0.8);
479
- border: 1px solid var(--border-glass);
480
- border-radius: 10px;
481
- padding: 12px;
482
- color: white;
483
- font-family: 'Poppins', sans-serif;
484
- font-size: 0.85rem;
485
- outline: none;
486
- margin-bottom: 20px;
487
- }
488
-
489
- .score-circle {
490
- width: 110px;
491
- height: 110px;
492
- border-radius: 50%;
493
- border: 4px solid var(--accent-cyan);
494
- box-shadow: 0 0 25px rgba(0, 229, 255, 0.5);
495
- display: flex;
496
- align-items: center;
497
- justify-content: center;
498
- font-family: 'Bebas Neue', sans-serif;
499
- font-size: 2.5rem;
500
- color: var(--accent-cyan);
501
- margin: 0 auto 20px auto;
502
- }
503
-
504
- .diff-card {
505
- background: rgba(255, 255, 255, 0.03);
506
- border: 1px solid rgba(255, 204, 0, 0.3);
507
- border-radius: 10px;
508
- padding: 12px 15px;
509
- margin-bottom: 12px;
510
- font-size: 0.85rem;
511
- }
512
-
513
- .diff-original { color: #eb5757; margin-bottom: 4px; }
514
- .diff-tailored { color: #27ae60; font-weight: 600; }
515
-
516
- /* JSON Output Box */
517
- .json-box {
518
- background: #05030a;
519
- border: 1px solid var(--border-glass);
520
- border-radius: 12px;
521
- padding: 15px;
522
- font-family: 'Courier New', Courier, monospace;
523
- font-size: 0.85rem;
524
- color: #00e5ff;
525
- max-height: 400px;
526
- overflow-y: auto;
527
- white-space: pre-wrap;
528
- }
529
- </style>
530
- </head>
531
- <body>
532
-
533
- <!-- 3D Cyber Mesh Canvas Background -->
534
- <canvas id="cyber-canvas"></canvas>
535
-
536
- <div class="app-container">
537
- <!-- Top Navbar -->
538
- <div class="navbar">
539
- <div class="brand-logo">⚡ TECHRADAR MCP</div>
540
- <div class="mcp-status-pill">
541
- <span class="status-dot"></span> FASTMCP SERVER ACTIVE (STDIO/SSE)
542
- </div>
543
- </div>
544
-
545
- <!-- Hero Banner (3D Glassmorphism, matching 5718062.jpg) -->
546
- <div class="hero-banner">
547
- <div class="hero-text">
548
- <div class="hero-tag">MODEL CONTEXT PROTOCOL ECOSYSTEM</div>
549
- <h1 class="hero-title">UNIVERSAL TECH HIRING INTELLIGENCE</h1>
550
- <p class="hero-subtitle">
551
- Autonomous AI job market intelligence, AST skill-gap evaluation, and ATS resume patching across Bengaluru, Pune, Hyderabad, Gurgaon, Mumbai & Remote tech hubs.
552
- </p>
553
- <button class="btn-gradient" onclick="switchTab('radar')">
554
- 🚀 EXPLORE TECH RADAR
555
- </button>
556
- </div>
557
- <div class="hero-graphic">
558
- <div class="orb-ring orb-ring-1"></div>
559
- <div class="orb-ring orb-ring-2"></div>
560
- <div class="orb-core">MCP AI</div>
561
- </div>
562
- </div>
563
-
564
- <!-- Filter Controls -->
565
- <div class="filter-section">
566
- <div class="filter-group">
567
- <span class="filter-label">Domain:</span>
568
- <button class="pill-btn active" onclick="filterDomain('All', this)">ALL DOMAINS</button>
569
- <button class="pill-btn" onclick="filterDomain('Backend Engineering', this)">BACKEND</button>
570
- <button class="pill-btn" onclick="filterDomain('Frontend Engineering', this)">FRONTEND</button>
571
- <button class="pill-btn" onclick="filterDomain('Full Stack Engineering', this)">FULL STACK</button>
572
- <button class="pill-btn" onclick="filterDomain('Cloud & DevOps', this)">DEVOPS</button>
573
- <button class="pill-btn" onclick="filterDomain('AI/ML & GenAI', this)">AI / GENAI</button>
574
- </div>
575
-
576
- <div class="filter-group">
577
- <span class="filter-label">City:</span>
578
- <button class="pill-btn active" onclick="filterCity('All', this)">ALL CITIES</button>
579
- <button class="pill-btn" onclick="filterCity('Bengaluru', this)">BLR</button>
580
- <button class="pill-btn" onclick="filterCity('Pune', this)">PUNE</button>
581
- <button class="pill-btn" onclick="filterCity('Hyderabad', this)">HYD</button>
582
- <button class="pill-btn" onclick="filterCity('Remote', this)">REMOTE</button>
583
- </div>
584
-
585
- <input type="text" id="searchInput" class="search-input" placeholder="🔍 Search Go, React, vLLM..." onkeyup="handleSearch()">
586
- </div>
587
-
588
- <!-- Navigation Tabs -->
589
- <div class="tab-menu">
590
- <div class="tab-item active" onclick="switchTab('radar', this)">📡 UNIVERSAL TECH RADAR</div>
591
- <div class="tab-item" onclick="switchTab('analytics', this)">📊 MARKET ANALYTICS</div>
592
- <div class="tab-item" onclick="switchTab('ats', this)">🎯 ATS SKILL GAP EVALUATOR</div>
593
- <div class="tab-item" onclick="switchTab('mcp', this)">🧪 FASTMCP SERVER TESTER</div>
594
- </div>
595
-
596
- <!-- TAB 1: RADAR GRID -->
597
- <div id="tab-radar" class="tab-pane active">
598
- <div class="job-grid" id="jobGrid">
599
- <!-- Rendered dynamically via JS -->
600
- </div>
601
- </div>
602
-
603
- <!-- TAB 2: ANALYTICS -->
604
- <div id="tab-analytics" class="tab-pane">
605
- <div class="analyzer-container">
606
- <div class="analyzer-box">
607
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-cyan);">🔥 TOP DEMANDED TECH FRAMEWORKS</h3>
608
- <canvas id="skillsChart" height="220"></canvas>
609
- </div>
610
- <div class="analyzer-box">
611
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-purple);">🏢 MAJOR TECH HIRING HUBS</h3>
612
- <canvas id="hubsChart" height="220"></canvas>
613
- </div>
614
- </div>
615
- </div>
616
-
617
- <!-- TAB 3: ATS ANALYZER -->
618
- <div id="tab-ats" class="tab-pane">
619
- <div class="analyzer-container">
620
- <div class="analyzer-box">
621
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px;">INPUT CANDIDATE PROFILE</h3>
622
- <label class="form-label">Paste Resume Text:</label>
623
- <textarea id="resumeInput" class="form-textarea">Senior Backend Engineer with 4 years experience building Go microservices, REST APIs, and Docker containers in Bengaluru. Seeking Senior Go or Distributed Systems roles.</textarea>
624
-
625
- <label class="form-label">Select Target Job Opening:</label>
626
- <select id="jobSelect" class="form-select"></select>
627
-
628
- <button class="btn-gradient" style="width: 100%; justify-content: center;" onclick="runAtsEvaluation()">
629
- 🚀 EVALUATE ATS MATCH & SKILL GAP
630
- </button>
631
- </div>
632
-
633
- <div class="analyzer-box" id="atsResultBox">
634
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; text-align: center;">EVALUATION RESULT</h3>
635
- <div class="score-circle" id="matchScore">--</div>
636
-
637
- <h4 style="font-size: 0.95rem; margin-bottom: 5px; color: #00ff9d;">✅ Matched Skills:</h4>
638
- <p id="matchedSkills" style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 15px;">-</p>
639
-
640
- <h4 style="font-size: 0.95rem; margin-bottom: 5px; color: #eb5757;">❌ Missing Skills (Dealbreakers):</h4>
641
- <p id="missingSkills" style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 20px;">-</p>
642
-
643
- <h4 style="font-size: 0.95rem; margin-bottom: 10px; color: var(--accent-cyan);">📝 ATS Tailored Resume Bullets:</h4>
644
- <div id="tailoredBullets"></div>
645
- </div>
646
- </div>
647
- </div>
648
-
649
- <!-- TAB 4: MCP TESTER -->
650
- <div id="tab-mcp" class="tab-pane">
651
- <div class="analyzer-container">
652
- <div class="analyzer-box">
653
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px;">SELECT FASTMCP TOOL</h3>
654
- <select id="mcpToolSelect" class="form-select" onchange="updateMcpInputs()">
655
- <option value="search_tech_jobs">search_tech_jobs</option>
656
- <option value="analyze_skill_gap">analyze_skill_gap</option>
657
- <option value="generate_tailored_resume_patch">generate_tailored_resume_patch</option>
658
- <option value="get_market_insights">get_market_insights</option>
659
- <option value="generate_interview_prep_kit">generate_interview_prep_kit</option>
660
- </select>
661
-
662
- <div id="mcpInputFields"></div>
663
-
664
- <button class="btn-gradient" style="width: 100%; justify-content: center;" onclick="executeMcpTool()">
665
- ⚡ EXECUTE FASTMCP TOOL
666
- </button>
667
- </div>
668
-
669
- <div class="analyzer-box">
670
- <h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-cyan);">JSON-RPC TOOL OUTPUT</h3>
671
- <div class="json-box" id="mcpJsonOutput">// Execute an MCP tool to view response</div>
672
- </div>
673
- </div>
674
- </div>
675
- </div>
676
-
677
- <!-- Scripts for 3D Mesh Canvas & App Logic -->
678
- <script>
679
- // 1. Interactive 3D Cyber Dot Mesh Canvas (Matching 5718062.jpg)
680
- const canvas = document.getElementById('cyber-canvas');
681
- const ctx = canvas.getContext('2d');
682
- let width = canvas.width = window.innerWidth;
683
- let height = canvas.height = window.innerHeight;
684
-
685
- window.addEventListener('resize', () => {
686
- width = canvas.width = window.innerWidth;
687
- height = canvas.height = window.innerHeight;
688
- });
689
-
690
- const dots = [];
691
- const spacing = 45;
692
- for (let x = 0; x < width + spacing; x += spacing) {
693
- for (let y = 0; y < height + spacing; y += spacing) {
694
- dots.push({
695
- baseX: x,
696
- baseY: y,
697
- x: x,
698
- y: y,
699
- angle: Math.random() * Math.PI * 2,
700
- speed: 0.02 + Math.random() * 0.02
701
- });
702
- }
703
- }
704
-
705
- let mouseX = width / 2;
706
- let mouseY = height / 2;
707
-
708
- window.addEventListener('mousemove', (e) => {
709
- mouseX = e.clientX;
710
- mouseY = e.clientY;
711
- });
712
-
713
- function animateCanvas() {
714
- ctx.clearRect(0, 0, width, height);
715
-
716
- dots.forEach(dot => {
717
- dot.angle += dot.speed;
718
- const dist = Math.hypot(mouseX - dot.baseX, mouseY - dot.baseY);
719
- const maxDist = 200;
720
- let offset = Math.sin(dot.angle) * 4;
721
-
722
- if (dist < maxDist) {
723
- offset += (1 - dist / maxDist) * 15;
724
- }
725
-
726
- ctx.fillStyle = dist < 150 ? 'rgba(0, 229, 255, 0.4)' : 'rgba(157, 78, 221, 0.15)';
727
- ctx.beginPath();
728
- ctx.arc(dot.baseX, dot.baseY + offset, dist < 150 ? 2 : 1.2, 0, Math.PI * 2);
729
- ctx.fill();
730
- });
731
-
732
- requestAnimationFrame(animateCanvas);
733
- }
734
- animateCanvas();
735
-
736
- // 2. State & API Fetching Logic
737
- let allJobs = [];
738
- let currentDomain = 'All';
739
- let currentCity = 'All';
740
-
741
- async function fetchJobs() {
742
- try {
743
- const res = await fetch('/api/jobs');
744
- const data = await res.json();
745
- allJobs = data.jobs || [];
746
- renderJobs();
747
- populateJobSelect();
748
- initCharts();
749
- } catch (err) {
750
- console.error("Failed to load jobs:", err);
751
- }
752
- }
753
-
754
- function renderJobs() {
755
- const grid = document.getElementById('jobGrid');
756
- grid.innerHTML = '';
757
-
758
- const filtered = allJobs.filter(j => {
759
- const domainMatch = currentDomain === 'All' || j.tech_domain === currentDomain;
760
- const cityMatch = currentCity === 'All' || j.city.toLowerCase() === currentCity.toLowerCase();
761
- return domainMatch && cityMatch;
762
- });
763
-
764
- if (filtered.length === 0) {
765
- grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 40px;">No matching tech roles found for selected filters.</div>`;
766
- return;
767
- }
768
-
769
- filtered.forEach(job => {
770
- const card = document.createElement('div');
771
- card.className = 'job-card-3d';
772
- card.innerHTML = `
773
- <div class="job-card-header">
774
- <h3 class="job-title">${job.title}</h3>
775
- <span class="badge-location">📍 ${job.city}</span>
776
- </div>
777
- <div class="job-company">🏢 ${job.company} &bull; <span style="color: var(--accent-cyan);">${job.tech_domain}</span></div>
778
- <div class="job-salary">💰 ₹${job.salary_min_lpa}L - ₹${job.salary_max_lpa}L PA &nbsp;|&nbsp; ⏳ ${job.experience_min_years}-${job.experience_max_years} Yrs</div>
779
- <p class="job-desc">${job.requirements}</p>
780
- <div class="stack-tags">
781
- ${job.tech_stack.map(s => `<span class="stack-tag">${s}</span>`).join('')}
782
- </div>
783
- <div class="job-card-footer">
784
- <span>Job ID: <code>${job.id}</code></span>
785
- <span style="color: #00ff9d;">🌐 ${job.work_mode}</span>
786
- </div>
787
- `;
788
-
789
- // 3D Card Hover Perspective Effect
790
- card.addEventListener('mousemove', (e) => {
791
- const rect = card.getBoundingClientRect();
792
- const x = e.clientX - rect.left - rect.width / 2;
793
- const y = e.clientY - rect.top - rect.height / 2;
794
- card.style.transform = `perspective(1000px) rotateX(${-y / 15}deg) rotateY(${x / 15}deg) translateY(-5px)`;
795
- });
796
-
797
- card.addEventListener('mouseleave', () => {
798
- card.style.transform = `perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(0)`;
799
- });
800
-
801
- grid.appendChild(card);
802
- });
803
- }
804
-
805
- function filterDomain(domain, el) {
806
- currentDomain = domain;
807
- document.querySelectorAll('.filter-group:nth-child(1) .pill-btn').forEach(b => b.classList.remove('active'));
808
- el.classList.add('active');
809
- renderJobs();
810
- }
811
-
812
- function filterCity(city, el) {
813
- currentCity = city;
814
- document.querySelectorAll('.filter-group:nth-child(2) .pill-btn').forEach(b => b.classList.remove('active'));
815
- el.classList.add('active');
816
- renderJobs();
817
- }
818
-
819
- function handleSearch() {
820
- const query = document.getElementById('searchInput').value.toLowerCase();
821
- const cards = document.querySelectorAll('.job-card-3d');
822
- cards.forEach(card => {
823
- const text = card.innerText.toLowerCase();
824
- card.style.display = text.includes(query) ? 'block' : 'none';
825
- });
826
- }
827
-
828
- function switchTab(tabId, el) {
829
- document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
830
- document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
831
-
832
- document.getElementById(`tab-${tabId}`).classList.add('active');
833
- if (el) el.classList.add('active');
834
- }
835
-
836
- function populateJobSelect() {
837
- const sel = document.getElementById('jobSelect');
838
- sel.innerHTML = allJobs.map(j => `<option value="${j.id}">${j.id} — ${j.title} at ${j.company} (${j.city})</option>`).join('');
839
- }
840
-
841
- async function runAtsEvaluation() {
842
- const resumeText = document.getElementById('resumeInput').value;
843
- const targetJobId = document.getElementById('jobSelect').value;
844
-
845
- try {
846
- const res = await fetch('/api/mcp/execute', {
847
- method: 'POST',
848
- headers: { 'Content-Type': 'application/json' },
849
- body: JSON.stringify({ tool: 'analyze_skill_gap', args: { resume_text: resumeText, target_job_id: targetJobId } })
850
- });
851
- const data = await res.json();
852
- const report = JSON.parse(data.result);
853
-
854
- document.getElementById('matchScore').innerText = `${report.match_percentage}%`;
855
- document.getElementById('matchedSkills').innerText = report.matched_skills.join(', ') || 'None';
856
- document.getElementById('missingSkills').innerText = report.missing_skills.join(', ') || 'None';
857
-
858
- // Fetch Patch
859
- const patchRes = await fetch('/api/mcp/execute', {
860
- method: 'POST',
861
- headers: { 'Content-Type': 'application/json' },
862
- body: JSON.stringify({ tool: 'generate_tailored_resume_patch', args: { resume_text: resumeText, target_job_id: targetJobId } })
863
- });
864
- const patchData = await patchRes.json();
865
- const patch = JSON.parse(patchData.result);
866
-
867
- document.getElementById('tailoredBullets').innerHTML = patch.tailored_bullets.map(b => `
868
- <div class="diff-card">
869
- <div class="diff-original">Original: ${b.original}</div>
870
- <div class="diff-tailored">Tailored: ${b.tailored}</div>
871
- </div>
872
- `).join('');
873
- } catch (err) {
874
- console.error("Evaluation failed:", err);
875
- }
876
- }
877
-
878
- function updateMcpInputs() {
879
- const tool = document.getElementById('mcpToolSelect').value;
880
- const fields = document.getElementById('mcpInputFields');
881
- if (tool === 'search_tech_jobs') {
882
- fields.innerHTML = `
883
- <label class="form-label">domain:</label><input id="arg_domain" class="search-input" style="width:100%; margin-bottom:10px;" value="Backend Engineering">
884
- <label class="form-label">city:</label><input id="arg_city" class="search-input" style="width:100%; margin-bottom:10px;" value="Bengaluru">
885
- <label class="form-label">query:</label><input id="arg_query" class="search-input" style="width:100%; margin-bottom:15px;" value="Go">
886
- `;
887
- } else {
888
- fields.innerHTML = `
889
- <label class="form-label">target_job_id:</label><input id="arg_job_id" class="search-input" style="width:100%; margin-bottom:15px;" value="${allJobs[0]?.id || 'BLR-BACKEND-101'}">
890
- `;
891
- }
892
- }
893
-
894
- async function executeMcpTool() {
895
- const tool = document.getElementById('mcpToolSelect').value;
896
- let args = {};
897
- if (tool === 'search_tech_jobs') {
898
- args = {
899
- domain: document.getElementById('arg_domain').value,
900
- city: document.getElementById('arg_city').value,
901
- query: document.getElementById('arg_query').value
902
- };
903
- } else {
904
- args = {
905
- target_job_id: document.getElementById('arg_job_id').value,
906
- resume_text: "Senior backend developer experienced in Go, Docker, AWS."
907
- };
908
- }
909
-
910
- try {
911
- const res = await fetch('/api/mcp/execute', {
912
- method: 'POST',
913
- headers: { 'Content-Type': 'application/json' },
914
- body: JSON.stringify({ tool: tool, args: args })
915
- });
916
- const data = await res.json();
917
- document.getElementById('mcpJsonOutput').innerText = JSON.stringify(JSON.parse(data.result), null, 2);
918
- } catch (err) {
919
- document.getElementById('mcpJsonOutput').innerText = "Error executing tool: " + err;
920
- }
921
- }
922
-
923
- function initCharts() {
924
- const skillsCtx = document.getElementById('skillsChart').getContext('2d');
925
- new Chart(skillsCtx, {
926
- type: 'bar',
927
- data: {
928
- labels: ['Go', 'React', 'Kubernetes', 'Python', 'FastMCP', 'Spark'],
929
- datasets: [{
930
- label: 'Skill Demand %',
931
- data: [85, 78, 72, 90, 65, 55],
932
- backgroundColor: 'rgba(0, 229, 255, 0.6)',
933
- borderColor: '#00e5ff',
934
- borderWidth: 1
935
- }]
936
- },
937
- options: {
938
- responsive: true,
939
- plugins: { legend: { display: false } },
940
- scales: { y: { ticks: { color: '#a0aec0' } }, x: { ticks: { color: '#a0aec0' } } }
941
- }
942
- });
943
-
944
- const hubsCtx = document.getElementById('hubsChart').getContext('2d');
945
- new Chart(hubsCtx, {
946
- type: 'doughnut',
947
- data: {
948
- labels: ['Bengaluru', 'Pune', 'Hyderabad', 'Gurgaon', 'Remote'],
949
- datasets: [{
950
- data: [40, 25, 20, 10, 5],
951
- backgroundColor: ['#00e5ff', '#9d4edd', '#e94057', '#00ff9d', '#ffcc00']
952
- }]
953
- },
954
- options: {
955
- responsive: true,
956
- plugins: { legend: { labels: { color: '#a0aec0' } } }
957
- }
958
- });
959
- }
960
-
961
- // Initialize
962
- fetchJobs();
963
- updateMcpInputs();
964
- </script>
965
- </body>
966
- </html>