AhmedShahan/portfolio_knowledge_base
0
1import os2from datetime import datetime, timezone3from dotenv import load_dotenv4from fastapi import FastAPI,HTTPException5from pydantic import BaseModel6from sentence_transformers import SentenceTransformer7from qdrant_client import QdrantClient8from groq import Groq9from groq import RateLimitError10from tavily import TavilyClient11 12import sheet_writer13 14load_dotenv()15 16# ── Config ────────────────────────────────────────────────────────────────────17COLLECTION_NAME = "portfolio"18TOP_K = 519MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5"20GROQ_MODEL = "llama-3.3-70b-versatile"21AI_NEWS_COUNT = 1022 23# ── Init ──────────────────────────────────────────────────────────────────────24app = FastAPI()25 26print("🔧 Loading embedding model...")27embedder = SentenceTransformer(MODEL_NAME, trust_remote_code=True)28 29qdrant = QdrantClient(30 url=os.getenv("QDRANT_URL"),31 api_key=os.getenv("QDRANT_API_KEY"),32)33 34groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))35tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))36 37# ── Schema ────────────────────────────────────────────────────────────────────38class QueryRequest(BaseModel):39 question: str40 41# ── Routes ───────────────────────────────────────────────────────────────────42@app.get("/")43def root():44 return {"status": "ok", "message": "Portfolio RAG API is running"}45 46@app.post("/query")47def query(request: QueryRequest):48 # Step 1: Embed query49 vector = embedder.encode(request.question, normalize_embeddings=True).tolist()50 51 # Step 2: Search Qdrant52 results = qdrant.query_points(53 collection_name=COLLECTION_NAME,54 query=vector,55 limit=TOP_K,56 ).points57 58 # Step 3: Build context59 context = "\n\n---\n\n".join([r.payload.get("text", "") for r in results])60 61 # Step 4: Ask Groq62 prompt = f"""You are an AI assistant for Ahmed Shahan's portfolio.63Answer the user's question using ONLY the context below.64If the answer is not in the context, say "I don't have that information."65 66Context:67{context}68 69Question: {request.question}70"""71 72 try:73 response = groq_client.chat.completions.create(74 model=GROQ_MODEL,75 messages=[{"role": "user", "content": prompt}],76 temperature=0.2,77 )78 return {79 "question": request.question,80 "answer": response.choices[0].message.content,81 }82 83 except RateLimitError as e:84 # Extract retry time from error message85 import re86 match = re.search(r"try again in (.+?)\.", str(e))87 retry_after = match.group(1) if match else "some time"88 raise HTTPException(89 status_code=429,90 detail=f"Daily chat limit hit. Please try again after {retry_after}."91 )92 93 94# ── AI News Endpoint ──────────────────────────────────────────────────────────95@app.get("/ai-news")96def get_ai_news():97 """Fetch today's top AI news, ranked by popularity, with simple explanations."""98 try:99 # Step 1: Fetch AI news from Tavily100 search_result = tavily_client.search(101 query="artificial intelligence AI news today",102 topic="news",103 time_range="day",104 max_results=AI_NEWS_COUNT,105 )106 articles = search_result.get("results", [])107 if not articles:108 return {"articles": [], "message": "No AI news found today."}109 110 # Step 2: Sort by popularity (score descending)111 articles.sort(key=lambda a: a.get("score", 0), reverse=True)112 113 # Step 3: Build a single prompt for Groq to simplify all articles114 news_block = "\n\n".join(115 f"--- Article {i+1} ---\n"116 f"Title: {a['title']}\n"117 f"Content: {a.get('content', 'No summary available.')}"118 for i, a in enumerate(articles)119 )120 121 simplify_prompt = f"""You are a helpful assistant that explains AI news in simple, easy-to-understand language for a general audience.122 123Below are {len(articles)} AI news articles from today. For EACH article, provide:1241. The original title1252. A point-by-point explanation using VERY SIMPLE words that anyone can understand (like you're explaining to your grandmother)126 127Format your response EXACTLY like this (separate each article with a blank line):128 129===== Article 1 =====130Title: <original title>131Key points in simple words:132• <point 1>133• <point 2>134• <point 3>135 136===== Article 2 =====137Title: <original title>138Key points in simple words:139• <point 1>140• <point 2>141 142...and so on for all articles.143 144Rules:145- Use ONLY simple, everyday words. No jargon.146- Each article should have 2-4 bullet points.147- Keep each bullet point short (1-2 sentences max).148- Focus on WHAT happened and WHY it matters.149 150Here are the articles:151 152{news_block}153"""154 155 response = groq_client.chat.completions.create(156 model=GROQ_MODEL,157 messages=[{"role": "user", "content": simplify_prompt}],158 temperature=0.3,159 )160 simplified_text = response.choices[0].message.content161 162 # Step 4: Parse the response back into structured data163 # Each article block starts with ===== Article N =====164 import re165 blocks = re.split(r"=====\s*Article\s+\d+\s*=====", simplified_text)166 blocks = [b.strip() for b in blocks if b.strip()]167 168 retrieved_at = datetime.now(timezone.utc).isoformat()169 170 result_articles = []171 for i, article in enumerate(articles):172 explanation = blocks[i] if i < len(blocks) else "Explanation not available."173 # Clean up the explanation - remove the "Title: ..." line since we have it separately174 explanation_lines = explanation.split("\n")175 # Filter out the "Title:" line if present176 clean_lines = [177 line for line in explanation_lines178 if not line.strip().lower().startswith("title:")179 ]180 clean_explanation = "\n".join(clean_lines).strip()181 182 result_articles.append({183 "title": article["title"],184 "url": article["url"],185 "published_date": article.get("published_date", ""),186 "popularity_score": round(article.get("score", 0), 4),187 "explanation_simple": clean_explanation,188 })189 190 # Write to Google Sheet (non-blocking — failures are logged, never crash the API)191 sheet_writer.append_ai_news_articles(retrieved_at, result_articles)192 193 return {194 "data_retrieved_at": retrieved_at,195 "timezone": "UTC",196 "articles": result_articles,197 "total": len(result_articles),198 }199 200 except Exception as e:201 raise HTTPException(status_code=500, detail=f"Failed to fetch AI news: {str(e)}")202 203 204if __name__ == "__main__":205 import uvicorn206 uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)