| import json |
| import base64 |
| from typing import List, Optional |
| from fastapi import HTTPException, UploadFile, File, Form |
| from fastapi.responses import StreamingResponse |
| from models import ChatRequest, ChatResponse, WardrobeItem, TextRequest |
| from query_processing import ( |
| extract_clothing_info, extract_colors_from_query, detect_query_type, |
| get_color_matches, is_greeting, is_name_question |
| ) |
| from conversation import ( |
| get_conversation_context, enhance_message_with_context, update_context |
| ) |
| from model_manager import generate_chat_response, generate_chat_response_streaming |
| from wardrobe import handle_wardrobe_chat |
| from rag import retrieve_relevant_context, format_rag_context |
| from config import COLOR_HARMONY |
| from model_manager import style_model |
|
|
| def setup_routes(app): |
| @app.get("/") |
| async def root(): |
| return { |
| "message": "Style GPT API - Milestone 1", |
| "version": "1.0.0", |
| "endpoints": { |
| "/text": "POST - Text-only chat", |
| "/chat": "POST - Chat with optional images", |
| "/chat/upload": "POST - Chat with file upload", |
| "/chat/upload/stream": "POST - Streaming chat with file upload", |
| "/health": "GET - Health check" |
| } |
| } |
|
|
| @app.get("/health") |
| async def health_check(): |
| return { |
| "status": "healthy" if style_model is not None else "loading", |
| "model_loaded": style_model is not None, |
| "model_name": "Qwen/Qwen2.5-VL-7B-Instruct" |
| } |
|
|
| @app.post("/text", response_model=ChatResponse) |
| async def text_only(request: TextRequest): |
| try: |
| message = request.message.strip() |
| session_id = request.session_id |
| |
| if not message: |
| raise HTTPException(status_code=400, detail="Message cannot be empty") |
| |
| conv_context = get_conversation_context(session_id) |
| |
| if is_name_question(message): |
| prompt = "What is your name? Respond naturally and friendly." |
| rag_chunks = retrieve_relevant_context(message, top_k=2) |
| rag_context = format_rag_context(rag_chunks) |
| response_text = generate_chat_response(prompt, max_length=100, temperature=0.8, rag_context=rag_context, images=None) |
| update_context(session_id, message, {"response": response_text}) |
| return ChatResponse(response=response_text, session_id=session_id) |
| |
| if is_greeting(message): |
| prompt = f"{message} Respond warmly and offer to help with fashion advice." |
| rag_chunks = retrieve_relevant_context(message, top_k=2) |
| rag_context = format_rag_context(rag_chunks) |
| response_text = generate_chat_response(prompt, max_length=150, temperature=0.8, rag_context=rag_context, images=None) |
| update_context(session_id, message, {"response": response_text}) |
| return ChatResponse(response=response_text, session_id=session_id) |
| |
| enhanced_message = enhance_message_with_context(message, conv_context["context"]) |
| query_type = detect_query_type(enhanced_message) |
| rag_chunks = retrieve_relevant_context(enhanced_message, top_k=3) |
| rag_context = format_rag_context(rag_chunks) |
| |
| if query_type == "color_compatibility": |
| found_colors = extract_colors_from_query(enhanced_message) |
| |
| if len(found_colors) >= 2: |
| color1_mapped = found_colors[0][1] |
| color2_mapped = found_colors[1][1] |
| color1_original = found_colors[0][0] |
| color2_original = found_colors[1][0] |
| |
| compatible = False |
| if color1_mapped in COLOR_HARMONY: |
| compatible = color2_mapped in COLOR_HARMONY[color1_mapped] |
| elif color2_mapped in COLOR_HARMONY: |
| compatible = color1_mapped in COLOR_HARMONY[color2_mapped] |
| |
| neutrals = ["white", "black", "grey", "gray", "beige", "navy"] |
| if color1_mapped in neutrals or color2_mapped in neutrals: |
| compatible = True |
| |
| if compatible: |
| response_text = f"Yes, {color1_original.title()} will go well with {color2_original.title()}. They create a balanced and stylish combination that works great together!" |
| else: |
| response_text = f"{color1_original.title()} and {color2_original.title()} can work together, though you might want to add some neutral pieces to balance the look." |
| |
| prompt = f"Does {color1_original} go well with {color2_original}? Answer naturally and conversationally." |
| ai_response = generate_chat_response(prompt, max_length=150, temperature=0.8, rag_context=rag_context, images=None) |
| if len(ai_response) > 15: |
| response_text = ai_response |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": color1_original, |
| "colors": [color1_original, color2_original] |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| elif query_type == "color_suggestion": |
| clothing_info = extract_clothing_info(enhanced_message) |
| base_color = clothing_info.get("color") |
| |
| if not base_color: |
| found_colors = extract_colors_from_query(enhanced_message) |
| if found_colors: |
| base_color = found_colors[0][1] |
| elif conv_context["context"].get("last_color"): |
| base_color = conv_context["context"]["last_color"] |
| |
| if not base_color: |
| return ChatResponse( |
| response="I'd love to help you with colors! Could you tell me which color you're working with? For example, 'what colors go with red?'", |
| session_id=session_id |
| ) |
| |
| matching_colors = get_color_matches(base_color) |
| clothing_item = clothing_info.get("existing_item") or clothing_info.get("type") or conv_context["context"].get("last_item", "outfit") |
| |
| suggested_colors = [c.title() for c in matching_colors[:4]] |
| |
| message_lower_for_style = message.lower() |
| style_keywords = [] |
| if "stylish" in message_lower_for_style or "standout" in message_lower_for_style or "stand out" in message_lower_for_style: |
| style_keywords.append("stylish and eye-catching") |
| if "professional" in message_lower_for_style or "formal" in message_lower_for_style: |
| style_keywords.append("professional") |
| if "casual" in message_lower_for_style: |
| style_keywords.append("casual") |
| |
| style_note = "" |
| if style_keywords: |
| style_note = f" The user wants something {', '.join(style_keywords)}." |
| |
| prompt = f"What colors go well with {base_color} {clothing_item}?{style_note} Give me a natural, conversational answer with specific color suggestions." |
| ai_response = generate_chat_response(prompt, max_length=300, temperature=0.8, rag_context=rag_context, images=None) |
| if len(ai_response) > 30: |
| response_text = ai_response |
| else: |
| response_text = f"For your {base_color} {clothing_item}, I'd suggest pairing it with {', '.join(suggested_colors[:3])}, or {suggested_colors[3] if len(suggested_colors) > 3 else 'other neutrals'}. These colors complement each other beautifully!" |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": base_color, |
| "item": clothing_item, |
| "colors": suggested_colors |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| else: |
| clothing_info = extract_clothing_info(enhanced_message) |
| |
| if not clothing_info.get("color") and conv_context["context"].get("last_color"): |
| enhanced_message = f"{enhanced_message} {conv_context['context']['last_color']}" |
| clothing_info = extract_clothing_info(enhanced_message) |
| |
| context_info = "" |
| if clothing_info.get("color"): |
| context_info += f"Color preference: {clothing_info.get('color')}. " |
| if clothing_info.get("type"): |
| context_info += f"Item type: {clothing_info.get('type')}. " |
| if clothing_info.get("existing_item"): |
| context_info += f"User has: {clothing_info.get('existing_item')}. " |
| |
| occasion_keywords = ["defense", "project", "presentation", "meeting", "interview", "formal", "casual", "party", "wedding"] |
| occasion = next((word for word in occasion_keywords if word in enhanced_message.lower()), None) |
| if occasion: |
| context_info += f"Occasion: {occasion}. " |
| |
| prompt = f"{enhanced_message}" |
| if context_info: |
| prompt += f"\n\nContext: {context_info.strip()}" |
| prompt += "\n\nGive helpful, detailed outfit suggestions that are practical and stylish. Be specific about item combinations and explain why they work well." |
| |
| response_text = generate_chat_response(prompt, max_length=1024, temperature=0.8, rag_context=rag_context, images=None) |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": clothing_info.get("color"), |
| "item": clothing_info.get("type") or clothing_info.get("requested_item"), |
| "items": clothing_info.get("items", []) |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Error processing text message: {str(e)}") |
|
|
| @app.post("/chat", response_model=ChatResponse) |
| async def chat(request: ChatRequest): |
| try: |
| message = request.message.strip() |
| session_id = request.session_id |
| |
| if not message: |
| raise HTTPException(status_code=400, detail="Message cannot be empty") |
| |
| if request.wardrobe and len(request.wardrobe) > 0: |
| print(f"[WARDROBE CHAT] ===== WARDROBE REQUEST DETECTED =====") |
| if request.wardrobe_description: |
| print(f"[WARDROBE CHAT] Using provided wardrobe description ({len(request.wardrobe_description)} chars)") |
| return await handle_wardrobe_chat(message, request.wardrobe, session_id, images=request.images, wardrobe_description=request.wardrobe_description) |
| |
| conv_context = get_conversation_context(session_id) |
| |
| if is_name_question(message): |
| prompt = "What is your name? Respond naturally and friendly." |
| rag_chunks = retrieve_relevant_context(message, top_k=2) |
| rag_context = format_rag_context(rag_chunks) |
| response_text = generate_chat_response(prompt, max_length=100, temperature=0.8, rag_context=rag_context, images=request.images) |
| update_context(session_id, message, {"response": response_text}) |
| return ChatResponse(response=response_text, session_id=session_id) |
| |
| if is_greeting(message): |
| prompt = f"{message} Respond warmly and offer to help with fashion advice." |
| rag_chunks = retrieve_relevant_context(message, top_k=2) |
| rag_context = format_rag_context(rag_chunks) |
| response_text = generate_chat_response(prompt, max_length=150, temperature=0.8, rag_context=rag_context, images=request.images) |
| update_context(session_id, message, {"response": response_text}) |
| return ChatResponse(response=response_text, session_id=session_id) |
| |
| enhanced_message = enhance_message_with_context(message, conv_context["context"]) |
| query_type = detect_query_type(enhanced_message) |
| rag_chunks = retrieve_relevant_context(enhanced_message, top_k=3) |
| rag_context = format_rag_context(rag_chunks) |
| |
| if query_type == "color_compatibility": |
| found_colors = extract_colors_from_query(enhanced_message) |
| |
| if len(found_colors) >= 2: |
| color1_mapped = found_colors[0][1] |
| color2_mapped = found_colors[1][1] |
| color1_original = found_colors[0][0] |
| color2_original = found_colors[1][0] |
| |
| compatible = False |
| if color1_mapped in COLOR_HARMONY: |
| compatible = color2_mapped in COLOR_HARMONY[color1_mapped] |
| elif color2_mapped in COLOR_HARMONY: |
| compatible = color1_mapped in COLOR_HARMONY[color2_mapped] |
| |
| neutrals = ["white", "black", "grey", "gray", "beige", "navy"] |
| if color1_mapped in neutrals or color2_mapped in neutrals: |
| compatible = True |
| |
| if compatible: |
| response_text = f"Yes, {color1_original.title()} will go well with {color2_original.title()}. They create a balanced and stylish combination that works great together!" |
| else: |
| response_text = f"{color1_original.title()} and {color2_original.title()} can work together, though you might want to add some neutral pieces to balance the look." |
| |
| prompt = f"Does {color1_original} go well with {color2_original}? Answer naturally and conversationally." |
| ai_response = generate_chat_response(prompt, max_length=150, temperature=0.8, rag_context=rag_context, images=request.images) |
| if len(ai_response) > 15: |
| response_text = ai_response |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": color1_original, |
| "colors": [color1_original, color2_original] |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| elif query_type == "color_suggestion": |
| clothing_info = extract_clothing_info(enhanced_message) |
| base_color = clothing_info.get("color") |
| |
| if not base_color: |
| found_colors = extract_colors_from_query(enhanced_message) |
| if found_colors: |
| base_color = found_colors[0][1] |
| elif conv_context["context"].get("last_color"): |
| base_color = conv_context["context"]["last_color"] |
| |
| if not base_color: |
| return ChatResponse( |
| response="I'd love to help you with colors! Could you tell me which color you're working with? For example, 'what colors go with red?'", |
| session_id=session_id |
| ) |
| |
| matching_colors = get_color_matches(base_color) |
| clothing_item = clothing_info.get("existing_item") or clothing_info.get("type") or conv_context["context"].get("last_item", "outfit") |
| |
| suggested_colors = [c.title() for c in matching_colors[:4]] |
| |
| message_lower_for_style = message.lower() |
| style_keywords = [] |
| if "stylish" in message_lower_for_style or "standout" in message_lower_for_style or "stand out" in message_lower_for_style: |
| style_keywords.append("stylish and eye-catching") |
| if "professional" in message_lower_for_style or "formal" in message_lower_for_style: |
| style_keywords.append("professional") |
| if "casual" in message_lower_for_style: |
| style_keywords.append("casual") |
| |
| style_note = "" |
| if style_keywords: |
| style_note = f" The user wants something {', '.join(style_keywords)}." |
| |
| prompt = f"What colors go well with {base_color} {clothing_item}?{style_note} Give me a natural, conversational answer with specific color suggestions." |
| ai_response = generate_chat_response(prompt, max_length=300, temperature=0.8, rag_context=rag_context, images=request.images) |
| if len(ai_response) > 30: |
| response_text = ai_response |
| else: |
| response_text = f"For your {base_color} {clothing_item}, I'd suggest pairing it with {', '.join(suggested_colors[:3])}, or {suggested_colors[3] if len(suggested_colors) > 3 else 'other neutrals'}. These colors complement each other beautifully!" |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": base_color, |
| "item": clothing_item, |
| "colors": suggested_colors |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| else: |
| clothing_info = extract_clothing_info(enhanced_message) |
| |
| if not clothing_info.get("color") and conv_context["context"].get("last_color"): |
| enhanced_message = f"{enhanced_message} {conv_context['context']['last_color']}" |
| clothing_info = extract_clothing_info(enhanced_message) |
| |
| context_info = "" |
| if clothing_info.get("color"): |
| context_info += f"Color preference: {clothing_info.get('color')}. " |
| if clothing_info.get("type"): |
| context_info += f"Item type: {clothing_info.get('type')}. " |
| if clothing_info.get("existing_item"): |
| context_info += f"User has: {clothing_info.get('existing_item')}. " |
| |
| occasion_keywords = ["defense", "project", "presentation", "meeting", "interview", "formal", "casual", "party", "wedding"] |
| occasion = next((word for word in occasion_keywords if word in enhanced_message.lower()), None) |
| if occasion: |
| context_info += f"Occasion: {occasion}. " |
| |
| prompt = f"{enhanced_message}" |
| if context_info: |
| prompt += f"\n\nContext: {context_info.strip()}" |
| prompt += "\n\nGive helpful, detailed outfit suggestions that are practical and stylish. Be specific about item combinations and explain why they work well." |
| |
| response_text = generate_chat_response(prompt, max_length=1024, temperature=0.8, rag_context=rag_context, images=request.images) |
| |
| update_context(session_id, message, { |
| "response": response_text, |
| "color": clothing_info.get("color"), |
| "item": clothing_info.get("type") or clothing_info.get("requested_item"), |
| "items": clothing_info.get("items", []) |
| }) |
| |
| return ChatResponse( |
| response=response_text, |
| session_id=session_id |
| ) |
| |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Error processing chat message: {str(e)}") |
|
|
| @app.post("/chat/upload", response_model=ChatResponse) |
| async def chat_with_upload( |
| message: str = Form(...), |
| session_id: str = Form(default="default"), |
| wardrobe: Optional[str] = Form(default=None), |
| wardrobe_description: Optional[str] = Form(default=None), |
| images: List[UploadFile] = File(default=[]) |
| ): |
| try: |
| wardrobe_items = [] |
| if wardrobe and wardrobe.strip() and wardrobe.strip() not in ["[]", "", "string"]: |
| try: |
| wardrobe_data = json.loads(wardrobe) |
| if isinstance(wardrobe_data, list): |
| wardrobe_items = [WardrobeItem(**item) for item in wardrobe_data] |
| except json.JSONDecodeError: |
| print(f"[UPLOAD] Ignoring invalid wardrobe value: {wardrobe[:50]}") |
| |
| image_data_urls = [] |
| for img_file in images: |
| if img_file.filename: |
| content = await img_file.read() |
| content_type = img_file.content_type or "image/jpeg" |
| base64_data = base64.b64encode(content).decode("utf-8") |
| data_url = f"data:{content_type};base64,{base64_data}" |
| image_data_urls.append(data_url) |
| print(f"[UPLOAD] Processed image: {img_file.filename} ({len(content)} bytes)") |
| |
| request = ChatRequest( |
| message=message, |
| session_id=session_id, |
| wardrobe=wardrobe_items if wardrobe_items else None, |
| wardrobe_description=wardrobe_description if wardrobe_description and wardrobe_description.strip() else None, |
| images=image_data_urls if image_data_urls else None |
| ) |
| |
| print(f"[UPLOAD] Processing chat request: message='{message[:50]}...', images={len(image_data_urls)}, wardrobe={len(wardrobe_items)}") |
| result = await chat(request) |
| print(f"[UPLOAD] Response generated: {len(result.response)} chars") |
| return result |
| |
| except Exception as e: |
| print(f"[UPLOAD] Error: {e}") |
| raise HTTPException(status_code=500, detail=f"Error processing upload: {str(e)}") |
|
|
| @app.post("/chat/upload/stream") |
| async def chat_with_upload_stream( |
| message: str = Form(...), |
| session_id: str = Form(default="default"), |
| wardrobe: Optional[str] = Form(default=None), |
| wardrobe_description: Optional[str] = Form(default=None), |
| images: List[UploadFile] = File(default=[]) |
| ): |
| image_data_urls = [] |
| for img_file in images: |
| if img_file.filename: |
| content = await img_file.read() |
| content_type = img_file.content_type or "image/jpeg" |
| base64_data = base64.b64encode(content).decode("utf-8") |
| data_url = f"data:{content_type};base64,{base64_data}" |
| image_data_urls.append(data_url) |
| print(f"[STREAM UPLOAD] Processed image: {img_file.filename} ({len(content)} bytes)") |
| |
| wardrobe_items = [] |
| if wardrobe and wardrobe.strip() and wardrobe.strip() not in ["[]", "", "string"]: |
| try: |
| wardrobe_data = json.loads(wardrobe) |
| if isinstance(wardrobe_data, list): |
| wardrobe_items = [WardrobeItem(**item) for item in wardrobe_data] |
| except json.JSONDecodeError: |
| print(f"[STREAM UPLOAD] Ignoring invalid wardrobe value: {wardrobe[:50]}") |
| |
| rag_chunks = retrieve_relevant_context(message, top_k=3) |
| rag_context = format_rag_context(rag_chunks) |
| |
| wardrobe_context = "" |
| if wardrobe_description and wardrobe_description.strip(): |
| wardrobe_context = wardrobe_description |
| print(f"[STREAM UPLOAD] Using provided wardrobe description ({len(wardrobe_context)} chars)") |
| elif wardrobe_items: |
| from wardrobe import format_wardrobe_for_prompt |
| wardrobe_context = format_wardrobe_for_prompt(wardrobe_items) |
| print(f"[STREAM UPLOAD] Generated wardrobe context ({len(wardrobe_context)} chars)") |
| |
| if wardrobe_context: |
| prompt = f"""{wardrobe_context} |
| |
| User request: {message} |
| |
| Suggest a complete outfit using ONLY the items listed above. Reference items by their exact names. Include accessories if available. Be friendly and conversational.""" |
| else: |
| prompt = message |
| |
| print(f"[STREAM UPLOAD] Starting streaming response for: {message[:50]}...") |
| |
| async def generate(): |
| yield f"data: {json.dumps({'type': 'start', 'session_id': session_id})}\n\n" |
| |
| full_response = "" |
| async for chunk in generate_chat_response_streaming( |
| prompt=prompt, |
| max_length=512, |
| temperature=0.7, |
| rag_context=rag_context, |
| images=image_data_urls if image_data_urls else None |
| ): |
| full_response += chunk |
| yield f"data: {json.dumps({'type': 'chunk', 'content': chunk})}\n\n" |
| |
| yield f"data: {json.dumps({'type': 'end', 'full_response': full_response, 'session_id': session_id})}\n\n" |
| print(f"[STREAM UPLOAD] Streaming complete: {len(full_response)} chars") |
| |
| return StreamingResponse( |
| generate(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| "X-Accel-Buffering": "no", |
| } |
| ) |
|
|
|
|