Spaces:
Runtime error
Runtime error
| """ | |
| agent/react_loop.py | |
| ReAct agent loop powered by Groq (llama-3.3-70b-versatile). | |
| """ | |
| import json | |
| import time | |
| from groq import Groq | |
| from typing import Generator | |
| from tools.definitions import TOOL_SCHEMAS, dispatch_tool | |
| def _to_groq_tools(schemas: list[dict]) -> list[dict]: | |
| return [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": s["name"], | |
| "description": s["description"], | |
| "parameters": s["input_schema"], | |
| }, | |
| } | |
| for s in schemas | |
| ] | |
| SYSTEM_PROMPT = """You are JobAgent — an expert career intelligence analyst with access to real job market tools. | |
| Your job: analyze a candidate's profile against real job postings and produce a grounded career intelligence report. | |
| TOOLS AVAILABLE: | |
| - search_jobs: semantic search over real HuggingFace job postings | |
| - score_match: TF-IDF cosine similarity scoring of jobs vs candidate profile | |
| - skill_demand: frequency analysis of skills across all postings | |
| - salary_lookup: BLS OES 2024 salary data, location + seniority adjusted | |
| - filter_by_location: filter results to a target city | |
| - summarize_findings: compile all data — call this LAST when ready | |
| RULES: | |
| 1. Always start with search_jobs AND skill_demand. | |
| 2. After search_jobs, always call score_match on the results. | |
| 3. Always call salary_lookup with the candidate's exact role, location, and years_exp. | |
| 4. If search results are weak (fewer than 10 results), retry search_jobs with a different query. | |
| 5. Only call summarize_findings after you have data from: search_jobs, score_match, skill_demand, salary_lookup. | |
| 6. After summarize_findings, write the final report immediately. | |
| REPORT FORMAT (write this after summarize_findings): | |
| ## Market Position | |
| ## Top Job Matches | |
| ## Skill Gap Analysis | |
| ## Salary Intelligence | |
| ## 30-Day Action Plan | |
| Every sentence must contain a specific number or data point. No filler.""" | |
| def run_agent( | |
| profile: dict, | |
| dataset_rows: list[dict], | |
| api_key: str, | |
| ) -> Generator[dict, None, None]: | |
| client = Groq(api_key=api_key) | |
| groq_tools = _to_groq_tools(TOOL_SCHEMAS) | |
| candidate_str = ( | |
| f"Candidate profile:\n" | |
| f" Role target: {profile['role']}\n" | |
| f" Location: {profile['location']}\n" | |
| f" Years experience: {profile['years_exp']}\n" | |
| f" Skills: {', '.join(profile['skills'])}\n\n" | |
| f"Dataset: {len(dataset_rows)} real job postings loaded from HuggingFace.\n\n" | |
| f"Analyze this candidate's market position and produce a career intelligence report. " | |
| f"Use your tools to gather real data — do not guess or make up numbers." | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": candidate_str}, | |
| ] | |
| iteration = 0 | |
| max_iterations = 14 | |
| while iteration < max_iterations: | |
| iteration += 1 | |
| yield {"type": "iteration", "n": iteration} | |
| response = client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=messages, | |
| tools=groq_tools, | |
| tool_choice="auto", | |
| max_tokens=4096, | |
| temperature=0.1, | |
| ) | |
| msg = response.choices[0].message | |
| finish = response.choices[0].finish_reason | |
| if msg.content and msg.content.strip(): | |
| yield {"type": "thought", "text": msg.content.strip()} | |
| if finish == "stop" or not msg.tool_calls: | |
| final_text = msg.content or "Analysis complete." | |
| yield {"type": "final_report", "text": final_text} | |
| break | |
| messages.append({ | |
| "role": "assistant", | |
| "content": msg.content or "", | |
| "tool_calls": [ | |
| { | |
| "id": tc.id, | |
| "type": "function", | |
| "function": { | |
| "name": tc.function.name, | |
| "arguments": tc.function.arguments, | |
| }, | |
| } | |
| for tc in msg.tool_calls | |
| ], | |
| }) | |
| for tc in msg.tool_calls: | |
| tool_name = tc.function.name | |
| try: | |
| tool_inputs = json.loads(tc.function.arguments) | |
| except json.JSONDecodeError: | |
| tool_inputs = {} | |
| yield {"type": "tool_call", "name": tool_name, "inputs": tool_inputs} | |
| start = time.time() | |
| try: | |
| result = dispatch_tool(tool_name, tool_inputs, dataset_rows) | |
| elapsed = round(time.time() - start, 2) | |
| summary = _summarize_result(tool_name, result) | |
| yield { | |
| "type": "tool_result", | |
| "name": tool_name, | |
| "summary": summary, | |
| "elapsed": elapsed, | |
| "full_result": result, | |
| } | |
| except Exception as e: | |
| result = {"error": str(e)} | |
| elapsed = round(time.time() - start, 2) | |
| yield { | |
| "type": "tool_result", | |
| "name": tool_name, | |
| "summary": f"Error: {e}", | |
| "elapsed": elapsed, | |
| "full_result": result, | |
| } | |
| messages.append({ | |
| "role": "tool", | |
| "tool_call_id": tc.id, | |
| "content": json.dumps(result), | |
| }) | |
| if tool_name == "summarize_findings" and result.get("ready_for_report"): | |
| yield {"type": "summary_ready", "data": result} | |
| yield {"type": "done", "iterations": iteration} | |
| def _summarize_result(tool_name: str, result: dict) -> str: | |
| if "error" in result: | |
| return f"Error: {result['error']}" | |
| if tool_name == "search_jobs": | |
| n = result.get("results_returned", 0) | |
| total = result.get("total_searched", 0) | |
| return f"Found {n} relevant jobs from {total} postings · query: '{result.get('query', '')}'" | |
| if tool_name == "score_match": | |
| n = result.get("total_scored", 0) | |
| avg = result.get("avg_match_top10", 0) | |
| top = (result.get("top_jobs") or [{}])[0] | |
| return ( | |
| f"Scored {n} jobs · top: '{top.get('title', '')}' " | |
| f"@ {top.get('company', '')} ({top.get('match_score', 0)}%) · avg top-10: {avg}%" | |
| ) | |
| if tool_name == "skill_demand": | |
| return result.get("gap_summary", "Skill demand analyzed.") | |
| if tool_name == "salary_lookup": | |
| med = result.get("salary_median_k", "N/A") | |
| band = result.get("band", "").title() | |
| anchor = result.get("negotiation_anchor", "") | |
| loc = result.get("location", "") | |
| src = result.get("source", "") | |
| return f"{band} median: ${med}k in {loc} · anchor: {anchor} · {src}" | |
| if tool_name == "filter_by_location": | |
| return f"Filtered to {result.get('filtered_count', 0)} jobs in {result.get('filter_applied', '')}" | |
| if tool_name == "summarize_findings": | |
| avg = result.get("avg_match_score", 0) | |
| pos = result.get("market_position", "") | |
| return f"Findings compiled · market position: {pos} · avg match: {avg}% · ready for report" | |
| return str(result)[:120] | |