| """ |
| FastAPI Backend for Giuseppe Rumore's Portfolio CV Assistant |
| Optimized for Hugging Face Spaces deployment |
| """ |
|
|
| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import JSONResponse |
| from pydantic import BaseModel |
| import json |
| import os |
| import torch |
| import psutil |
| import numpy as np |
| from pathlib import Path |
| from datetime import datetime, timezone |
| from contextlib import asynccontextmanager |
| import re |
| from typing import List, Dict, Any, Optional |
|
|
| |
| try: |
| from sentence_transformers import SentenceTransformer |
| SENTENCE_TRANSFORMERS_AVAILABLE = True |
| print("Sentence transformers imported successfully") |
| except ImportError as e: |
| print(f"Failed to import sentence_transformers: {e}") |
| SentenceTransformer = None |
| SENTENCE_TRANSFORMERS_AVAILABLE = False |
|
|
| try: |
| from openai import OpenAI |
| OPENAI_AVAILABLE = True |
| print("OpenAI imported successfully") |
| except ImportError as e: |
| print(f"Failed to import OpenAI: {e}") |
| OpenAI = None |
| OPENAI_AVAILABLE = False |
|
|
| |
| from config import get_settings |
| from middleware import RateLimitMiddleware, create_error_response |
| from cv_content_extractor import CVContentExtractor |
|
|
| class SemanticSearchService: |
| """Service for semantic search using sentence embeddings""" |
| |
| def __init__(self, embedding_model): |
| self.embedding_model = embedding_model |
| self.content_embeddings = None |
| self.content_chunks = None |
| self.is_initialized = False |
| |
| def load_cv_content(self): |
| """Load and prepare CV content for semantic search""" |
| try: |
| |
| ai_content_path = Path(__file__).parent / "data" / "cv_ai_content.json" |
| |
| cv_data = None |
| if ai_content_path.exists(): |
| with open(ai_content_path, 'r', encoding='utf-8') as f: |
| cv_data = json.load(f) |
| source = "ai_content" |
| else: |
| raise FileNotFoundError("No CV content files found") |
| |
| |
| self.content_chunks = [] |
| |
| |
| for section, content in cv_data.items(): |
| if isinstance(content, str) and content.strip(): |
| self.content_chunks.append({ |
| "content": content, |
| "section": section, |
| "source": "ai_content" |
| }) |
| |
| |
| if self.embedding_model and self.content_chunks: |
| texts = [chunk["content"] for chunk in self.content_chunks] |
| self.content_embeddings = self.embedding_model.encode(texts, convert_to_numpy=True) |
| self.is_initialized = True |
| print(f"Loaded {len(self.content_chunks)} content chunks for semantic search") |
| |
| except Exception as e: |
| print(f"Error loading CV content for semantic search: {e}") |
| self.is_initialized = False |
| |
| def search(self, query: str, max_results: int = 5, min_confidence: float = 0.25): |
| """Perform semantic search on CV content""" |
| if not self.is_initialized or not self.embedding_model: |
| raise ValueError("Semantic search service not initialized") |
| |
| |
| expanded_query = self._preprocess_query(query) |
| if expanded_query != query: |
| print(f"Expanded query from '{query}' to '{expanded_query}'") |
| |
| |
| query_embedding = self.embedding_model.encode([expanded_query], convert_to_numpy=True) |
| |
| |
| similarities = np.dot(self.content_embeddings, query_embedding.T).flatten() |
| |
| |
| results = [] |
| for i, similarity in enumerate(similarities): |
| if similarity >= min_confidence: |
| results.append({ |
| "content": self.content_chunks[i]["content"], |
| "section": self.content_chunks[i]["section"], |
| "source": self.content_chunks[i]["source"], |
| "confidence": float(similarity) |
| }) |
| |
| |
| results.sort(key=lambda x: x["confidence"], reverse=True) |
| return results[:max_results] |
| |
| def _preprocess_query(self, query: str) -> str: |
| """ |
| Preprocess query to expand company names and improve semantic matching. |
| """ |
| |
| company_mappings = { |
| "alten": "ALTEN GmbH Engineering Consultant Cologne Ford suppliers automotive component", |
| "imi": "IMI Climate Control Mechanical Engineer Basel Switzerland components HVAC", |
| "steltix": "Steltix ERP Consultant Berlin Germany software integration implementation", |
| "european patent office": "European Patent Office Munich patent management analysis machine tools plastic welding" |
| } |
| |
| |
| skill_mappings = { |
| "computer vision": "image processing visual recognition computer vision object detection", |
| "machine learning": "ML artificial intelligence data science predictive analytics", |
| "deep learning": "neural networks AI machine learning data science", |
| "programming": "coding development software programming languages", |
| "frameworks": "tools libraries frameworks software development", |
| "languages": "programming languages coding development" |
| } |
| |
| |
| language_mappings = { |
| "spoken languages": "native speaker fluent advanced proficiency language skills", |
| "languages speaks": "native speaker fluent advanced proficiency language skills", |
| "what languages": "native speaker fluent advanced proficiency language skills" |
| } |
| |
| query_lower = query.lower() |
| expanded_parts = [query] |
| |
| |
| for phrase, expansion in language_mappings.items(): |
| if phrase in query_lower: |
| expanded_parts.append(expansion) |
| break |
| |
| |
| if len(expanded_parts) == 1: |
| for keyword, expansion in skill_mappings.items(): |
| if keyword in query_lower: |
| expanded_parts.append(expansion) |
| |
| |
| for keyword, expansion in company_mappings.items(): |
| if keyword in query_lower: |
| expanded_parts.append(expansion) |
| |
| return " ".join(expanded_parts) |
|
|
| class ConversationalAIService: |
| """Service for generating natural language responses using retrieved content""" |
| |
| def __init__(self): |
| self.openai_client = None |
| self.is_initialized = False |
| self.settings = get_settings() |
| |
| def load_model(self): |
| """Load conversational AI model""" |
| try: |
| |
| if self.settings.use_openai and self.settings.openai_api_key and OPENAI_AVAILABLE: |
| |
| self.openai_client = OpenAI( |
| api_key=self.settings.openai_api_key, |
| timeout=30.0 |
| ) |
| self.is_initialized = True |
| print(f"OpenAI client initialized with model: {self.settings.openai_model}") |
| return True |
| else: |
| print("OpenAI configuration not available") |
| return False |
| |
| except Exception as e: |
| print(f"Error loading conversational AI model: {e}") |
| self.is_initialized = False |
| return False |
| |
| def generate_response(self, user_question: str, context_chunks: List[Dict], session_history: List[Dict] = None) -> Dict: |
| """Generate a conversational response based on retrieved context""" |
| if not self.is_initialized: |
| return { |
| "text": "I'm sorry, but I'm currently unable to process your question. Please try again later.", |
| "confidence": 0.0, |
| "sources": [], |
| "response_type": "error" |
| } |
| |
| try: |
| |
| context_text = self._build_context_from_chunks(context_chunks) |
| |
| |
| extracted_response = self._extract_factual_response(user_question, context_text, context_chunks) |
| if extracted_response: |
| return extracted_response |
| |
| |
| if self.openai_client: |
| return self._generate_openai_response(user_question, context_text, context_chunks) |
| else: |
| return self._generate_fallback_response(user_question, context_chunks) |
| |
| except Exception as e: |
| print(f"Error generating response: {e}") |
| return { |
| "text": "I'm sorry, but I encountered an error while processing your question.", |
| "confidence": 0.0, |
| "sources": [], |
| "response_type": "error" |
| } |
| |
| def _extract_factual_response(self, question: str, context: str, context_chunks: List[Dict]) -> Dict: |
| """Extract factual information directly from context without AI generation""" |
| question_lower = question.lower() |
| |
| |
| if any(keyword in question_lower for keyword in ['what languages', 'languages speak', 'spoken languages', 'languages does']): |
| |
| for chunk in context_chunks: |
| if 'language_' in chunk['section']: |
| languages = [] |
| for lang_chunk in context_chunks: |
| if 'language_' in lang_chunk['section']: |
| languages.append(lang_chunk['content']) |
| if languages: |
| lang_text = ', '.join([lang.replace('Language: ', '') for lang in languages]) |
| return { |
| "text": f"Giuseppe speaks: {lang_text}", |
| "confidence": 0.95, |
| "sources": [chunk["section"] for chunk in context_chunks if 'language_' in chunk['section']], |
| "response_type": "high_confidence" |
| } |
| |
| |
| return None |
| |
| def _generate_openai_response(self, user_question: str, context_text: str, context_chunks: List[Dict]) -> Dict: |
| """Generate response using OpenAI API""" |
| try: |
| |
| system_prompt = """You are Giuseppe Rumore's professional AI assistant, helping recruiters and potential employers understand his capabilities and expertise. |
| |
| Your role: |
| - Present Giuseppe's qualifications in the most compelling and professional way |
| - Focus on his strengths, achievements, and what he CAN do for potential employers |
| - Highlight relevant experience, skills, and projects that match the inquiry |
| - Be enthusiastic about his capabilities while staying factual |
| - Use a confident, positive tone that positions Giuseppe as a strong candidate |
| |
| Response guidelines: |
| - Lead with Giuseppe's strengths and relevant experience |
| - Use specific examples from his CV to demonstrate capabilities |
| - Quantify achievements when available (percentages, improvements, results) |
| - Frame information positively - focus on what he brings to the table |
| - Use professional, confident language that showcases his value proposition |
| - Keep responses concise but comprehensive (under 200 words) |
| - Structure information clearly with bullet points for easy scanning |
| - Always conclude with Giuseppe's potential value or next steps for the recruiter |
| |
| Avoid: |
| - Mentioning what's NOT in the CV or what he lacks |
| - Negative framing or limitations |
| - Uncertain language ("might", "possibly", "not sure") |
| - Technical jargon without context for business value""" |
| |
| |
| if not context_text or context_text.strip() == "": |
| context_text = """Giuseppe Rumore is an accomplished Data Scientist and ML Engineer with extensive experience in developing and optimizing models for industrial applications. |
| |
| Key Strengths & Capabilities: |
| • Expertise: Machine Learning, Deep Learning, Data Analytics with proven industrial applications |
| • Technical Proficiency: Python, TensorFlow, PyTorch with hands-on project experience |
| • Educational Foundation: MSc Mechanical Engineering with R&D specialization from INSA Lyon |
| • Industry Experience: Led data-driven improvements at IMI Climate Control (30% defect detection improvement, 20% testing time reduction) |
| • Project Portfolio: Advanced ML implementations including computer vision, NLP, and automation |
| • Multilingual: 6 languages including native Italian/Albanian, C1 English/Spanish/French, B2 German |
| • Current Focus: Advancing Data Science and MLOps expertise at Université Paris 1 Panthéon-Sorbonne |
| • Location: Based in Berlin, Germany with European work authorization""" |
| |
| user_prompt = f"""Giuseppe's Professional Background: |
| {context_text} |
| |
| Recruiter Question: {user_question} |
| |
| Please provide a compelling response that showcases Giuseppe's relevant capabilities and experience. Focus on what he brings to the table and how his background aligns with what the recruiter is looking for. Present him as a strong candidate while being factual and specific.""" |
| |
| response = self.openai_client.chat.completions.create( |
| model=self.settings.openai_model, |
| messages=[ |
| {"role": "system", "content": system_prompt}, |
| {"role": "user", "content": user_prompt} |
| ] |
| ) |
| |
| generated_text = response.choices[0].message.content.strip() |
| |
| |
| formatted_text = self._format_response_text(generated_text) |
| |
| |
| confidence = 0.9 if context_chunks else 0.7 |
| |
| return { |
| "text": formatted_text, |
| "confidence": confidence, |
| "sources": [chunk["section"] for chunk in context_chunks[:3]] if context_chunks else ["general_info"], |
| "response_type": "openai_generated" if context_chunks else "openai_fallback" |
| } |
| |
| except Exception as e: |
| print(f"OpenAI API error: {e}") |
| return self._generate_fallback_response(user_question, context_chunks) |
| |
| def _format_response_text(self, text: str) -> str: |
| """Format response text for better readability""" |
| |
| text = text.replace('- ', '\n- ') |
| text = text.replace('• ', '\n• ') |
| |
| |
| text = re.sub(r'(\d+\.\s)', r'\n\1', text) |
| |
| |
| text = re.sub(r'(Key points?:)', r'\n\1', text, flags=re.IGNORECASE) |
| text = re.sub(r'(Experience:)', r'\n\1', text, flags=re.IGNORECASE) |
| text = re.sub(r'(Skills?:)', r'\n\1', text, flags=re.IGNORECASE) |
| text = re.sub(r'(Projects?:)', r'\n\1', text, flags=re.IGNORECASE) |
| |
| |
| text = re.sub(r'\n\s*\n+', '\n\n', text) |
| |
| |
| text = text.lstrip('\n') |
| |
| return text |
| |
| def _build_context_from_chunks(self, context_chunks: List[Dict]) -> str: |
| """Build context string from retrieved content chunks""" |
| if not context_chunks: |
| return "" |
| |
| context_parts = [] |
| for chunk in context_chunks[:3]: |
| context_parts.append(f"{chunk['section']}: {chunk['content']}") |
| |
| return "\n\n".join(context_parts) |
| |
| def _generate_fallback_response(self, user_question: str, context_chunks: List[Dict]) -> Dict: |
| """Generate fallback response when OpenAI is not available""" |
| if not context_chunks: |
| return { |
| "text": "I'm Giuseppe's AI assistant. I can help you learn about his background in Data Science, Machine Learning, and his experience in industrial applications. Please ask about his skills, projects, or work experience!", |
| "confidence": 0.5, |
| "sources": ["general_info"], |
| "response_type": "fallback" |
| } |
| |
| |
| best_chunk = context_chunks[0] |
| |
| template_response = f"Based on Giuseppe's CV: {best_chunk['content'][:300]}{'...' if len(best_chunk['content']) > 300 else ''}" |
| |
| return { |
| "text": template_response, |
| "confidence": 0.6, |
| "sources": [best_chunk["section"]], |
| "response_type": "template" |
| } |
|
|
| |
| embedding_model = None |
| semantic_search_service = None |
| conversational_ai_service = None |
| settings = get_settings() |
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Initialize models on startup""" |
| global embedding_model, semantic_search_service, conversational_ai_service |
| try: |
| |
| if SENTENCE_TRANSFORMERS_AVAILABLE and SentenceTransformer: |
| print(f"Loading sentence transformer model: {settings.hf_model_name}") |
| try: |
| embedding_model = SentenceTransformer(settings.hf_model_name) |
| print(f"Embedding model loaded successfully: {settings.hf_model_name}") |
| |
| |
| semantic_search_service = SemanticSearchService(embedding_model) |
| |
| semantic_search_service.load_cv_content() |
| |
| if semantic_search_service.is_initialized: |
| print(f"Semantic search service initialized with {len(semantic_search_service.content_chunks)} chunks") |
| else: |
| print("Semantic search service failed to initialize") |
| |
| except Exception as e: |
| print(f"Error loading sentence transformer model: {e}") |
| embedding_model = None |
| semantic_search_service = None |
| else: |
| print("Sentence transformers not available - running without embedding model") |
| embedding_model = None |
| semantic_search_service = None |
| |
| |
| if OPENAI_AVAILABLE: |
| conversational_ai_service = ConversationalAIService() |
| if conversational_ai_service.load_model(): |
| print("Conversational AI service initialized successfully") |
| else: |
| print("Conversational AI service initialization failed") |
| else: |
| print("OpenAI not available - running without conversational AI") |
| conversational_ai_service = None |
| |
| except Exception as e: |
| print(f"Error during startup: {e}") |
| print("Continuing without AI models") |
| embedding_model = None |
| semantic_search_service = None |
| conversational_ai_service = None |
| yield |
| |
|
|
| app = FastAPI( |
| title=settings.api_title, |
| description=settings.api_description, |
| version=settings.api_version, |
| lifespan=lifespan |
| ) |
|
|
| |
| app.add_middleware( |
| RateLimitMiddleware, |
| requests_per_window=settings.rate_limit_requests, |
| window_seconds=settings.rate_limit_window |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| class HealthResponse(BaseModel): |
| status: str |
| timestamp: str |
| version: str |
| environment: str |
| system_info: dict |
| models_loaded: bool |
|
|
| class ChatMessage(BaseModel): |
| role: str |
| content: str |
| timestamp: Optional[str] = None |
|
|
| class ChatRequest(BaseModel): |
| message: str |
| session_id: Optional[str] = None |
| history: List[ChatMessage] = [] |
| max_context_chunks: int = 5 |
| min_confidence: float = 0.3 |
|
|
| class ChatResponse(BaseModel): |
| status: str |
| response: str |
| confidence: float |
| sources: List[str] |
| response_type: str |
| session_id: str |
| timestamp: str |
|
|
| @app.exception_handler(404) |
| async def not_found_handler(request, exc): |
| """Custom 404 handler with standardized error format""" |
| return create_error_response( |
| error_type="not_found", |
| message=f"Endpoint {request.url.path} not found", |
| status_code=404, |
| details={"available_endpoints": ["/", "/health", "/docs"]} |
| ) |
|
|
| @app.exception_handler(500) |
| async def internal_error_handler(request, exc): |
| """Custom 500 handler with standardized error format""" |
| return create_error_response( |
| error_type="internal_server_error", |
| message="An internal server error occurred", |
| status_code=500, |
| details={"contact": "Check logs for details"} if settings.debug_mode else {} |
| ) |
|
|
| @app.get("/health", response_model=HealthResponse) |
| async def health_check(): |
| """Health check endpoint with system status""" |
| try: |
| |
| memory = psutil.virtual_memory() |
| disk = psutil.disk_usage('/') |
| |
| system_info = { |
| "cpu_percent": psutil.cpu_percent(interval=1), |
| "memory_percent": memory.percent, |
| "memory_available_gb": round(memory.available / (1024**3), 2), |
| "disk_percent": disk.percent, |
| "disk_free_gb": round(disk.free / (1024**3), 2), |
| "python_version": os.sys.version.split()[0], |
| "torch_version": torch.__version__, |
| "cuda_available": torch.cuda.is_available(), |
| "sentence_transformers_available": SENTENCE_TRANSFORMERS_AVAILABLE, |
| "semantic_search_initialized": semantic_search_service.is_initialized if semantic_search_service else False, |
| "conversational_ai_initialized": conversational_ai_service.is_initialized if conversational_ai_service else False |
| } |
| |
| models_loaded = embedding_model is not None |
| |
| return HealthResponse( |
| status="healthy", |
| timestamp=datetime.now(timezone.utc).isoformat() + "Z", |
| version=settings.api_version, |
| environment=settings.environment, |
| system_info=system_info, |
| models_loaded=models_loaded |
| ) |
| except Exception as e: |
| error_message = f"Health check failed: {str(e)}" |
| if settings.debug_mode: |
| raise HTTPException(status_code=500, detail=error_message) |
| else: |
| return create_error_response( |
| error_type="health_check_failed", |
| message="Health check encountered an error", |
| status_code=500 |
| ) |
|
|
| @app.get("/") |
| async def root(): |
| """Root endpoint with API information""" |
| return { |
| "message": f"{settings.api_title} API", |
| "version": settings.api_version, |
| "environment": settings.environment, |
| "docs": "/docs", |
| "health": "/health", |
| "deployed_on": "Hugging Face Spaces", |
| "timestamp": datetime.now(timezone.utc).isoformat() + "Z" |
| } |
|
|
| @app.post("/api/chat", response_model=ChatResponse) |
| async def chat(request: ChatRequest): |
| """Main chat endpoint combining semantic search and conversational AI""" |
| try: |
| |
| session_id = request.session_id or f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}" |
| |
| |
| if not semantic_search_service or not semantic_search_service.is_initialized: |
| |
| fallback_response = "I'm Giuseppe's AI assistant! I can help you learn about his background in Data Science, Machine Learning, and engineering. Please ask about his skills, projects, or work experience." |
| |
| return ChatResponse( |
| status="success", |
| response=fallback_response, |
| confidence=0.5, |
| sources=["fallback"], |
| response_type="fallback", |
| session_id=session_id, |
| timestamp=datetime.now(timezone.utc).isoformat() + "Z" |
| ) |
| |
| |
| search_results = semantic_search_service.search( |
| query=request.message, |
| max_results=request.max_context_chunks, |
| min_confidence=request.min_confidence |
| ) |
| |
| |
| if conversational_ai_service and conversational_ai_service.is_initialized: |
| ai_response = conversational_ai_service.generate_response( |
| user_question=request.message, |
| context_chunks=search_results, |
| session_history=request.history |
| ) |
| else: |
| |
| if search_results: |
| best_match = search_results[0] |
| ai_response = { |
| "text": f"Based on Giuseppe's background: {best_match['content'][:200]}...", |
| "confidence": best_match["confidence"], |
| "sources": [best_match["section"]], |
| "response_type": "semantic_only" |
| } |
| else: |
| ai_response = { |
| "text": "I don't have specific information to answer that question about Giuseppe's background.", |
| "confidence": 0.3, |
| "sources": [], |
| "response_type": "no_match" |
| } |
| |
| return ChatResponse( |
| status="success", |
| response=ai_response["text"], |
| confidence=ai_response["confidence"], |
| sources=ai_response["sources"], |
| response_type=ai_response["response_type"], |
| session_id=session_id, |
| timestamp=datetime.now(timezone.utc).isoformat() + "Z" |
| ) |
| |
| except ValueError as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
| except Exception as e: |
| if settings.debug_mode: |
| raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}") |
| else: |
| raise HTTPException(status_code=500, detail="Internal server error in chat processing") |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=7860) |