diff --git a/.gitignore b/.gitignore index 8a6192a26..697533397 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/.env.example b/SLAB/Deadline_Detective_Backend_Webcmd/.env.example new file mode 100644 index 000000000..2c4f49e40 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/.env.example @@ -0,0 +1,3 @@ +GEMINI_API_KEY=your_gemini_api_key_here +WEBCMD_PROFILE=default +MAX_PAGES=5 diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile b/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile new file mode 100644 index 000000000..2e3f142f5 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install Node.js (required for Webcmd) +RUN apt-get update && apt-get install -y curl && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Install Webcmd globally +RUN npm install -g @agentrhq/webcmd + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/README.md b/SLAB/Deadline_Detective_Backend_Webcmd/README.md new file mode 100644 index 000000000..040fbc28e --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/README.md @@ -0,0 +1,57 @@ +# Deadline Detective – Backend (Webcmd Version) + +AI Browser Agent that finds real college opportunities using **Webcmd** for browser automation. + +## What changed +- Replaced raw Playwright with **Webcmd** +- Still uses FastAPI + Gemini for planning and analysis +- Browser control now goes through Webcmd sessions + +## Tech Stack +- FastAPI +- Google Gemini +- **Webcmd** (self-learning browser infrastructure) +- Docker ready + +## Prerequisites + +1. Node.js 20+ +2. Webcmd installed: + ```bash + npm install -g @agentrhq/webcmd + webcmd doctor + ``` + +## Local Setup + +```bash +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements.txt + +# Create .env +cp .env.example .env +# Edit .env and add your GEMINI_API_KEY + +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +## API + +- `POST /research` → main agent endpoint +- `GET /health` → health check +- Docs: http://localhost:8000/docs + +## How it works + +1. Gemini creates a research plan +2. Webcmd opens a browser session +3. Webcmd visits real websites and extracts content +4. Gemini analyzes eligibility + deadlines +5. Returns prioritized action plan + +## Deploy on Render + +- Use the included Dockerfile +- Add environment variable `GEMINI_API_KEY` +- Make sure the instance has enough memory (Webcmd + browser needs it) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc new file mode 100644 index 000000000..e1aa20de5 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py new file mode 100644 index 000000000..b9ca8ad19 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py @@ -0,0 +1 @@ +# Deadline Detective Backend diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..565caa69d Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc new file mode 100644 index 000000000..7f0dbcd81 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 000000000..3f31abfe7 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py new file mode 100644 index 000000000..ed088e40c --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py @@ -0,0 +1,17 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + gemini_api_key: str + webcmd_profile: str = "default" + max_pages: int = 5 + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +@lru_cache() +def get_settings() -> Settings: + return Settings() diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py new file mode 100644 index 000000000..f7dfa77c6 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, Field, HttpUrl +from typing import List, Optional +from enum import Enum + + +class StudentProfile(BaseModel): + year: Optional[str] = Field(None, example="1st Year") + branch: Optional[str] = Field(None, example="CSE / AI") + interests: Optional[str] = Field(None, example="hackathons, AI, web development") + location: Optional[str] = Field(None, example="India") + + +class ResearchRequest(BaseModel): + task: str = Field( + ..., + min_length=10, + example="Find currently open opportunities for a first-year CSE/AI student. Check official pages, verify eligibility and deadline, and create a priority list of what I should apply for this week." + ) + profile: Optional[StudentProfile] = None + + +class Opportunity(BaseModel): + title: str + eligible: bool + deadline: Optional[str] = None + what_to_do: str + source: str + reason: Optional[str] = None + + +class ResearchResponse(BaseModel): + opportunities: List[Opportunity] + next_3_actions: List[str] + summary: str + sources_checked: List[str] = [] + + +class HealthResponse(BaseModel): + status: str + message: str diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py new file mode 100644 index 000000000..873f7bbbe --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py @@ -0,0 +1 @@ +# Routers package diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..144311244 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc new file mode 100644 index 000000000..a9459978e Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py new file mode 100644 index 000000000..0f336d643 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, HTTPException +from app.models import ResearchRequest, ResearchResponse, HealthResponse +from app.services.agent import DeadlineDetectiveAgent + +router = APIRouter(tags=["Research"]) + +agent = DeadlineDetectiveAgent() + + +@router.post("/research", response_model=ResearchResponse) +async def run_research(request: ResearchRequest): + """ + Main endpoint: Run the Deadline Detective browser agent. + """ + if not request.task or len(request.task.strip()) < 10: + raise HTTPException(status_code=400, detail="Task must be at least 10 characters long.") + + result = await agent.run(request) + return result + + +@router.get("/health", response_model=HealthResponse) +async def health_check(): + return HealthResponse( + status="ok", + message="Deadline Detective backend is running" + ) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py new file mode 100644 index 000000000..453a80dff --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py @@ -0,0 +1 @@ +# Services package - Webcmd version diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..ad91bb3b8 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc new file mode 100644 index 000000000..66a7ec0bf Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc new file mode 100644 index 000000000..37be005e5 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc new file mode 100644 index 000000000..12e1fbb39 Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc differ diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py new file mode 100644 index 000000000..6d3281038 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py @@ -0,0 +1,69 @@ +from app.services.gemini import GeminiService +from app.services.webcmd_service import WebcmdService +from app.models import ResearchRequest, ResearchResponse, Opportunity +from typing import List +import traceback + + +class DeadlineDetectiveAgent: + def __init__(self): + self.gemini = GeminiService() + self.webcmd = WebcmdService() + + async def run(self, request: ResearchRequest) -> ResearchResponse: + try: + # Step 1: Gemini creates the research plan + plan = self.gemini.create_research_plan( + task=request.task, + profile=request.profile + ) + + # Step 2: Webcmd collects evidence from real websites + evidence = await self.webcmd.search_and_collect(plan) + + if not evidence: + return ResearchResponse( + opportunities=[], + next_3_actions=["No opportunities found. Try a more specific query or check if Webcmd is installed."], + summary="The agent could not extract useful information. Make sure Webcmd is installed and working (`webcmd doctor`).", + sources_checked=[] + ) + + # Step 3: Gemini analyzes eligibility and ranks + analysis = self.gemini.analyze_and_rank( + task=request.task, + profile=request.profile, + extracted_data=evidence + ) + + # Step 4: Build final structured response + opportunities: List[Opportunity] = [] + for item in analysis.get("opportunities", []): + opportunities.append( + Opportunity( + title=item.get("title", "Unknown"), + eligible=bool(item.get("eligible", False)), + deadline=item.get("deadline"), + what_to_do=item.get("what_to_do", "Check the official page"), + source=item.get("source", ""), + reason=item.get("reason") + ) + ) + + sources = [e["url"] for e in evidence if e.get("url")] + + return ResearchResponse( + opportunities=opportunities, + next_3_actions=analysis.get("next_3_actions", []), + summary=analysis.get("summary", "Research completed using Webcmd browser agent."), + sources_checked=sources + ) + + except Exception as e: + traceback.print_exc() + return ResearchResponse( + opportunities=[], + next_3_actions=[], + summary=f"An error occurred while running the agent: {str(e)[:300]}", + sources_checked=[] + ) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py new file mode 100644 index 000000000..927acf03d --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py @@ -0,0 +1,181 @@ +from google import genai +from google.genai import types +from app.config import get_settings +from app.models import StudentProfile, Opportunity +from typing import List, Dict, Any +import json +import re +import time + + +class GeminiService: + def __init__(self): + settings = get_settings() + self.client = genai.Client(api_key=settings.gemini_api_key) + self.model = "gemini-3.6-flash" + + def _extract_json(self, text: str) -> Any: + """Extract JSON from Gemini response (handles markdown code blocks).""" + text = text.strip() + # Try to find JSON inside ```json ... ``` + match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text) + if match: + text = match.group(1).strip() + try: + return json.loads(text) + except json.JSONDecodeError: + # Try to find first { ... } or [ ... ] + start = text.find("{") + end = text.rfind("}") + 1 + if start != -1 and end > start: + try: + return json.loads(text[start:end]) + except json.JSONDecodeError: + pass + # Try array + start = text.find("[") + end = text.rfind("]") + 1 + if start != -1 and end > start: + try: + return json.loads(text[start:end]) + except json.JSONDecodeError: + pass + raise + + def _generate_with_retry(self, prompt: str, max_tokens: int, temperature: float = 0.2, retries: int = 2) -> Any: + """Call Gemini with retry logic for transient failures and JSON parsing.""" + last_error = None + for attempt in range(retries + 1): + try: + response = self.client.models.generate_content( + model=self.model, + contents=prompt, + config=types.GenerateContentConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ) + ) + return self._extract_json(response.text) + except (json.JSONDecodeError, Exception) as e: + last_error = e + print(f"[Gemini] Attempt {attempt + 1} failed: {e}") + if attempt < retries: + time.sleep(1.5 * (attempt + 1)) + # If all retries failed, try once more without response_mime_type + try: + print("[Gemini] Trying without response_mime_type constraint...") + response = self.client.models.generate_content( + model=self.model, + contents=prompt, + config=types.GenerateContentConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ) + ) + return self._extract_json(response.text) + except Exception as e: + print(f"[Gemini] Final fallback also failed: {e}") + raise last_error or e + + def create_research_plan(self, task: str, profile: StudentProfile | None) -> Dict[str, Any]: + profile_text = "No specific profile provided." + if profile: + profile_text = ( + f"Year: {profile.year or 'Not specified'}\n" + f"Branch: {profile.branch or 'Not specified'}\n" + f"Interests: {profile.interests or 'Not specified'}\n" + f"Location: {profile.location or 'India'}" + ) + + prompt = f""" +You are an expert research planner for college students in India. + +Student Profile: +{profile_text} + +User Task: +{task} + +Create a focused research plan to find currently open opportunities (hackathons, internships, scholarships, competitions, college events). + +Return ONLY valid JSON in this exact format: +{{ + "search_queries": ["query1", "query2", "query3"], + "target_sites": ["https://example.com", "..."], + "extraction_goals": ["title", "eligibility", "deadline", "application link"], + "priority": "focus on deadlines within next 14-21 days and first-year / beginner friendly opportunities" +}} + +Rules: +- Prefer official pages and well-known platforms (Unstop, Devfolio, Internshala, college sites, AICTE, etc.) +- Maximum 5 search queries +- Maximum 6 target sites +- Make queries specific to the student profile +""" + + return self._generate_with_retry(prompt, max_tokens=4096, temperature=0.2) + + def analyze_and_rank( + self, + task: str, + profile: StudentProfile | None, + extracted_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + profile_text = "No specific profile provided." + if profile: + profile_text = ( + f"Year: {profile.year or 'Not specified'}\n" + f"Branch: {profile.branch or 'Not specified'}\n" + f"Interests: {profile.interests or 'Not specified'}" + ) + + data_text = json.dumps(extracted_data, indent=2, ensure_ascii=False) + + prompt = f""" +You are Deadline Detective – an expert AI that helps college students find real opportunities. + +Student Profile: +{profile_text} + +Original Task: +{task} + +Here is the raw data extracted from live websites: +{data_text} + +Your job: +1. Filter only relevant and currently open opportunities. +2. Decide eligibility based on the student profile (be strict but fair). +3. Extract clean deadline (prefer exact date). +4. Create a clear "what_to_do" action. +5. Rank by urgency + relevance. + +Return ONLY valid JSON in this exact format: +{{ + "opportunities": [ + {{ + "title": "Name of opportunity", + "eligible": true, + "deadline": "15 Sept 2025 or null", + "what_to_do": "Register on the official page / Apply before deadline", + "source": "https://full-url.com", + "reason": "Short reason for eligibility decision" + }} + ], + "next_3_actions": [ + "1. Opportunity Name — deadline XX — do this", + "2. ...", + "3. ..." + ], + "summary": "2-3 sentence overall summary for the student" +}} + +Rules: +- Only include real opportunities that appear in the data. +- If not eligible, still include it with eligible=false and clear reason. +- Prefer opportunities with clear deadlines in the near future. +- next_3_actions should only contain eligible items when possible. +- Be honest. Do not invent deadlines or opportunities. +""" + + return self._generate_with_retry(prompt, max_tokens=8192, temperature=0.1) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py new file mode 100644 index 000000000..dd67a997b --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py @@ -0,0 +1,211 @@ +import asyncio +import json +import subprocess +import shlex +from typing import List, Dict, Any, Optional +from app.config import get_settings + + +class WebcmdService: + """ + Service that controls the browser through Webcmd CLI. + Replaces the old Playwright service. + """ + + def __init__(self): + self.settings = get_settings() + self.profile = self.settings.webcmd_profile + self.session_id: Optional[str] = None + + def _run_cmd(self, args: List[str], input_text: str = None, timeout: int = 60) -> Dict[str, Any]: + """Run a webcmd command and return parsed JSON when possible.""" + webcmd_path = __import__('shutil').which('webcmd') or 'webcmd' + cmd = [webcmd_path, "--profile", self.profile] + args + + try: + result = subprocess.run( + cmd, + input=input_text, + capture_output=True, + text=True, + timeout=timeout, + ) + + stdout = result.stdout.strip() + stderr = result.stderr.strip() + + if result.returncode != 0: + return { + "success": False, + "error": stderr or stdout or "Webcmd command failed", + "raw": stdout + } + + # Try to parse JSON + try: + data = json.loads(stdout) + return {"success": True, "data": data, "raw": stdout} + except json.JSONDecodeError: + return {"success": True, "data": stdout, "raw": stdout} + + except subprocess.TimeoutExpired: + return {"success": False, "error": "Webcmd command timed out"} + except FileNotFoundError: + return { + "success": False, + "error": "webcmd command not found. Please install it: npm install -g @agentrhq/webcmd" + } + except Exception as e: + return {"success": False, "error": str(e)} + + async def start_session(self, name: str = "DeadlineDetective") -> bool: + """Create a new Webcmd browser session.""" + result = self._run_cmd(["session", "create", name, "-f", "json"]) + + if not result["success"]: + print(f"[Webcmd] Failed to create session: {result.get('error')}") + return False + + data = result.get("data") + if isinstance(data, dict): + self.session_id = data.get("id") or data.get("session_id") or data.get("session") + elif isinstance(data, str): + # Sometimes the ID is printed as plain text + self.session_id = data.strip() + + if not self.session_id: + # Fallback: try to extract from raw output + raw = result.get("raw", "") + for line in raw.splitlines(): + if "id:" in line.lower(): + self.session_id = line.split(":")[-1].strip() + break + + print(f"[Webcmd] Session started: {self.session_id}") + return bool(self.session_id) + + async def close_session(self): + """Close the current Webcmd session.""" + if not self.session_id: + return + + self._run_cmd(["session", "close", self.session_id]) + print(f"[Webcmd] Session closed: {self.session_id}") + self.session_id = None + + async def run_browser_js(self, js_code: str, timeout: int = 45) -> Dict[str, Any]: + """ + Execute a Playwright-style JavaScript program inside the Webcmd session. + """ + if not self.session_id: + return {"success": False, "error": "No active Webcmd session"} + + args = [ + "--session", self.session_id, + "browser", "run", + "--stdin", + "--no-snapshot-diff", + "--timeout", str(timeout), + "-f", "json" + ] + + result = self._run_cmd(args, input_text=js_code, timeout=timeout + 10) + return result + + async def visit_and_extract(self, url: str) -> Dict[str, Any]: + """Visit a URL and extract title + readable text.""" + js_code = f""" +await page.goto('{url}', {{ waitUntil: 'domcontentloaded', timeout: 30000 }}); +await page.waitForTimeout(1500); + +const title = await page.title(); +const text = await page.innerText('body'); + +return {{ + url: page.url(), + title: title, + text: text.slice(0, 10000) +}}; +""" + result = await self.run_browser_js(js_code) + + if not result["success"]: + return { + "url": url, + "title": "", + "text": "", + "success": False, + "error": result.get("error", "Failed to extract") + } + + data = result.get("data") + if isinstance(data, dict): + # Webcmd sometimes wraps the return value + content = data.get("result") or data.get("data") or data + if isinstance(content, dict): + return { + "url": content.get("url", url), + "title": content.get("title", ""), + "text": content.get("text", ""), + "success": True, + "error": None + } + + return { + "url": url, + "title": "", + "text": str(data)[:5000] if data else "", + "success": True, + "error": None + } + + async def search_and_collect(self, plan: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Execute the research plan using Webcmd. + Visits target sites + does simple searches. + """ + if not await self.start_session("DeadlineDetective"): + return [] + + all_urls = set() + + # Add target sites from plan + for site in plan.get("target_sites", [])[:4]: + if isinstance(site, str) and site.startswith("http"): + all_urls.add(site) + + # Simple search via DuckDuckGo using browser + for query in plan.get("search_queries", [])[:2]: + try: + search_js = f""" +await page.goto('https://duckduckgo.com/?q={query.replace(" ", "+")}', {{ waitUntil: 'domcontentloaded' }}); +await page.waitForTimeout(2000); + +const links = await page.$$eval('a[data-testid="result-title-a"], a.result__a', els => + els.slice(0, 3).map(a => a.href).filter(h => h && h.startsWith('http')) +); +return {{ links }}; +""" + search_result = await self.run_browser_js(search_js) + if search_result["success"]: + data = search_result.get("data") + if isinstance(data, dict): + content = data.get("result") or data.get("data") or data + links = content.get("links", []) if isinstance(content, dict) else [] + for link in links: + all_urls.add(link) + except Exception as e: + print(f"[Webcmd] Search failed for '{query}': {e}") + + urls_to_visit = list(all_urls)[: self.settings.max_pages] + results = [] + + for url in urls_to_visit: + print(f"[Webcmd] Visiting: {url}") + page_data = await self.visit_and_extract(url) + if page_data.get("success") and len(page_data.get("text", "")) > 80: + results.append(page_data) + await asyncio.sleep(1) + + await self.close_session() + return results diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py b/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py new file mode 100644 index 000000000..ea6f9ecfa --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py @@ -0,0 +1,12 @@ +import os +from google import genai +from dotenv import load_dotenv + +load_dotenv() +api_key = os.environ.get("GEMINI_API_KEY") +client = genai.Client(api_key=api_key) + +print("Available models:") +for m in client.models.list(): + if "gemini" in m.name: + print(m.name) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/main.py b/SLAB/Deadline_Detective_Backend_Webcmd/main.py new file mode 100644 index 000000000..54136e820 --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/main.py @@ -0,0 +1,33 @@ +from pathlib import Path +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.routers import research + +app = FastAPI( + title="Deadline Detective", + description="AI Browser Agent that finds real college opportunities, verifies eligibility & deadlines, and creates action plans.", + version="1.0.0" +) + +# Allow frontend (Vercel / local / Render static) to call the API +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # For hackathon – tighten later + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(research.router) + +# Serve frontend static files +FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" + +app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static") + + +@app.get("/") +async def root(): + return FileResponse(str(FRONTEND_DIR / "index.html")) diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt b/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt new file mode 100644 index 000000000..63be73c8a --- /dev/null +++ b/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +google-genai==0.3.0 +pydantic==2.9.2 +pydantic-settings==2.5.2 +python-dotenv==1.0.1 +httpx==0.27.2 diff --git a/SLAB/frontend/index.html b/SLAB/frontend/index.html new file mode 100644 index 000000000..f667dd5e5 --- /dev/null +++ b/SLAB/frontend/index.html @@ -0,0 +1,172 @@ + + + + + +Deadline Detective — AI Browser Agent for College Opportunities + + + + + + + +
+ + + +
+ + +
+
+ File No. 001 +

Open a new case

+

Tell the detective what you're hunting for — scholarships, hackathons, internships, research programs — and it'll comb the web for leads.

+
+ +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + + + + + + + + + +
+ + + + + + diff --git a/SLAB/frontend/script.js b/SLAB/frontend/script.js new file mode 100644 index 000000000..7db7e8fea --- /dev/null +++ b/SLAB/frontend/script.js @@ -0,0 +1,251 @@ +(() => { + const form = document.getElementById('research-form'); + const submitBtn = document.getElementById('submit-btn'); + const btnLabel = submitBtn.querySelector('.btn-label'); + + const intakePanel = document.getElementById('intake-panel'); + const loadingPanel = document.getElementById('loading-panel'); + const errorPanel = document.getElementById('error-panel'); + const errorMessage = document.getElementById('error-message'); + const resultsSection = document.getElementById('results-section'); + const retryBtn = document.getElementById('retry-btn'); + + const progressItems = Array.from(document.querySelectorAll('#progress-list li')); + + const summaryText = document.getElementById('summary-text'); + const actionsList = document.getElementById('actions-list'); + const resultsTbody = document.getElementById('results-tbody'); + const sourcesList = document.getElementById('sources-list'); + + const API_ENDPOINT = '/research'; + + let progressTimer = null; + + function showPanel(el) { el.hidden = false; } + function hidePanel(el) { el.hidden = true; } + + function resetProgressUI() { + progressItems.forEach(li => li.classList.remove('active', 'done')); + } + + // Step through the progress list while the request is in flight. + // If the response comes back before the animation finishes, we + // immediately mark everything done in advance() on completion. + function startProgressAnimation() { + resetProgressUI(); + let index = 0; + const advance = () => { + progressItems.forEach((li, i) => { + li.classList.toggle('active', i === index); + li.classList.toggle('done', i < index); + }); + if (index < progressItems.length - 1) { + index += 1; + progressTimer = setTimeout(advance, 1600); + } + }; + advance(); + } + + function stopProgressAnimation() { + if (progressTimer) { + clearTimeout(progressTimer); + progressTimer = null; + } + progressItems.forEach(li => { + li.classList.add('done'); + li.classList.remove('active'); + }); + } + + function setSubmitting(isSubmitting) { + submitBtn.disabled = isSubmitting; + btnLabel.textContent = isSubmitting ? 'Investigating…' : 'Start Research'; + } + + function escapeHtml(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function isLikelyUrl(str) { + if (!str) return false; + try { + const u = new URL(str); + return u.protocol === 'http:' || u.protocol === 'https:'; + } catch { + return false; + } + } + + function renderEligibilityPill(value) { + const v = String(value ?? '').toLowerCase(); + if (v === 'true' || v === 'yes' || v === 'eligible') { + return 'Eligible'; + } + if (v === 'false' || v === 'no' || v === 'not eligible' || v === 'ineligible') { + return 'Not eligible'; + } + return `${escapeHtml(value || 'Unclear')}`; + } + + function isEligibleValue(value) { + const v = String(value ?? '').toLowerCase(); + return v === 'true' || v === 'yes' || v === 'eligible'; + } + + function renderResults(data) { + // Summary + summaryText.textContent = data.summary || 'The investigation is complete. See the findings below.'; + + // Next actions + const actions = data.next_3_actions || data.next_actions || data.nextActions || data.actions || []; + actionsList.innerHTML = ''; + if (actions.length === 0) { + const li = document.createElement('li'); + li.innerHTML = 'No specific actions were identified. Review the opportunities table below.'; + actionsList.appendChild(li); + } else { + actions.slice(0, 3).forEach(action => { + const li = document.createElement('li'); + li.innerHTML = `${escapeHtml(action)}`; + actionsList.appendChild(li); + }); + } + + // Opportunities table + const opportunities = data.opportunities || data.results || []; + resultsTbody.innerHTML = ''; + if (opportunities.length === 0) { + const tr = document.createElement('tr'); + tr.innerHTML = 'No opportunities were found for this task. Try broadening the search.'; + resultsTbody.appendChild(tr); + } else { + opportunities.forEach(op => { + const name = op.name || op.opportunity || op.title || 'Untitled opportunity'; + const eligible = op.eligible ?? op.eligibility; + const deadline = op.deadline || 'Not specified'; + const action = op.action || op.what_to_do || op.next_step || '—'; + const source = op.source || op.source_url || op.url || ''; + + const tr = document.createElement('tr'); + if (isEligibleValue(eligible)) tr.classList.add('eligible-row'); + + const sourceCell = isLikelyUrl(source) + ? `${escapeHtml(source)}` + : escapeHtml(source || 'Not provided'); + + tr.innerHTML = ` + ${escapeHtml(name)} + ${renderEligibilityPill(eligible)} + ${escapeHtml(deadline)} + ${escapeHtml(action)} + ${sourceCell} + `; + resultsTbody.appendChild(tr); + }); + } + + // Sources checked + const sources = data.sources || data.sources_checked || data.sourcesChecked || []; + sourcesList.innerHTML = ''; + if (sources.length === 0) { + const li = document.createElement('li'); + li.textContent = 'No source list was returned.'; + sourcesList.appendChild(li); + } else { + sources.forEach(src => { + const li = document.createElement('li'); + if (isLikelyUrl(src)) { + li.innerHTML = `${escapeHtml(src)}`; + } else { + li.textContent = src; + } + sourcesList.appendChild(li); + }); + } + } + + function showError(message) { + errorMessage.textContent = message || 'Something interrupted the investigation. Please try again.'; + hidePanel(loadingPanel); + hidePanel(resultsSection); + showPanel(errorPanel); + showPanel(intakePanel); + } + + async function runResearch(payload) { + stopProgressAnimation(); // clear any stale timers first + resetProgressUI(); + hidePanel(errorPanel); + hidePanel(resultsSection); + hidePanel(intakePanel); + showPanel(loadingPanel); + setSubmitting(true); + startProgressAnimation(); + + try { + const response = await fetch(API_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + let detail = ''; + try { + const errJson = await response.json(); + detail = errJson.detail || errJson.message || ''; + } catch { + /* ignore parse errors on error body */ + } + throw new Error(detail || `The research service returned an error (${response.status}).`); + } + + const data = await response.json(); + stopProgressAnimation(); + + // brief pause so the final "done" state is visible before switching views + await new Promise(r => setTimeout(r, 350)); + + renderResults(data); + hidePanel(loadingPanel); + showPanel(resultsSection); + } catch (err) { + stopProgressAnimation(); + showError( + err && err.message + ? err.message + : 'Could not reach the research service. Check your connection and try again.' + ); + } finally { + setSubmitting(false); + } + } + + form.addEventListener('submit', (e) => { + e.preventDefault(); + + const task = document.getElementById('task').value.trim(); + if (!task) return; + + const payload = { + task, + profile: { + year: document.getElementById('year').value || null, + branch: document.getElementById('branch').value.trim() || null, + interests: document.getElementById('interests').value.trim() || null, + }, + }; + + runResearch(payload); + }); + + retryBtn.addEventListener('click', () => { + hidePanel(errorPanel); + showPanel(intakePanel); + }); +})(); diff --git a/SLAB/frontend/style.css b/SLAB/frontend/style.css new file mode 100644 index 000000000..7c8d4a024 --- /dev/null +++ b/SLAB/frontend/style.css @@ -0,0 +1,497 @@ +/* ------------------------------------------------------- + Deadline Detective — Design tokens + Color: + --ink #12172A (base background, deep ink navy) + --ink-soft #1B2140 (raised panels on dark) + --paper #F6F1E4 (case-file paper cream) + --paper-line #E4DBC4 (paper rules / borders) + --brass #C69A3C (accent — desk-lamp brass) + --brass-dark #9C7626 (accent, pressed/hover) + --ok #3F7D58 (eligible green, muted) + --no #A24B3F (not eligible, muted rust) + --ink-text #21241C (body text on paper) + Type: + Display / headings: 'Fraunces' (warm slab-serif, case-file stamp feel) + Body / UI: 'Inter' + Layout: single column, centered, max-width 880px, generous vertical rhythm +------------------------------------------------------- */ + +:root { + --ink: #12172A; + --ink-soft: #1B2140; + --ink-line: #2C335A; + --paper: #F6F1E4; + --paper-raised: #FBF8EF; + --paper-line: #E4DBC4; + --brass: #C69A3C; + --brass-dark: #9C7626; + --ok: #3F7D58; + --ok-bg: #E7F0E7; + --no: #A24B3F; + --no-bg: #F5E7E3; + --ink-text: #21241C; + --muted-text: #6B6455; + --radius-panel: 6px; + --shadow-panel: 0 18px 40px -20px rgba(0,0,0,0.55); + --font-display: 'Fraunces', Georgia, serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + background: var(--ink); + color: var(--paper); + font-family: var(--font-body); + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +.case-backdrop { + position: fixed; + inset: 0; + z-index: -1; + background: + radial-gradient(ellipse 900px 500px at 15% -10%, rgba(198,154,60,0.10), transparent 60%), + radial-gradient(ellipse 700px 500px at 100% 10%, rgba(198,154,60,0.06), transparent 55%), + var(--ink); +} + +a { color: var(--brass); } + +:focus-visible { + outline: 2px solid var(--brass); + outline-offset: 3px; +} + +/* ---------- Header ---------- */ + +.site-header { + padding: 2.4rem 1.5rem 1.6rem; + border-bottom: 1px solid var(--ink-line); +} + +.header-inner { + max-width: 880px; + margin: 0 auto; + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 1.5rem; + flex-wrap: wrap; +} + +.brand { + display: flex; + align-items: center; + gap: 0.85rem; +} + +.brand-mark { + color: var(--brass); + flex-shrink: 0; +} + +.brand-text h1 { + font-family: var(--font-display); + font-weight: 600; + font-size: clamp(1.7rem, 3.4vw, 2.3rem); + margin: 0 0 0.15rem; + letter-spacing: -0.01em; + color: var(--paper); +} + +.tagline { + margin: 0; + color: #B9BEDB; + font-size: 0.95rem; +} + +.case-stamp { + font-family: var(--font-body); + font-size: 0.78rem; + color: var(--brass); + border: 1px solid var(--ink-line); + padding: 0.4rem 0.75rem; + border-radius: 999px; + white-space: nowrap; +} + +/* ---------- Layout ---------- */ + +main { + max-width: 880px; + margin: 0 auto; + padding: 2.5rem 1.5rem 4rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.panel { + background: var(--paper); + color: var(--ink-text); + border-radius: var(--radius-panel); + box-shadow: var(--shadow-panel); + padding: 2rem; + border: 1px solid var(--paper-line); +} + +.panel-header { margin-bottom: 1.4rem; } + +.panel-index { + display: inline-block; + font-size: 0.72rem; + letter-spacing: 0.02em; + color: var(--brass-dark); + font-weight: 600; + margin-bottom: 0.5rem; +} + +.panel-header h2 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.4rem; + color: var(--ink-text); +} + +.panel-sub { + margin: 0; + color: var(--muted-text); + max-width: 60ch; +} + +/* ---------- Intake form ---------- */ + +.field { margin-bottom: 1.3rem; } + +.field label { + display: block; + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 0.45rem; + color: var(--ink-text); +} + +textarea, input[type="text"], select { + width: 100%; + font-family: var(--font-body); + font-size: 0.96rem; + color: var(--ink-text); + background: var(--paper-raised); + border: 1px solid var(--paper-line); + border-radius: 4px; + padding: 0.75rem 0.9rem; + resize: vertical; + transition: border-color 0.15s ease; +} + +textarea:focus, input[type="text"]:focus, select:focus { + border-color: var(--brass); +} + +.field-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 0.5rem; +} + +.btn-primary { + margin-top: 0.6rem; + display: inline-flex; + align-items: center; + gap: 0.6rem; + background: var(--ink); + color: var(--paper); + border: none; + font-family: var(--font-body); + font-weight: 600; + font-size: 1rem; + padding: 0.85rem 1.6rem; + border-radius: 4px; + cursor: pointer; + transition: background 0.15s ease, transform 0.1s ease; +} + +.btn-primary:hover:not(:disabled) { background: #23294A; } +.btn-primary:active:not(:disabled) { transform: translateY(1px); } + +.btn-primary:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.btn-icon { flex-shrink: 0; } + +.btn-secondary { + background: transparent; + color: var(--ink-text); + border: 1px solid var(--paper-line); + font-family: var(--font-body); + font-weight: 600; + font-size: 0.95rem; + padding: 0.7rem 1.4rem; + border-radius: 4px; + cursor: pointer; +} + +.btn-secondary:hover { border-color: var(--brass); color: var(--brass-dark); } + +/* ---------- Loading ---------- */ + +.loading-panel { text-align: center; } + +.loading-inner { + display: flex; + flex-direction: column; + align-items: center; + padding: 1rem 0; +} + +.magnifier-spinner { + color: var(--brass); + margin-bottom: 1.2rem; + animation: spin-sweep 1.8s ease-in-out infinite; +} + +@keyframes spin-sweep { + 0% { transform: rotate(0deg); } + 50% { transform: rotate(25deg); } + 100% { transform: rotate(0deg); } +} + +.loading-panel h2 { + font-family: var(--font-display); + font-size: 1.4rem; + margin: 0 0 1.4rem; +} + +.progress-list { + list-style: none; + margin: 0; + padding: 0; + text-align: left; + display: inline-flex; + flex-direction: column; + gap: 0.75rem; + min-width: min(420px, 100%); +} + +.progress-list li { + display: flex; + align-items: center; + gap: 0.7rem; + color: var(--muted-text); + font-size: 0.95rem; + transition: color 0.2s ease; +} + +.step-marker { + width: 9px; + height: 9px; + border-radius: 50%; + border: 1.5px solid var(--paper-line); + flex-shrink: 0; + transition: background 0.2s ease, border-color 0.2s ease; +} + +.progress-list li.active { + color: var(--ink-text); + font-weight: 600; +} + +.progress-list li.active .step-marker { + background: var(--brass); + border-color: var(--brass); + box-shadow: 0 0 0 4px rgba(198,154,60,0.18); +} + +.progress-list li.done .step-marker { + background: var(--ok); + border-color: var(--ok); +} + +.progress-list li.done { color: var(--muted-text); } + +/* ---------- Error ---------- */ + +.error-panel { + text-align: left; + border-left: 4px solid var(--no); +} + +.error-panel h2 { + font-family: var(--font-display); + font-size: 1.4rem; + margin: 0 0 0.5rem; +} + +.error-panel p { color: var(--muted-text); margin: 0 0 1.2rem; } + +/* ---------- Results ---------- */ + +.results-wrap { + display: flex; + flex-direction: column; + gap: 1.6rem; +} + +.summary-text { + margin: 0; + font-size: 1.02rem; + color: var(--ink-text); + max-width: 68ch; +} + +/* Next 3 actions — most prominent card */ +.actions-panel { + background: var(--ink-soft); + color: var(--paper); + border: 1px solid var(--ink-line); + box-shadow: 0 22px 48px -18px rgba(0,0,0,0.65); +} + +.actions-panel .panel-index { color: var(--brass); } +.actions-panel .panel-header h2 { color: var(--paper); } + +.actions-list { + list-style: none; + counter-reset: action-counter; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.actions-list li { + counter-increment: action-counter; + display: flex; + gap: 1rem; + align-items: flex-start; + background: rgba(246,241,228,0.05); + border: 1px solid var(--ink-line); + border-radius: 5px; + padding: 0.95rem 1.1rem; +} + +.actions-list li::before { + content: counter(action-counter); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.1rem; + color: var(--ink); + background: var(--brass); + width: 1.9rem; + height: 1.9rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.actions-list .action-text { padding-top: 0.15rem; font-size: 0.98rem; } + +/* Table */ + +.table-scroll { overflow-x: auto; } + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; + min-width: 640px; +} + +thead th { + text-align: left; + font-size: 0.75rem; + letter-spacing: 0.02em; + color: var(--muted-text); + font-weight: 600; + padding: 0.6rem 0.8rem; + border-bottom: 2px solid var(--paper-line); +} + +tbody td { + padding: 0.85rem 0.8rem; + border-bottom: 1px solid var(--paper-line); + vertical-align: top; +} + +tbody tr:last-child td { border-bottom: none; } + +tbody tr.eligible-row { + background: var(--ok-bg); +} + +.eligible-pill, .not-eligible-pill, .unclear-pill { + display: inline-block; + font-size: 0.78rem; + font-weight: 600; + padding: 0.25rem 0.6rem; + border-radius: 999px; +} + +.eligible-pill { background: var(--ok); color: #fff; } +.not-eligible-pill { background: var(--no); color: #fff; } +.unclear-pill { background: #D8CBA0; color: #5C4A1E; } + +td.opportunity-cell { font-weight: 600; } + +td a.source-link { + color: var(--brass-dark); + text-decoration: underline; + text-underline-offset: 2px; + word-break: break-word; +} + +/* Sources */ + +.sources-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sources-list li { + font-size: 0.9rem; + padding-bottom: 0.55rem; + border-bottom: 1px dashed var(--paper-line); +} + +.sources-list li:last-child { border-bottom: none; padding-bottom: 0; } + +.sources-list a { word-break: break-word; } + +/* ---------- Footer ---------- */ + +.site-footer { + text-align: center; + padding: 2rem 1.5rem 3rem; + color: #8890B8; + font-size: 0.85rem; +} + +/* ---------- Responsive ---------- */ + +@media (max-width: 640px) { + .field-grid { grid-template-columns: 1fr; } + .panel { padding: 1.4rem; } + .header-inner { align-items: flex-start; } + .case-stamp { order: 3; } +} + +@media (prefers-reduced-motion: reduce) { + .magnifier-spinner { animation: none; } + html { scroll-behavior: auto; } +}