edupick-api / comparison_tool.py
Ali Abozahra
Deploy: Groq migration, comparison fix, roadmap quizzes, 844-college dataset
3df4c2e
Raw
History Blame Contribute Delete
9.51 kB
"""
Career / Track Comparison Tool — Phase 4 core logic.
Exports consumed by app.py:
llm — LLM instance for the agent
LANG_CONFIG — Language-specific prompts, criteria labels
search_web — SERPER web-search tool instance
create_comparator_agent — Factory → CrewAI Agent
"""
import os
import json
import requests
from dotenv import load_dotenv
load_dotenv()
# Comparison now runs entirely on Groq: we do the web search ourselves (Serper)
# and feed the results to a plain Groq JSON call. This avoids both the
# OpenRouter dependency (out of credits) and CrewAI's Groq tool-calling issue.
_GROQ_MODEL = "llama-3.3-70b-versatile"
# ---------------------------------------------------------------------------
# Language configuration
# ---------------------------------------------------------------------------
LANG_CONFIG = {
"en": {
"criteria": {
"1": "Required Skills",
"2": "Average Salary",
"3": "Learning Duration",
"4": "Market Demand",
"5": "Difficulty Level",
"6": "Job Opportunities",
"7": "Alternative Paths",
},
"task_prompt": (
"Search the web for the most recent available information about these tracks/careers: {tracks}\n\n"
"Context: Location={location}, Currency={currency}. Reference year: {year} "
"(use the latest data you can find — recent figures are perfectly fine as estimates for this year).\n"
"Compare them on: {criteria}\n\n"
"For EACH criterion and EACH track provide:\n"
" - The actual data/value (give your best estimate from recent data — NEVER refuse or leave blank)\n"
" - A source URL where this info was found\n\n"
"IMPORTANT: You must always produce the comparison. If exact current-year data is unavailable, "
"use the most recent figures and note them as estimates. Do not apologize or decline.\n\n"
"Return ONLY valid JSON — no markdown, no preamble.\n"
"CRITICAL STRUCTURE RULE: for every criterion, the \"cells\" array MUST contain exactly one "
"object per track, in the SAME ORDER as the \"tracks\" array (cells[0] is for tracks[0], "
"cells[1] is for tracks[1], and so on). Never merge two tracks into one cell and never leave a cell empty.\n\n"
"{{\n"
' "tracks": ["First Track Name", "Second Track Name"],\n'
' "rows": [\n'
' {{\n'
' "criterion": "Required Skills",\n'
' "cells": [\n'
' {{"value": "value for the FIRST track", "source": "https://..."}},\n'
' {{"value": "value for the SECOND track", "source": "https://..."}}\n'
" ]\n"
" }}\n"
" ],\n"
' "insights": {{\n'
' "highest_salary": "track name",\n'
' "fastest_growing": "track name",\n'
' "easiest_to_start": "track name",\n'
' "summary": "2-3 sentence overall summary"\n'
" }}\n"
"}}"
),
"expected_output": (
"Valid JSON comparing the requested tracks, with a source URL "
"for each data point and an insights summary."
),
},
"ar": {
"criteria": {
"1": "المهارات المطلوبة",
"2": "متوسط الراتب",
"3": "مدة التعلم",
"4": "الطلب في السوق",
"5": "مستوى الصعوبة",
"6": "فرص العمل",
"7": "المسارات البديلة",
},
"task_prompt": (
"ابحث على الإنترنت عن أحدث المعلومات المتاحة عن هذه المسارات/الوظائف: {tracks}\n\n"
"السياق: الموقع={location}، العملة={currency}. السنة المرجعية: {year} "
"(استخدم أحدث بيانات تجدها — الأرقام الحديثة مقبولة تماماً كتقديرات لهذه السنة).\n"
"قارنها على أساس: {criteria}\n\n"
"لكل معيار ولكل مسار قدّم:\n"
" - القيمة الفعلية (أعطِ أفضل تقدير من البيانات الحديثة — لا ترفض أبداً ولا تترك فراغاً)\n"
" - رابط المصدر الذي وجدت فيه المعلومة\n\n"
"مهم: يجب دائماً إنتاج المقارنة. إذا لم تتوفر بيانات دقيقة للسنة الحالية، "
"استخدم أحدث الأرقام المتاحة واعتبرها تقديرات. لا تعتذر ولا ترفض.\n\n"
"أرجع فقط JSON صالح — بدون markdown أو مقدمة.\n"
"قاعدة بنية حاسمة: لكل معيار، يجب أن تحتوي مصفوفة \"cells\" على عنصر واحد بالضبط لكل مسار، "
"وبنفس ترتيب مصفوفة \"tracks\" (cells[0] للمسار الأول، cells[1] للمسار الثاني، وهكذا). "
"لا تدمج مسارين في خلية واحدة ولا تترك أي خلية فارغة.\n\n"
"{{\n"
' "tracks": ["اسم المسار الأول", "اسم المسار الثاني"],\n'
' "rows": [\n'
' {{\n'
' "criterion": "المهارات المطلوبة",\n'
' "cells": [\n'
' {{"value": "القيمة الخاصة بالمسار الأول", "source": "https://..."}},\n'
' {{"value": "القيمة الخاصة بالمسار الثاني", "source": "https://..."}}\n'
" ]\n"
" }}\n"
" ],\n"
' "insights": {{\n'
' "highest_salary": "اسم المسار",\n'
' "fastest_growing": "اسم المسار",\n'
' "easiest_to_start": "اسم المسار",\n'
' "summary": "ملخص 2-3 جمل"\n'
" }}\n"
"}}"
),
"expected_output": (
"JSON صالح يقارن المسارات المطلوبة مع رابط مصدر لكل نقطة بيانات وملخص insights."
),
},
}
# ---------------------------------------------------------------------------
# Web search (Serper) + Groq comparison
# ---------------------------------------------------------------------------
def _serper(query: str, num: int = 4) -> list:
key = os.environ.get("SERPER_API_KEY", "")
if not key:
return []
try:
resp = requests.post(
"https://google.serper.dev/search",
headers={"X-API-KEY": key, "Content-Type": "application/json"},
json={"q": query, "num": num},
timeout=10,
)
resp.raise_for_status()
return resp.json().get("organic", [])[:num]
except Exception as exc:
print(f"[Comparison] Serper error: {exc}")
return []
def _gather_context(tracks: list, location: str) -> str:
"""Collect real web snippets + source links for each track."""
blocks = []
for tr in tracks:
snips = []
for q in (
f"{tr} required skills and qualifications {location}",
f"{tr} average salary {location}",
f"{tr} job market demand and growth {location}",
):
for item in _serper(q, 3):
title = item.get("title", "")
snippet = item.get("snippet", "")
link = item.get("link", "")
if snippet:
snips.append(f"- {title}: {snippet} (source: {link})")
blocks.append(f"### {tr}\n" + ("\n".join(snips[:8]) or "- (no results found; use your best estimate)"))
return "\n\n".join(blocks)
def run_comparison(tracks_str: str, location: str, currency: str,
year: str, criteria_str: str, lang: str = "en") -> str:
"""Produce the structured comparison JSON string (runs entirely on Groq)."""
from groq import Groq
cfg = LANG_CONFIG.get(lang, LANG_CONFIG["en"])
tracks = [t.strip() for t in tracks_str.split(",") if t.strip()]
context = _gather_context(tracks, location)
prompt = cfg["task_prompt"].format(
tracks=tracks_str, location=location, currency=currency,
year=year, criteria=criteria_str,
)
prompt += (
"\n\n--- WEB SEARCH RESULTS (base your data and sources on these) ---\n"
f"{context}\n"
"Use these real results for the values and the source URLs. "
"If something is missing, give your best estimate and still fill every cell."
)
client = Groq(api_key=os.environ.get("GROQ_API_KEY", ""))
resp = client.chat.completions.create(
model=_GROQ_MODEL,
messages=[
{"role": "system", "content": (
"You are an expert career research analyst. You always return valid JSON only, "
"following the exact structure requested, and you never leave a cell empty."
)},
{"role": "user", "content": prompt},
],
temperature=0.3,
max_tokens=4000,
response_format={"type": "json_object"},
)
return resp.choices[0].message.content