|
|
import json |
|
|
import os |
|
|
import re |
|
|
import time |
|
|
import uuid |
|
|
import sys |
|
|
import logging |
|
|
import asyncio |
|
|
import traceback |
|
|
from datetime import datetime, timezone, timedelta |
|
|
from dataclasses import dataclass |
|
|
from typing import List, Optional, Tuple |
|
|
|
|
|
import gradio as gr |
|
|
import requests |
|
|
from mcp import ClientSession |
|
|
from mcp.client.sse import sse_client |
|
|
from openai import OpenAI |
|
|
from dotenv import load_dotenv |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
MCP_SSE_URL = "https://api.topcoder-dev.com/v6/mcp/sse" |
|
|
MCP_MESSAGES_URL = "https://api.topcoder-dev.com/v6/mcp/messages" |
|
|
MCP_HTTP_BASE = "https://api.topcoder-dev.com/v6/mcp/mcp" |
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig( |
|
|
level=logging.INFO, |
|
|
format="%(asctime)s [%(levelname)s] %(message)s", |
|
|
stream=sys.stdout, |
|
|
) |
|
|
logger = logging.getLogger("challenge_scout") |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class Challenge: |
|
|
id: str |
|
|
title: str |
|
|
prize: float |
|
|
deadline: str |
|
|
tags: List[str] |
|
|
description: str = "" |
|
|
|
|
|
|
|
|
FALLBACK_DATA: List[Challenge] = [ |
|
|
Challenge(id="123", title="Build a Minimal MCP Agent UI", prize=300.0, deadline="2025-08-20", tags=["mcp", "python", "gradio"]), |
|
|
Challenge(id="456", title="Topcoder Data Parsing Toolkit", prize=500.0, deadline="2025-08-15", tags=["data", "api", "json"]), |
|
|
Challenge(id="789", title="AI-Powered Web Application", prize=250.0, deadline="2025-08-18", tags=["ai", "web", "optimization"]), |
|
|
] |
|
|
|
|
|
|
|
|
def _open_mcp_session( |
|
|
connect_timeout: float = 5.0, |
|
|
read_window_seconds: float = 8.0, |
|
|
debug: bool = False, |
|
|
max_retries: int = 2, |
|
|
) -> Tuple[Optional[str], str]: |
|
|
"""Open SSE and extract a sessionId from the first data event. |
|
|
|
|
|
We keep the stream open for up to `read_window_seconds` to wait for the |
|
|
line like: "data: /v6/mcp/messages?sessionId=<uuid>". |
|
|
""" |
|
|
logs: List[str] = [] |
|
|
headers = { |
|
|
"Accept": "text/event-stream", |
|
|
"Cache-Control": "no-cache", |
|
|
"Connection": "keep-alive", |
|
|
} |
|
|
backoff = 0.6 |
|
|
for attempt in range(1, max_retries + 2): |
|
|
if debug: |
|
|
logger.info(f"SSE dialing attempt={attempt} url={MCP_SSE_URL}") |
|
|
try: |
|
|
t0 = time.time() |
|
|
resp = requests.get( |
|
|
MCP_SSE_URL, |
|
|
headers=headers, |
|
|
timeout=(connect_timeout, read_window_seconds + 2.0), |
|
|
stream=True, |
|
|
) |
|
|
elapsed = time.time() - t0 |
|
|
logs.append(f"SSE attempt {attempt}: status={resp.status_code}, elapsed={elapsed:.2f}s") |
|
|
if debug: |
|
|
logger.info(f"SSE attempt {attempt} status={resp.status_code} elapsed={elapsed:.2f}s") |
|
|
resp.raise_for_status() |
|
|
|
|
|
start = time.time() |
|
|
pattern = re.compile(r"^data:\s*/v6/mcp/messages\?sessionId=([a-f0-9-]+)\s*$", re.IGNORECASE) |
|
|
for line in resp.iter_lines(decode_unicode=True, chunk_size=1): |
|
|
if line is None: |
|
|
if time.time() - start > read_window_seconds: |
|
|
logs.append("SSE: timeout waiting for data line") |
|
|
break |
|
|
continue |
|
|
if not isinstance(line, str): |
|
|
if time.time() - start > read_window_seconds: |
|
|
logs.append("SSE: timeout (non-str line)") |
|
|
break |
|
|
continue |
|
|
if debug: |
|
|
logs.append(f"SSE line: {line[:160]}") |
|
|
logger.info(f"SSE line: {line[:160]}") |
|
|
m = pattern.match(line) |
|
|
if m: |
|
|
sid = m.group(1) |
|
|
logs.append(f"SSE: obtained sessionId={sid}") |
|
|
if debug: |
|
|
logger.info(f"SSE obtained sessionId={sid}") |
|
|
return sid, "\n".join(logs) |
|
|
if time.time() - start > read_window_seconds: |
|
|
logs.append("SSE: read window exceeded without sessionId") |
|
|
if debug: |
|
|
logger.info("SSE: read window exceeded without sessionId") |
|
|
break |
|
|
except Exception as e: |
|
|
logs.append(f"SSE error on attempt {attempt}: {e}") |
|
|
if debug: |
|
|
logger.info(f"SSE error on attempt {attempt}: {e}") |
|
|
time.sleep(backoff) |
|
|
backoff *= 1.6 |
|
|
return None, "\n".join(logs) |
|
|
|
|
|
|
|
|
def _mcp_list_challenges( |
|
|
debug: bool = False, |
|
|
retries: int = 2, |
|
|
) -> Tuple[List[Challenge], Optional[str], str]: |
|
|
"""Call MCP using the official SDK (JSON-RPC over streamable HTTP) and return compact challenges. |
|
|
|
|
|
Returns (challenges, error, debug_logs) |
|
|
""" |
|
|
logs: List[str] = [] |
|
|
async def _do() -> Tuple[List[Challenge], Optional[str], str]: |
|
|
try: |
|
|
t0 = time.time() |
|
|
async with sse_client(MCP_SSE_URL) as (read, write): |
|
|
async with ClientSession(read, write) as session: |
|
|
await session.initialize() |
|
|
tools = await session.list_tools() |
|
|
names = [t.name for t in tools.tools] |
|
|
logs.append(f"SDK tools: {names}") |
|
|
|
|
|
tool_name = None |
|
|
for cand in [ |
|
|
"query-tc-challenges", |
|
|
"query-tc-challenges-public", |
|
|
"query-tc-challenges-private", |
|
|
]: |
|
|
if cand in names: |
|
|
tool_name = cand |
|
|
break |
|
|
if not tool_name and names: |
|
|
tool_name = names[0] |
|
|
if not tool_name: |
|
|
return [], "No tools available", "\n".join(logs) |
|
|
|
|
|
args = {"page": 1, "pageSize": 20} |
|
|
logs.append(f"Calling tool {tool_name} with args: {args}") |
|
|
try: |
|
|
result = await session.call_tool(tool_name, args) |
|
|
except Exception as e: |
|
|
logs.append(f"call_tool error: {e}") |
|
|
return [], str(e), "\n".join(logs) |
|
|
|
|
|
items: List[Challenge] = [] |
|
|
payload = getattr(result, "structuredContent", None) |
|
|
if not payload: |
|
|
|
|
|
combined_text = "\n".join( |
|
|
getattr(c, "text", "") for c in getattr(result, "content", []) if hasattr(c, "text") |
|
|
) |
|
|
obj_match = re.search(r"\{[\s\S]*\}$", combined_text) |
|
|
if obj_match: |
|
|
try: |
|
|
payload = json.loads(obj_match.group(0)) |
|
|
except Exception: |
|
|
payload = None |
|
|
|
|
|
raw_list = [] |
|
|
if isinstance(payload, dict) and isinstance(payload.get("data"), list): |
|
|
raw_list = payload.get("data") |
|
|
logs.append(f"Received {len(raw_list)} items from data[]") |
|
|
elif isinstance(payload, list): |
|
|
raw_list = payload |
|
|
logs.append(f"Received {len(raw_list)} items from top-level list") |
|
|
else: |
|
|
logs.append(f"Unexpected payload shape: {type(payload)}") |
|
|
|
|
|
def to_ch(item: dict) -> Challenge: |
|
|
title = ( |
|
|
str(item.get("name") or item.get("title") or item.get("challengeTitle") or "") |
|
|
) |
|
|
|
|
|
prize = 0.0 |
|
|
prize_sets = item.get("prizeSets") |
|
|
if isinstance(prize_sets, list): |
|
|
for s in prize_sets: |
|
|
prizes = s.get("prizes") if isinstance(s, dict) else None |
|
|
if isinstance(prizes, list): |
|
|
for p in prizes: |
|
|
val = None |
|
|
if isinstance(p, dict): |
|
|
val = p.get("value") or p.get("amount") |
|
|
try: |
|
|
prize += float(val or 0) |
|
|
except Exception: |
|
|
pass |
|
|
else: |
|
|
prize_val = item.get("totalPrizes") or item.get("prize") or item.get("totalPrize") or 0 |
|
|
try: |
|
|
prize = float(prize_val or 0) |
|
|
except Exception: |
|
|
prize = 0.0 |
|
|
|
|
|
deadline = ( |
|
|
str( |
|
|
item.get("endDate") |
|
|
or item.get("submissionEndDate") |
|
|
or item.get("registrationEndDate") |
|
|
or item.get("startDate") |
|
|
or "" |
|
|
) |
|
|
) |
|
|
|
|
|
tg = item.get("tags") |
|
|
if not isinstance(tg, list): |
|
|
tg = [] |
|
|
|
|
|
for extra in [item.get("track"), item.get("type"), item.get("status")]: |
|
|
if extra: |
|
|
tg.append(extra) |
|
|
|
|
|
sk = item.get("skills") |
|
|
if isinstance(sk, list): |
|
|
for s in sk: |
|
|
if isinstance(s, dict): |
|
|
name = s.get("name") or s.get("id") |
|
|
if name: |
|
|
tg.append(str(name)) |
|
|
else: |
|
|
tg.append(str(s)) |
|
|
tg = [str(x) for x in tg if x] |
|
|
desc = str(item.get("description") or "") |
|
|
return Challenge( |
|
|
id=str(item.get("id", "")), |
|
|
title=title, |
|
|
prize=prize, |
|
|
deadline=deadline, |
|
|
tags=tg, |
|
|
description=desc, |
|
|
) |
|
|
|
|
|
for it in raw_list: |
|
|
if isinstance(it, dict): |
|
|
items.append(to_ch(it)) |
|
|
logs.append(f"Normalized {len(items)} challenges") |
|
|
return items, None, "\n".join(logs) |
|
|
except Exception as e: |
|
|
logs.append(f"SDK fatal: {e}") |
|
|
logs.append(traceback.format_exc()) |
|
|
|
|
|
try: |
|
|
from exceptiongroup import ExceptionGroup |
|
|
except Exception: |
|
|
ExceptionGroup = None |
|
|
if ExceptionGroup and isinstance(e, ExceptionGroup): |
|
|
for idx, sub in enumerate(e.exceptions): |
|
|
logs.append(f" sub[{idx}]: {type(sub).__name__}: {sub}") |
|
|
return [], str(e), "\n".join(logs) |
|
|
|
|
|
|
|
|
return asyncio.run(_do()) |
|
|
|
|
|
|
|
|
def shortlist(challenge_list: List[Challenge], keyword: str, min_prize: float) -> List[Challenge]: |
|
|
keyword_lower = keyword.lower().strip() |
|
|
results = [] |
|
|
for ch in challenge_list: |
|
|
if ch.prize < min_prize: |
|
|
continue |
|
|
hay = f"{ch.title} {' '.join(ch.tags)}".lower() |
|
|
if keyword_lower and keyword_lower not in hay: |
|
|
continue |
|
|
results.append(ch) |
|
|
|
|
|
results.sort(key=lambda c: c.prize, reverse=True) |
|
|
return results |
|
|
|
|
|
|
|
|
def _parse_deadline(dt_str: str) -> Optional[datetime]: |
|
|
if not dt_str: |
|
|
return None |
|
|
try: |
|
|
|
|
|
return datetime.fromisoformat(dt_str.replace("Z", "+00:00")) |
|
|
except Exception: |
|
|
try: |
|
|
return datetime.strptime(dt_str, "%Y-%m-%d") |
|
|
except Exception: |
|
|
return None |
|
|
|
|
|
|
|
|
def _filter_by_days(items: List[Challenge], days_ahead: int) -> List[Challenge]: |
|
|
if days_ahead <= 0: |
|
|
return items |
|
|
now = datetime.now(timezone.utc) |
|
|
limit = now + timedelta(days=days_ahead) |
|
|
kept: List[Challenge] = [] |
|
|
for ch in items: |
|
|
dt = _parse_deadline(ch.deadline) |
|
|
if dt is None: |
|
|
kept.append(ch) |
|
|
continue |
|
|
if now <= dt <= limit: |
|
|
kept.append(ch) |
|
|
return kept |
|
|
|
|
|
|
|
|
def _generate_plan(items: List[Challenge], keyword: str, min_prize: float, days_ahead: int, debug: bool = False) -> Tuple[str, str]: |
|
|
|
|
|
top = items[:8] |
|
|
compact = [ |
|
|
{ |
|
|
"title": c.title, |
|
|
"prize": c.prize, |
|
|
"deadline": c.deadline, |
|
|
"tags": c.tags[:5], |
|
|
} |
|
|
for c in top |
|
|
] |
|
|
|
|
|
logs: List[str] = [] |
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") |
|
|
|
|
|
if not api_key: |
|
|
logs.append("Missing OPENAI_API_KEY") |
|
|
return "", "\n".join(logs) |
|
|
|
|
|
try: |
|
|
client = OpenAI( |
|
|
api_key=api_key, |
|
|
base_url=base_url |
|
|
) |
|
|
|
|
|
prompt = ( |
|
|
"You are a concise challenge scout. Given compact challenge metadata, output:\n" |
|
|
"- Top 3 picks (title + brief reason)\n" |
|
|
"- Quick plan of action (3 bullets)\n" |
|
|
f"Constraints: keyword='{keyword}', min_prize>={min_prize}, within {days_ahead} days.\n" |
|
|
f"Data: {json.dumps(compact)}" |
|
|
) |
|
|
|
|
|
if debug: |
|
|
logs.append(f"PLAN OpenAI prompt: {prompt[:1500]}") |
|
|
|
|
|
response = client.chat.completions.create( |
|
|
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), |
|
|
messages=[ |
|
|
{"role": "system", "content": "You are a helpful, terse assistant."}, |
|
|
{"role": "user", "content": prompt}, |
|
|
], |
|
|
temperature=0.3, |
|
|
timeout=20 |
|
|
) |
|
|
|
|
|
text = response.choices[0].message.content.strip() |
|
|
|
|
|
if debug: |
|
|
logs.append(f"PLAN OpenAI output: {text[:800]}") |
|
|
logger.info("\n".join(logs)) |
|
|
|
|
|
return text, "\n".join(logs) |
|
|
|
|
|
except Exception as e: |
|
|
if debug: |
|
|
logs.append(f"PLAN OpenAI error: {e}") |
|
|
logger.info("\n".join(logs)) |
|
|
return "", "\n".join(logs) |
|
|
|
|
|
|
|
|
def _generate_plan_fixed( |
|
|
ranked: List[tuple[Challenge, float, str]], |
|
|
keyword: str, |
|
|
days_ahead: int, |
|
|
debug: bool = False, |
|
|
) -> Tuple[str, str]: |
|
|
"""Generate a plan using the already-ranked top picks without changing order. |
|
|
|
|
|
The LLM is only used to phrase reasons and the action plan. It must not re-rank. |
|
|
""" |
|
|
top = ranked[:3] |
|
|
data = [ |
|
|
{ |
|
|
"title": c.title, |
|
|
"prize": c.prize, |
|
|
"deadline": c.deadline, |
|
|
"tags": c.tags[:6], |
|
|
"score": round(s, 4), |
|
|
"reason": (r or "").strip()[:300], |
|
|
} |
|
|
for c, s, r in top |
|
|
] |
|
|
|
|
|
logs: List[str] = [] |
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") |
|
|
if not api_key: |
|
|
logs.append("Missing OPENAI_API_KEY") |
|
|
return "", "\n".join(logs) |
|
|
|
|
|
try: |
|
|
client = OpenAI(api_key=api_key, base_url=base_url) |
|
|
prompt = ( |
|
|
"You are a concise challenge scout. You are given pre-ranked top picks (in order).\n" |
|
|
"Do NOT change the order or add/remove items.\n" |
|
|
"Output exactly:\n" |
|
|
"- Top 3 picks (title + short reason).\n" |
|
|
"- Quick plan of action (3 bullets).\n" |
|
|
f"Constraints: query='{keyword}', within {days_ahead} days.\n" |
|
|
f"Ranked data: {json.dumps(data)}" |
|
|
) |
|
|
if debug: |
|
|
logs.append(f"PLAN(FIXED) prompt: {prompt[:1200]}") |
|
|
resp = client.chat.completions.create( |
|
|
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), |
|
|
messages=[ |
|
|
{"role": "system", "content": "Be terse. Do not re-rank or add items."}, |
|
|
{"role": "user", "content": prompt}, |
|
|
], |
|
|
temperature=0.3, |
|
|
timeout=20, |
|
|
) |
|
|
text = (resp.choices[0].message.content or "").strip() |
|
|
if debug: |
|
|
logs.append(f"PLAN(FIXED) output: {text[:800]}") |
|
|
return text, "\n".join(logs) |
|
|
except Exception as e: |
|
|
if debug: |
|
|
logs.append(f"PLAN(FIXED) error: {e}") |
|
|
return "", "\n".join(logs) |
|
|
|
|
|
def _score_items(items: List[Challenge], keyword: str, debug: bool = False) -> Tuple[List[tuple[Challenge, float, str]], str]: |
|
|
"""Score challenges using OpenAI API and return (challenge, score, reason) tuples""" |
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") |
|
|
|
|
|
results: List[tuple[Challenge, float, str]] = [] |
|
|
slog: List[str] = [] |
|
|
|
|
|
if not items: |
|
|
return results, "\n".join(slog) |
|
|
|
|
|
if not api_key: |
|
|
if debug: |
|
|
slog.append("Missing OPENAI_API_KEY") |
|
|
return results, "\n".join(slog) |
|
|
|
|
|
compact = [ |
|
|
{ |
|
|
"id": c.id, |
|
|
"title": c.title, |
|
|
"prize": c.prize, |
|
|
"deadline": c.deadline, |
|
|
"tags": c.tags[:6], |
|
|
"description": (c.description or "").replace("\n", " ")[:400], |
|
|
} |
|
|
for c in items[:8] |
|
|
] |
|
|
|
|
|
scoring_prompt = ( |
|
|
"You are an expert Topcoder challenge analyst. Analyze items and rate match to the query.\n" |
|
|
f"Query: {keyword}\n" |
|
|
"Items: " + json.dumps(compact) + "\n\n" |
|
|
"Instructions:\n" |
|
|
"- Consider skills, tags, and brief description.\n" |
|
|
"- Relevance matters most, but higher prize should be explicitly favored.\n" |
|
|
"- When two items are similarly relevant, prioritize the one with the larger prize.\n" |
|
|
"- Incorporate prize into the score (0-1), not just the reason.\n" |
|
|
"- Return ONLY JSON array of objects: [{id, score, reason}] where 0<=score<=1.\n" |
|
|
"- Do not include any extra text." |
|
|
) |
|
|
|
|
|
try: |
|
|
client = OpenAI( |
|
|
api_key=api_key, |
|
|
base_url=base_url |
|
|
) |
|
|
|
|
|
if debug: |
|
|
slog.append(f"OpenAI model: {os.getenv('OPENAI_MODEL', 'gpt-4o-mini')}") |
|
|
slog.append(f"OpenAI prompt: {scoring_prompt[:1500]}") |
|
|
|
|
|
response = client.chat.completions.create( |
|
|
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), |
|
|
messages=[ |
|
|
{"role": "system", "content": "You are a helpful, terse assistant. Return JSON only."}, |
|
|
{"role": "user", "content": scoring_prompt}, |
|
|
], |
|
|
temperature=0.2, |
|
|
timeout=40 |
|
|
) |
|
|
|
|
|
text = response.choices[0].message.content.strip() |
|
|
|
|
|
if debug: |
|
|
slog.append(f"OpenAI raw output: {text[:800]}") |
|
|
|
|
|
|
|
|
m = re.search(r"\[\s*\{[\s\S]*\}\s*\]", text) |
|
|
if not m: |
|
|
if debug: |
|
|
slog.append("No valid JSON array found in response") |
|
|
return results, "\n".join(slog) |
|
|
|
|
|
try: |
|
|
arr = json.loads(m.group(0)) |
|
|
score_map: dict[str, tuple[float, str]] = {} |
|
|
|
|
|
for obj in arr: |
|
|
if isinstance(obj, dict): |
|
|
cid = str(obj.get("id", "")) |
|
|
score = float(obj.get("score", 0)) |
|
|
reason = str(obj.get("reason", "")) |
|
|
score_map[cid] = (max(0.0, min(1.0, score)), reason) |
|
|
|
|
|
|
|
|
out: List[tuple[Challenge, float, str]] = [] |
|
|
for c in items: |
|
|
if c.id in score_map: |
|
|
s, r = score_map[c.id] |
|
|
out.append((c, s, r)) |
|
|
else: |
|
|
|
|
|
out.append((c, min(c.prize / 1000.0, 1.0) * 0.3, "")) |
|
|
|
|
|
if debug: |
|
|
slog.append(f"OpenAI parsed {len(score_map)} scores") |
|
|
logger.info("\n".join(slog)) |
|
|
|
|
|
return out, "\n".join(slog) |
|
|
|
|
|
except json.JSONDecodeError as e: |
|
|
if debug: |
|
|
slog.append(f"JSON decode error: {e}") |
|
|
return results, "\n".join(slog) |
|
|
|
|
|
except Exception as e: |
|
|
if debug: |
|
|
slog.append(f"OpenAI API error: {e}") |
|
|
logger.info("\n".join(slog)) |
|
|
return results, "\n".join(slog) |
|
|
|
|
|
|
|
|
def _require_llm_config() -> Tuple[bool, str]: |
|
|
"""Check if OpenAI API is properly configured""" |
|
|
if os.getenv("OPENAI_API_KEY"): |
|
|
return True, "" |
|
|
return False, "Set OPENAI_API_KEY for LLM scoring" |
|
|
|
|
|
|
|
|
def _extract_keywords(requirements: str, debug: bool = False) -> Tuple[str, str]: |
|
|
"""Use LLM to extract 1-3 broad, concise keywords from free-form requirements.""" |
|
|
logs: List[str] = [] |
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") |
|
|
if not api_key: |
|
|
return "", "Missing OPENAI_API_KEY" |
|
|
try: |
|
|
client = OpenAI(api_key=api_key, base_url=base_url) |
|
|
prompt = ( |
|
|
"You extract compact search keywords.\n" |
|
|
"Given a user's requirements, output a comma-separated list of up to 3 broad keywords (1-3 words each).\n" |
|
|
"Keep them minimal, generic, and deduplicated.\n" |
|
|
"Examples:\n" |
|
|
"- 'Need an LLM project with Python and simple UI' -> LLM, Python, UI\n" |
|
|
"- 'Data visualization challenges about dashboards' -> data visualization, dashboard\n" |
|
|
f"Requirements: {requirements.strip()}\n" |
|
|
"Return only the keywords, comma-separated." |
|
|
) |
|
|
if debug: |
|
|
logs.append(f"KW prompt: {prompt[:500]}") |
|
|
resp = client.chat.completions.create( |
|
|
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), |
|
|
messages=[ |
|
|
{"role": "system", "content": "You are a terse assistant. Return only keywords."}, |
|
|
{"role": "user", "content": prompt}, |
|
|
], |
|
|
temperature=0.2, |
|
|
timeout=20, |
|
|
) |
|
|
text = (resp.choices[0].message.content or "").strip() |
|
|
|
|
|
text = re.sub(r"\s*[,\n]\s*", ", ", text) |
|
|
|
|
|
parts = [p.strip() for p in text.split(",") if p.strip()] |
|
|
keywords = ", ".join(parts[:3]) |
|
|
if debug: |
|
|
logs.append(f"KW extracted: {keywords}") |
|
|
return keywords, "\n".join(logs) |
|
|
except Exception as e: |
|
|
if debug: |
|
|
logs.append(f"KW error: {e}") |
|
|
return "", "\n".join(logs) |
|
|
|
|
|
|
|
|
def _filter_active(items: List[Challenge]) -> List[Challenge]: |
|
|
"""Keep items that appear to be active/online based on tags or status hints.""" |
|
|
kept: List[Challenge] = [] |
|
|
for ch in items: |
|
|
tags_lower = [t.lower() for t in ch.tags] |
|
|
if any(t == "active" or t == "open" for t in tags_lower): |
|
|
kept.append(ch) |
|
|
else: |
|
|
|
|
|
if not any(t in ("completed", "closed", "cancelled", "draft") for t in tags_lower): |
|
|
kept.append(ch) |
|
|
return kept |
|
|
|
|
|
|
|
|
def _stars_for_score(score: float) -> str: |
|
|
n = max(1, min(5, int(round(score * 5)))) |
|
|
return "★" * n + "☆" * (5 - n) |
|
|
|
|
|
|
|
|
def run_query(requirements: str): |
|
|
|
|
|
min_prize = 0.0 |
|
|
days_ahead = 90 |
|
|
|
|
|
|
|
|
keyword, _ = _extract_keywords(requirements, debug=False) |
|
|
|
|
|
|
|
|
items, err, _ = _mcp_list_challenges(debug=False) |
|
|
if err: |
|
|
items = FALLBACK_DATA |
|
|
status = f"MCP fallback: {err}" |
|
|
else: |
|
|
status = "MCP OK" |
|
|
|
|
|
|
|
|
items = _filter_by_days(items, days_ahead) |
|
|
items = _filter_active(items) |
|
|
|
|
|
filtered = shortlist(items, "", min_prize) |
|
|
|
|
|
ok, cfg_msg = _require_llm_config() |
|
|
if not ok: |
|
|
status = f"{status} | LLM config required: {cfg_msg}" |
|
|
else: |
|
|
scored, _ = _score_items(filtered, keyword, debug=False) |
|
|
if scored: |
|
|
|
|
|
|
|
|
prizes = [c.prize for c, _, _ in scored] |
|
|
pmin = min(prizes) if prizes else 0.0 |
|
|
pmax = max(prizes) if prizes else 0.0 |
|
|
denom = (pmax - pmin) if (pmax - pmin) > 0 else 1.0 |
|
|
def prize_norm(v: float) -> float: |
|
|
return max(0.0, min(1.0, (v - pmin) / denom)) |
|
|
|
|
|
alpha = 0.75 |
|
|
ranked = [] |
|
|
for c, s, r in scored: |
|
|
cs = alpha * float(s) + (1 - alpha) * prize_norm(c.prize) |
|
|
ranked.append((c, cs, r)) |
|
|
ranked.sort(key=lambda t: t[1], reverse=True) |
|
|
filtered = [c for c, _, _ in ranked] |
|
|
|
|
|
if not filtered and items: |
|
|
filtered = items |
|
|
status = f"{status} (no matches; showing unfiltered)" |
|
|
plan_text, _ = _generate_plan_fixed(ranked, keyword, days_ahead, debug=False) if ok and 'ranked' in locals() and ranked else ("", "") |
|
|
|
|
|
|
|
|
id_to_score_reason: dict[str, tuple[float, str]] = {} |
|
|
if ok and 'ranked' in locals(): |
|
|
for c, s, r in ranked: |
|
|
id_to_score_reason[c.id] = (s, r) |
|
|
|
|
|
rows = [] |
|
|
for c in filtered: |
|
|
s, r = id_to_score_reason.get(c.id, (0.0, "")) |
|
|
stars = _stars_for_score(s) |
|
|
rows.append([ |
|
|
c.title, |
|
|
f"${c.prize:,.0f}", |
|
|
c.deadline, |
|
|
", ".join(c.tags), |
|
|
stars, |
|
|
(r[:160] + ("…" if len(r) > 160 else "")) if r else "", |
|
|
c.id, |
|
|
]) |
|
|
return rows, status, plan_text |
|
|
|
|
|
|
|
|
with gr.Blocks(title="Topcoder Challenge Scout") as demo: |
|
|
gr.Markdown("**Topcoder Challenge Scout** — agent picks tools, you provide requirements") |
|
|
requirements = gr.Textbox(label="Requirements", placeholder="e.g. Looking for recent active LLM development challenges with web UI", lines=3) |
|
|
gr.Markdown("Default filters: within last 90 days, active status. The agent extracts minimal keywords automatically.") |
|
|
run_btn = gr.Button("Find challenges") |
|
|
status = gr.Textbox(label="Status", interactive=False) |
|
|
table = gr.Dataframe(headers=["Title", "Prize", "Deadline", "Tags", "Recommend", "AI Reason", "Id"], wrap=True) |
|
|
plan_md = gr.Markdown("", label="Plan") |
|
|
|
|
|
run_btn.click( |
|
|
fn=run_query, |
|
|
inputs=[requirements], |
|
|
outputs=[table, status, plan_md], |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860"))) |
|
|
|