""" PIOE LLM Client Abstraction Layer Supports Gemini (default) and OpenAI as providers. """ from abc import ABC, abstractmethod from typing import Optional import json from ..config import get_settings class BaseLLMClient(ABC): """Abstract base class for LLM providers.""" @abstractmethod def classify(self, text: str) -> dict: """Classify opportunity text into category and domain.""" pass @abstractmethod def summarize(self, text: str, max_length: int = 150) -> str: """Generate concise summary of opportunity.""" pass @abstractmethod def recommend_action(self, opportunity: dict) -> dict: """Recommend action based on opportunity context.""" pass @abstractmethod def extract_metadata(self, text: str) -> dict: """Extract structured metadata (deadline, location, reward, etc.).""" pass class GeminiClient(BaseLLMClient): """Google Gemini implementation.""" def __init__(self, api_key: str): import google.generativeai as genai genai.configure(api_key=api_key) self.model = genai.GenerativeModel('gemini-2.5-flash') def _generate(self, prompt: str, as_json: bool = False) -> str: """Generate response from Gemini.""" response = self.model.generate_content(prompt) return response.text def classify(self, text: str) -> dict: """Classify opportunity into category and domain.""" prompt = f"""Analyze this opportunity and classify it. Return JSON only. TEXT: {text[:2000]} Return this exact JSON structure: {{ "category": "one of: scholarship, fellowship, internship, job, research, hackathon, competition, grant, conference, open_source, investment, weak_signal, other", "domain": "one of: ai, computer_vision, robotics, finance, crypto, academia, mixed", "confidence": 0.0 to 1.0 }}""" try: result = self._generate(prompt) # Extract JSON from response start = result.find('{') end = result.rfind('}') + 1 if start != -1 and end > start: return json.loads(result[start:end]) except Exception as e: print(f"Classification error: {e}") return {"category": "other", "domain": "mixed", "confidence": 0.0} def summarize(self, text: str, max_length: int = 150) -> str: """Generate concise summary.""" prompt = f"""Summarize this opportunity in {max_length} characters or less. Focus on: what it is, who it's for, and deadline if any. TEXT: {text[:2000]} Return only the summary, no quotes or labels.""" try: return self._generate(prompt).strip()[:max_length] except Exception as e: print(f"Summary error: {e}") return text[:max_length] def recommend_action(self, opportunity: dict) -> dict: """ PIOE 2.0 Enhanced Action Guidance. Returns comprehensive recommendations for how to approach the opportunity. """ prompt = f"""You are an expert career and opportunity advisor. Analyze this opportunity and provide detailed action guidance. OPPORTUNITY DETAILS: - Title: {opportunity.get('title', '')} - Category: {opportunity.get('category', '')} - Domain: {opportunity.get('domain', '')} - Deadline: {opportunity.get('deadline', 'No deadline specified')} - Description: {opportunity.get('raw_text', '')[:1500]} - ROI Score: {opportunity.get('roi_score', 'N/A')} - Competition Level: {opportunity.get('competition_level', 'N/A')} - Region: {opportunity.get('region', 'global')} USER CONTEXT: - Location: Nigeria, Africa - Interests: AI, Computer Vision, Robotics, Web3 - Status: Student/Early Career Provide strategic action guidance. Return JSON only: {{ "primary_action": "one of: apply_now, apply_prepared, track, save_for_later, deep_research, network_first, skip", "urgency": "one of: immediate, this_week, this_month, whenever, expired", "timing_status": "one of: early, optimal, late, unknown", "skills_to_highlight": ["skill1", "skill2", "skill3"], "portfolio_pieces": ["project type 1", "project type 2"], "preparation_steps": [ "step 1", "step 2", "step 3" ], "networking_tips": "who to contact or how to stand out (1 sentence)", "differentiation_angle": "what unique angle to take (1 sentence)", "success_probability": 0.0 to 1.0, "time_investment_hours": estimated hours to apply well, "risk_level": "low, medium, or high", "why": "brief strategic reasoning (max 100 chars)", "red_flags": ["any concerns"] or [] }}""" try: result = self._generate(prompt) start = result.find('{') end = result.rfind('}') + 1 if start != -1 and end > start: parsed = json.loads(result[start:end]) # Ensure required fields exist return { "primary_action": parsed.get("primary_action", "save_for_later"), "urgency": parsed.get("urgency", "whenever"), "timing_status": parsed.get("timing_status", "unknown"), "skills_to_highlight": parsed.get("skills_to_highlight", []), "portfolio_pieces": parsed.get("portfolio_pieces", []), "preparation_steps": parsed.get("preparation_steps", []), "networking_tips": parsed.get("networking_tips", ""), "differentiation_angle": parsed.get("differentiation_angle", ""), "success_probability": parsed.get("success_probability", 0.3), "time_investment_hours": parsed.get("time_investment_hours", 10), "risk_level": parsed.get("risk_level", "medium"), "why": parsed.get("why", "Review and decide"), "red_flags": parsed.get("red_flags", []), } except Exception as e: print(f"Action guidance error: {e}") # Fallback response return { "primary_action": "save_for_later", "urgency": "whenever", "timing_status": "unknown", "skills_to_highlight": [], "portfolio_pieces": [], "preparation_steps": ["Review the opportunity details", "Assess fit with your goals"], "networking_tips": "", "differentiation_angle": "", "success_probability": 0.3, "time_investment_hours": 10, "risk_level": "medium", "why": "Needs manual review", "red_flags": [], } def extract_metadata(self, text: str) -> dict: """Extract structured metadata from text.""" prompt = f"""Extract metadata from this opportunity text. Return JSON only. TEXT: {text[:2000]} Return this structure (use null for missing info): {{ "deadline": "YYYY-MM-DD or null", "location": "location or 'remote' or null", "reward": "amount or null", "organization": "org name or null", "requirements": ["skill1", "skill2"] or [], "url": "application url or null" }}""" try: result = self._generate(prompt) start = result.find('{') end = result.rfind('}') + 1 if start != -1 and end > start: return json.loads(result[start:end]) except Exception as e: print(f"Metadata extraction error: {e}") return {} class OpenAIClient(BaseLLMClient): """OpenAI implementation (fallback).""" def __init__(self, api_key: str): from openai import OpenAI self.client = OpenAI(api_key=api_key) self.model = "gpt-3.5-turbo" def _generate(self, prompt: str) -> str: """Generate response from OpenAI.""" response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.3 ) return response.choices[0].message.content def classify(self, text: str) -> dict: """Classify opportunity - same logic as Gemini.""" prompt = f"""Classify this opportunity. Return JSON only with keys: category, domain, confidence. Categories: scholarship, fellowship, internship, job, research, hackathon, competition, grant, conference, open_source, investment, weak_signal, other Domains: ai, computer_vision, robotics, finance, crypto, academia, mixed TEXT: {text[:2000]}""" try: result = self._generate(prompt) start = result.find('{') end = result.rfind('}') + 1 if start != -1 and end > start: return json.loads(result[start:end]) except Exception: pass return {"category": "other", "domain": "mixed", "confidence": 0.0} def summarize(self, text: str, max_length: int = 150) -> str: prompt = f"Summarize in {max_length} chars: {text[:2000]}" try: return self._generate(prompt).strip()[:max_length] except Exception: return text[:max_length] def recommend_action(self, opportunity: dict) -> dict: return {"action": "save", "reason": "Review later", "urgency": "low"} def extract_metadata(self, text: str) -> dict: return {} class LLMClient: """ Factory class that provides the configured LLM client. Uses Gemini by default, falls back to OpenAI if configured. """ _instance: Optional[BaseLLMClient] = None @classmethod def get_client(cls) -> BaseLLMClient: """Get or create the LLM client instance.""" if cls._instance is None: settings = get_settings() if settings.ai_provider == "gemini" and settings.gemini_api_key: cls._instance = GeminiClient(settings.gemini_api_key) elif settings.openai_api_key: cls._instance = OpenAIClient(settings.openai_api_key) else: # Return a mock client if no API keys configured cls._instance = MockLLMClient() return cls._instance class MockLLMClient(BaseLLMClient): """Mock client for development without API keys. PIOE 2.0 compatible.""" def classify(self, text: str) -> dict: # Basic rule-based classification text_lower = text.lower() if any(kw in text_lower for kw in ["scholarship", "fellowship", "grant"]): return {"category": "scholarship", "domain": "academia", "confidence": 0.7} elif any(kw in text_lower for kw in ["hackathon", "competition", "challenge"]): return {"category": "hackathon", "domain": "ai", "confidence": 0.7} elif any(kw in text_lower for kw in ["internship", "intern"]): return {"category": "internship", "domain": "mixed", "confidence": 0.7} elif any(kw in text_lower for kw in ["job", "hiring", "position"]): return {"category": "job", "domain": "mixed", "confidence": 0.7} elif any(kw in text_lower for kw in ["bounty", "ecosystem", "solana", "ethereum"]): return {"category": "bounty", "domain": "crypto", "confidence": 0.7} elif any(kw in text_lower for kw in ["pitch", "demo day", "accelerator"]): return {"category": "pitch_event", "domain": "mixed", "confidence": 0.7} elif any(kw in text_lower for kw in ["collaborat", "partner", "looking for"]): return {"category": "collaboration", "domain": "mixed", "confidence": 0.6} return {"category": "other", "domain": "mixed", "confidence": 0.3} def summarize(self, text: str, max_length: int = 150) -> str: return text[:max_length] def recommend_action(self, opportunity: dict) -> dict: """PIOE 2.0 action guidance - rule-based fallback.""" category = opportunity.get("category", "other") # Category-based action mapping action_map = { "hackathon": ("apply_now", "this_week", ["Python", "ML/AI"], ["Previous hackathon project"]), "grant": ("apply_prepared", "this_month", ["Technical writing", "Project planning"], ["Open source contributions"]), "ecosystem_grant": ("apply_prepared", "this_month", ["Solidity/Rust", "Web3"], ["DApp or smart contract"]), "internship": ("apply_now", "this_week", ["Relevant coursework", "Projects"], ["GitHub portfolio"]), "scholarship": ("apply_prepared", "this_month", ["Academic excellence", "Leadership"], ["Research paper or thesis"]), "bounty": ("apply_now", "immediate", ["Specific tech stack"], ["Related code samples"]), "pitch_event": ("apply_prepared", "this_month", ["Presentation", "Business model"], ["Pitch deck", "Demo video"]), "collaboration": ("network_first", "whenever", ["Domain expertise"], ["Relevant projects"]), } action, urgency, skills, portfolio = action_map.get( category, ("save_for_later", "whenever", [], []) ) return { "primary_action": action, "urgency": urgency, "timing_status": "unknown", "skills_to_highlight": skills, "portfolio_pieces": portfolio, "preparation_steps": [ "Review the opportunity requirements", "Prepare relevant materials", "Submit before deadline" ], "networking_tips": "Research the organization and connect with past participants", "differentiation_angle": "Highlight unique projects and Africa/Nigeria perspective", "success_probability": 0.3, "time_investment_hours": 10, "risk_level": "medium", "why": f"Standard approach for {category}", "red_flags": [], } def extract_metadata(self, text: str) -> dict: return {}