diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f1d3cf7 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +# Use official Python 3.11 image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements first for caching +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# Copy backend source code +COPY . . + +# Expose FastAPI port +EXPOSE 8089 + +# Run FastAPI with uvicorn +CMD ["uvicorn", "backend:app", "--host", "0.0.0.0", "--port", "8089", "--reload"] diff --git a/backend/backend.py b/backend/backend.py new file mode 100644 index 0000000..8d04968 --- /dev/null +++ b/backend/backend.py @@ -0,0 +1,164 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from datetime import datetime +import re +import requests + +app = FastAPI(title="PC Budget Assistant API") + +# Allow React frontend to call API +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # or ["http://localhost:5173"] to restrict + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# -------------------------- +# Knowledge base +# -------------------------- +KNOWLEDGE_BASE = { + "budget_ranges": [ + { + "range": "$300-$500", + "cpu": "Intel Pentium Gold or AMD Athlon", + "gpu": "Integrated Graphics", + "ram": "8GB DDR4", + "storage": "256GB SSD", + "use_case": "Basic computing, web browsing, office work" + }, + { + "range": "$600-$900", + "cpu": "Intel Core i3-12100F or AMD Ryzen 5 5600", + "gpu": "GTX 1650 or RX 6500 XT", + "ram": "16GB DDR4", + "storage": "500GB NVMe SSD", + "use_case": "Gaming at 1080p, content creation, multitasking" + }, + { + "range": "$1000-$1500", + "cpu": "Intel Core i5-13400F or AMD Ryzen 5 7600", + "gpu": "RTX 4060 or RX 7600", + "ram": "16GB DDR4/DDR5", + "storage": "1TB NVMe SSD", + "use_case": "High-end gaming, streaming, video editing" + }, + { + "range": "$1500-$2500", + "cpu": "Intel Core i7-14700K or AMD Ryzen 7 7800X3D", + "gpu": "RTX 4070 Super or RX 7800 XT", + "ram": "32GB DDR5", + "storage": "2TB NVMe Gen4 SSD", + "use_case": "4K gaming, professional work, heavy multitasking" + } + ], + "components": { + "cpu": "The processor is the brain of your computer.", + "gpu": "Graphics card handles visuals and gaming.", + "ram": "Memory for running programs.", + "storage": "SSD (Solid State Drive) is faster than HDD.", + "motherboard": "Connects all components. Must match CPU socket.", + "psu": "Power Supply Unit. Bronze/Gold/Platinum ratings show efficiency.", + "case": "Houses all components. Ensure good airflow." + }, + "tips": [ + "Check component compatibility before buying", + "Invest more in GPU if gaming is priority", + "Don't cheap out on PSU - it powers everything", + "SSD for OS is essential", + "Consider future upgradability" + ] +} + +# -------------------------- +# Pydantic models +# -------------------------- +class QueryRequest(BaseModel): + query: str + +class QueryResponse(BaseModel): + query: str + context: str + response: str + timestamp: str + +# -------------------------- +# RAG System +# -------------------------- +class RAGSystem: + def __init__(self): + self.knowledge_base = KNOWLEDGE_BASE + + def retrieve_context(self, query: str) -> str: + """Retrieve context for a query""" + query_lower = query.lower() + context = [] + + # Match budget + budget_match = re.search(r'\$?(\d+)', query_lower) + if budget_match: + budget = int(budget_match.group(1)) + for b in self.knowledge_base["budget_ranges"]: + min_b, max_b = map(int, b["range"].replace("$", "").split("-")) + if min_b <= budget <= max_b: + context.append(f"Budget: {b['range']}\nCPU: {b['cpu']}\nGPU: {b['gpu']}\nRAM: {b['ram']}\nStorage: {b['storage']}\nUse Case: {b['use_case']}") + + # Match components + for comp, desc in self.knowledge_base["components"].items(): + if comp in query_lower: + context.append(f"{comp.upper()}: {desc}") + + # Tips + if any(w in query_lower for w in ["tip", "advice", "recommend", "help"]): + context.append("Tips:\n" + "\n".join(f"- {t}" for t in self.knowledge_base["tips"][:3])) + + if any(w in query_lower for w in ["option", "choice", "range", "budget"]): + for b in self.knowledge_base["budget_ranges"]: + context.append(f"{b['range']}: {b['use_case']}") + + return "\n\n".join(context) if context else "General PC building info available." + + def generate_response(self, query: str, context: str) -> str: + """Simple fallback response for React frontend""" + if "General PC building info available." in context: + return "I can help you build PCs! Ask me about budgets like '$800' or components like 'GPU'." + return f"Based on your question about '{query}':\n{context}" + +# Initialize RAG system +rag_system = RAGSystem() + +# -------------------------- +# API Endpoints +# -------------------------- +@app.get("/") +async def root(): + return {"message": "PC Budget Assistant API", "version": "1.0"} + +@app.post("/query", response_model=QueryResponse) +async def query_pc(request: QueryRequest): + if not request.query.strip(): + raise HTTPException(status_code=400, detail="Query cannot be empty") + + context = rag_system.retrieve_context(request.query) + response = rag_system.generate_response(request.query, context) + + return QueryResponse( + query=request.query, + context=context, + response=response, + timestamp=datetime.now().isoformat() + ) + +@app.get("/budgets") +async def get_budgets(): + return {"budgets": KNOWLEDGE_BASE["budget_ranges"]} + +@app.get("/components") +async def get_components(): + return {"components": KNOWLEDGE_BASE["components"]} + +@app.get("/tips") +async def get_tips(): + return {"tips": KNOWLEDGE_BASE["tips"]} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f3a249e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn +tkinter \ No newline at end of file diff --git a/logic.py b/logic.py.bak similarity index 100% rename from logic.py rename to logic.py.bak diff --git a/main.py b/main.py index acd9b59..b17b3d7 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,97 @@ from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel -from typing import Optional, List, Any -from logic import generate_build_suggestion, generate_chat_response -from dotenv import load_dotenv -import uvicorn +from datetime import datetime +from typing import Optional, List, Dict, Any, Union +import re +import requests import os +import random +import logging -# Load environment variables from .env file -load_dotenv() +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -app = FastAPI(title="AI Service", description="Service for AI features like Builder Bot and Chat", version="1.0.0") +# Product Catalog Service URL +CATALOG_SERVICE_URL = os.getenv("PRODUCT_CATALOGUE_SERVICE_URL", "http://localhost:8082") -class ChatRequest(BaseModel): - message: str +app = FastAPI(title="AI Service", description="Dynamic PC Budget Assistant", version="2.0") + +# Allow React frontend to call API +# app.add_middleware( +# CORSMiddleware, +# allow_origins=["*"], +# allow_credentials=True, +# allow_methods=["*"], +# allow_headers=["*"], +# ) + +# -------------------------- +# Knowledge Base (Static Fallback + Structure) +# -------------------------- +KNOWLEDGE_BASE = { + "budget_ranges": [ + {"range": "$300-$500", "desc": "Basic computing"}, + {"range": "$600-$900", "desc": "Entry-level gaming (1080p)"}, + {"range": "$1000-$1500", "desc": "Mid-tier gaming (1440p)"}, + {"range": "$1500-$2500", "desc": "High-end gaming (4K)"} + ], + "components": { + "cpu": "The processor is the brain of your computer.", + "gpu": "Graphics card handles visuals and gaming.", + "motherboard": "Connects all components.", + "ram": "Memory for running programs.", + "storage": "SSD/HDD for storing files.", + "psu": "Power Supply Unit.", + "case": "Computer case." + }, + "tips": [ + "Always check socket compatibility between CPU and Motherboard.", + "Don't cheap out on the PSU.", + "SSDs are essential for a responsive system." + ] +} -class ChatResponse(BaseModel): +# -------------------------- +# Dynamic Data Fetching +# -------------------------- +def fetch_products_from_catalog() -> List[Dict[str, Any]]: + """Fetch products from the Product Catalog Service.""" + try: + response = requests.get(f"{CATALOG_SERVICE_URL}/api/products", params={"size": 100}, timeout=5) + if response.status_code == 200: + data = response.json() + if isinstance(data, dict) and "content" in data: + return data["content"] + elif isinstance(data, list): + return data + return [] + except Exception as e: + logger.error(f"Error fetching products: {e}") + return get_mock_products() + +def get_mock_products() -> List[Dict[str, Any]]: + """Fallback mock product data.""" + return [ + {"id": "1", "name": "Intel Core i9-13900K", "subcategory": "CPU", "price": 589.99, "brand": "Intel", "specs": {"socketType": "LGA1700"}}, + {"id": "2", "name": "NVIDIA GeForce RTX 4090", "subcategory": "GPU", "price": 1599.99, "brand": "NVIDIA", "specs": {"powerRequirement": "450"}}, + {"id": "3", "name": "Corsair Vengeance 32GB", "subcategory": "RAM", "price": 129.99, "brand": "Corsair", "specs": {"type": "DDR5"}}, + ] + +# -------------------------- +# Models +# -------------------------- +class QueryRequest(BaseModel): + query: str + +class QueryResponse(BaseModel): + query: str + context: str response: str + timestamp: str +# Frontend compatibility models class BuilderBotRequest(BaseModel): query: str @@ -28,20 +103,142 @@ class BuilderBotResponse(BaseModel): message: str buildSuggestion: BuildSuggestion +# -------------------------- +# RAG System (Dynamic) +# -------------------------- +class RAGSystem: + def __init__(self): + self.knowledge_base = KNOWLEDGE_BASE + + def retrieve_context(self, query: str) -> str: + query_lower = query.lower() + context = [] + + # 1. Fetch Dynamic Products + products = fetch_products_from_catalog() + + # Simple keyword matching for products + relevant_products = [] + for p in products: + p_name = p.get('name', '').lower() + p_sub = p.get('subcategory', '').lower() + if p_sub in query_lower or any(word in query_lower for word in p_name.split()): + relevant_products.append(f"{p.get('name')} (${p.get('price')})") + + if relevant_products: + context.append("Available Products:\n" + "\n".join(relevant_products[:5])) + + # 2. Static Knowledge Base + if "budget" in query_lower or re.search(r'\$\d+', query): + context.append("Budget Ranges:\n" + "\n".join([f"{b['range']}: {b['desc']}" for b in self.knowledge_base["budget_ranges"]])) + + for comp, desc in self.knowledge_base["components"].items(): + if comp in query_lower: + context.append(f"{comp.upper()}: {desc}") + + if not context: + context.append("General PC building advice available.") + + return "\n\n".join(context) + + def generate_response(self, query: str, context: str) -> str: + if "Available Products" in context: + return f"Based on your request, here are some options from our catalog:\n{context}\n\nWould you like me to build a PC with these?" + + return f"Here is some information regarding '{query}':\n{context}" + +rag_system = RAGSystem() + +# -------------------------- +# Logic for Builder Bot (Frontend Compat) +# -------------------------- +def generate_build_suggestion_logic(query: str) -> Dict[str, Any]: + # Simplified logic reusing the dynamic fetch + products = fetch_products_from_catalog() + + # Simple budget extraction + budget = 2000 + budget_match = re.search(r'\$?(\d{3,5})', query) + if budget_match: + budget = int(budget_match.group(1)) + + # Basic filtering logic (simplified from original logic.py) + # real logic would be more complex, but this proves the dynamic connection + def get_price(p): return float(p.get('price', 0)) + def get_sub(p): return p.get('subcategory', '') + + components = [] + required_types = ['CPU', 'GPU', 'Motherboard', 'RAM', 'Storage', 'PSU', 'Case'] + + current_total = 0 + for type_ in required_types: + # Find a product of this type consistent with budget + # Very naive heuristic: allocate ~1/7th of budget per component (not realistic but functional) + target_price = budget / 7 + candidates = [p for p in products if type_ in get_sub(p) or (type_ == 'Storage' and 'SSD' in get_sub(p))] + + if candidates: + # Pick one closest to target price + chosen = min(candidates, key=lambda x: abs(get_price(x) - target_price)) + + # Normalize for frontend + components.append({ + "id": str(chosen.get('id')), + "name": chosen.get('name'), + "subcategory": chosen.get('subcategory'), + "price": get_price(chosen), + "brand": chosen.get('brand', ''), + "imageUrl": chosen.get('imageUrl', '/placeholder-product.png'), + "specs": chosen.get('specs', {}) + }) + current_total += get_price(chosen) + + return { + "components": components, + "totalPrice": current_total + } + +# -------------------------- +# API Endpoints +# -------------------------- +@app.get("/api/ai") +def root(): + return {"message": "AI Service Running", "mode": "Dynamic"} + @app.get("/health") def health_check(): return {"status": "healthy"} -@app.post("/chat", response_model=ChatResponse) -def chat(request: ChatRequest): - response_text = generate_chat_response(request.message) - return ChatResponse(response=response_text) +@app.post("/api/ai/query", response_model=QueryResponse) +def query_endpoint(request: QueryRequest): + context = rag_system.retrieve_context(request.query) + response = rag_system.generate_response(request.query, context) + return QueryResponse( + query=request.query, + context=context, + response=response, + timestamp=datetime.now().isoformat() + ) -@app.post("/builder-bot", response_model=BuilderBotResponse) +# Frontend Endpoint +@app.post("/api/ai/builder-bot", response_model=BuilderBotResponse) def builder_bot(request: BuilderBotRequest): - result = generate_build_suggestion(request.query) - return BuilderBotResponse(**result) + # 1. Get Text Response from RAG + context = rag_system.retrieve_context(request.query) + text_response = rag_system.generate_response(request.query, context) + + # 2. Get Build Suggestion (Dynamic) + build_data = generate_build_suggestion_logic(request.query) + + return BuilderBotResponse( + message=text_response, + buildSuggestion=BuildSuggestion( + components=build_data['components'], + totalPrice=build_data['totalPrice'] + ) + ) if __name__ == "__main__": - port = int(os.getenv("PORT", "8089")) - uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True) + import uvicorn + # Clean up port 8089 user before starting? (Handled by user instructions usually, but we can try) + uvicorn.run(app, host="0.0.0.0", port=8089) diff --git a/main.py.bak b/main.py.bak new file mode 100644 index 0000000..acd9b59 --- /dev/null +++ b/main.py.bak @@ -0,0 +1,47 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import Optional, List, Any +from logic import generate_build_suggestion, generate_chat_response +from dotenv import load_dotenv +import uvicorn +import os + +# Load environment variables from .env file +load_dotenv() + +app = FastAPI(title="AI Service", description="Service for AI features like Builder Bot and Chat", version="1.0.0") + +class ChatRequest(BaseModel): + message: str + +class ChatResponse(BaseModel): + response: str + +class BuilderBotRequest(BaseModel): + query: str + +class BuildSuggestion(BaseModel): + components: List[Any] + totalPrice: float + +class BuilderBotResponse(BaseModel): + message: str + buildSuggestion: BuildSuggestion + +@app.get("/health") +def health_check(): + return {"status": "healthy"} + +@app.post("/chat", response_model=ChatResponse) +def chat(request: ChatRequest): + response_text = generate_chat_response(request.message) + return ChatResponse(response=response_text) + +@app.post("/builder-bot", response_model=BuilderBotResponse) +def builder_bot(request: BuilderBotRequest): + result = generate_build_suggestion(request.query) + return BuilderBotResponse(**result) + +if __name__ == "__main__": + port = int(os.getenv("PORT", "8089")) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)