File size: 28,164 Bytes
b540dd9 f85338f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 | """
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
# Optional imports for AI features
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
# Import our custom modules
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:
# Try to load AI-ready content first
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")
# Prepare content chunks for embedding
self.content_chunks = []
# Process AI-ready content format
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"
})
# Generate embeddings for all content chunks
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")
# Preprocess query to expand company names
expanded_query = self._preprocess_query(query)
if expanded_query != query:
print(f"Expanded query from '{query}' to '{expanded_query}'")
# Encode the query
query_embedding = self.embedding_model.encode([expanded_query], convert_to_numpy=True)
# Calculate cosine similarities
similarities = np.dot(self.content_embeddings, query_embedding.T).flatten()
# Get top results above confidence threshold
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)
})
# Sort by confidence and limit results
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 name mappings based on CV content
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 category mappings - generic terms that should find relevant content
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"
}
# Spoken language context mappings
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]
# Add language context if this is about spoken languages
for phrase, expansion in language_mappings.items():
if phrase in query_lower:
expanded_parts.append(expansion)
break # Only apply one language mapping
# Add skill expansions if no language context was added
if len(expanded_parts) == 1: # No language mapping was added
for keyword, expansion in skill_mappings.items():
if keyword in query_lower:
expanded_parts.append(expansion)
# Add company expansions
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:
# Check if OpenAI should be used
if self.settings.use_openai and self.settings.openai_api_key and OPENAI_AVAILABLE:
# Initialize OpenAI client without proxies parameter (fixed for openai>=1.0)
self.openai_client = OpenAI(
api_key=self.settings.openai_api_key,
timeout=30.0 # Set timeout instead of proxies
)
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:
# Build context from retrieved chunks
context_text = self._build_context_from_chunks(context_chunks)
# Try direct fact extraction first (more reliable)
extracted_response = self._extract_factual_response(user_question, context_text, context_chunks)
if extracted_response:
return extracted_response
# Use OpenAI for complex responses
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()
# Spoken languages question (prioritize this over programming languages)
if any(keyword in question_lower for keyword in ['what languages', 'languages speak', 'spoken languages', 'languages does']):
# Look for language proficiency info
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"
}
# No direct factual match found
return None
def _generate_openai_response(self, user_question: str, context_text: str, context_chunks: List[Dict]) -> Dict:
"""Generate response using OpenAI API"""
try:
# Create professional prompt for OpenAI
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"""
# Always provide comprehensive context - focus on capabilities and achievements
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()
# Enhance formatting for better readability
formatted_text = self._format_response_text(generated_text)
# Determine confidence based on context quality
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"""
# Add line breaks before bullet points and numbered lists
text = text.replace('- ', '\n- ')
text = text.replace('• ', '\n• ')
# Add line breaks before numbered items
text = re.sub(r'(\d+\.\s)', r'\n\1', text)
# Add line breaks before headers
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)
# Clean up multiple consecutive newlines
text = re.sub(r'\n\s*\n+', '\n\n', text)
# Remove leading newlines
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]: # Use top 3 most relevant chunks
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"
}
# Use the most relevant chunk for a simple response
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"
}
# Global variables for model loading
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:
# Load sentence transformer model only if available
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}")
# Initialize semantic search service
semantic_search_service = SemanticSearchService(embedding_model)
# Load CV content and generate embeddings
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
# Load conversational AI service (OpenAI-based)
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
# Cleanup (if needed)
app = FastAPI(
title=settings.api_title,
description=settings.api_description,
version=settings.api_version,
lifespan=lifespan
)
# Add rate limiting middleware
app.add_middleware(
RateLimitMiddleware,
requests_per_window=settings.rate_limit_requests,
window_seconds=settings.rate_limit_window
)
# CORS middleware for frontend integration - Allow all origins for HF Spaces
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for HF Spaces
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Pydantic models
class HealthResponse(BaseModel):
status: str
timestamp: str
version: str
environment: str
system_info: dict
models_loaded: bool
class ChatMessage(BaseModel):
role: str # "user" or "assistant"
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:
# Get system information
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:
# Generate session ID if not provided
session_id = request.session_id or f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
# Check if semantic search is available
if not semantic_search_service or not semantic_search_service.is_initialized:
# Fallback response without semantic search
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"
)
# Perform semantic search
search_results = semantic_search_service.search(
query=request.message,
max_results=request.max_context_chunks,
min_confidence=request.min_confidence
)
# Generate conversational response
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:
# Simple fallback when AI service is not available
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) |