0504ankitsharma commited on
Commit
afdcbba
·
1 Parent(s): b26daf4

final changes

Browse files
.env.example CHANGED
@@ -7,6 +7,7 @@ PINECONE_API_KEY=your_pinecone_api_key_here
7
  PINECONE_INDEX_NAME=ikarus
8
  PINECONE_DIMENSION=1024
9
  PINECONE_ENVIRONMENT=us-east-1-aws
 
10
 
11
  # Data Configuration
12
  DATA_PATH=./data/dataset.csv
 
7
  PINECONE_INDEX_NAME=ikarus
8
  PINECONE_DIMENSION=1024
9
  PINECONE_ENVIRONMENT=us-east-1-aws
10
+ PINECONE_REGION=us-east-1
11
 
12
  # Data Configuration
13
  DATA_PATH=./data/dataset.csv
app/config.py CHANGED
@@ -11,6 +11,7 @@ class Settings(BaseSettings):
11
  PINECONE_INDEX_NAME: str
12
  PINECONE_DIMENSION: int = 1024
13
  PINECONE_ENVIRONMENT: str = "us-east-1-aws"
 
14
 
15
  # Data Configuration
16
  DATA_PATH: str = "data/dataset.csv"
 
11
  PINECONE_INDEX_NAME: str
12
  PINECONE_DIMENSION: int = 1024
13
  PINECONE_ENVIRONMENT: str = "us-east-1-aws"
14
+ PINECONE_REGION: str = "us-east-1"
15
 
16
  # Data Configuration
17
  DATA_PATH: str = "data/dataset.csv"
app/main.py CHANGED
@@ -2,8 +2,7 @@ import asyncio
2
  import logging
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
5
- from app.services.recommendation_service import RecommendationService
6
- from app.models import Product
7
 
8
  # Setup logging
9
 
@@ -25,31 +24,26 @@ allow_methods=["*"],
25
  allow_headers=["*"],
26
  )
27
 
28
- recommendation_service = RecommendationService()
 
 
 
29
 
30
  @app.on_event("startup")
31
  async def startup_event():
32
- """Run background indexing on startup."""
33
- logger.info("🚀 Starting up application...")
34
- try:
35
- logger.info("🔄 Starting background product indexing...")
36
- asyncio.create_task(recommendation_service.index_products())
37
- except Exception as e:
38
- logger.error(f"❌ Error during background indexing: {e}")
 
39
 
40
  @app.get("/")
41
  async def root():
42
- return {"message": "✅ Ikarus Furniture Recommendation API is running!"}
43
 
44
  @app.get("/health")
45
  async def health():
46
- return {"status": "ok"}
47
-
48
- @app.post("/recommend")
49
- async def recommend(product: Product):
50
- recommendations = await recommendation_service.recommend_products(product)
51
- return recommendations
52
-
53
- @app.post("/generate-description")
54
- async def generate_description(product: Product):
55
- return await recommendation_service.generate_description(product)
 
2
  import logging
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
5
+ from app.routes import recommendations, analytics, chat
 
6
 
7
  # Setup logging
8
 
 
24
  allow_headers=["*"],
25
  )
26
 
27
+ # Register routers
28
+ app.include_router(recommendations.router)
29
+ app.include_router(chat.router)
30
+ app.include_router(analytics.router)
31
 
32
  @app.on_event("startup")
33
  async def startup_event():
34
+ """Run background indexing on startup."""
35
+ logger.info("🚀 Starting up application...")
36
+ try:
37
+ from app.services.recommendation_service import recommendation_service
38
+ logger.info("🔄 Starting background product indexing...")
39
+ asyncio.create_task(recommendation_service.index_products())
40
+ except Exception as e:
41
+ logger.error(f"❌ Error during background indexing: {e}")
42
 
43
  @app.get("/")
44
  async def root():
45
+ return {"message": "✅ Ikarus Furniture Recommendation API is running!"}
46
 
47
  @app.get("/health")
48
  async def health():
49
+ return {"status": "ok"}
 
 
 
 
 
 
 
 
 
app/models.py CHANGED
@@ -22,8 +22,8 @@ class RecommendationRequest(BaseModel):
22
  include_description: bool = True
23
 
24
  class GeneratedDescription(BaseModel):
 
25
  original: Optional[str] = None
26
- generated: str
27
  timestamp: datetime = Field(default_factory=datetime.now)
28
 
29
  class RecommendedProduct(BaseModel):
 
22
  include_description: bool = True
23
 
24
  class GeneratedDescription(BaseModel):
25
+ text: str
26
  original: Optional[str] = None
 
27
  timestamp: datetime = Field(default_factory=datetime.now)
28
 
29
  class RecommendedProduct(BaseModel):
app/routes/recommendations.py CHANGED
@@ -15,10 +15,10 @@ from app.utils.data_loader import data_loader
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
- router = APIRouter(prefix="/recommendations", tags=["Recommendations"])
19
 
20
 
21
- @router.post("/", response_model=RecommendationResponse)
22
  async def get_recommendations(req: RecommendationRequest):
23
  """
24
  Generate product recommendations based on a user's query.
@@ -91,3 +91,64 @@ async def get_recommendations(req: RecommendationRequest):
91
  except Exception as e:
92
  logger.error(f"❌ Error during recommendation: {e}", exc_info=True)
93
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
+ router = APIRouter(prefix="/api/recommendations", tags=["Recommendations"])
19
 
20
 
21
+ @router.post("/search", response_model=RecommendationResponse)
22
  async def get_recommendations(req: RecommendationRequest):
23
  """
24
  Generate product recommendations based on a user's query.
 
91
  except Exception as e:
92
  logger.error(f"❌ Error during recommendation: {e}", exc_info=True)
93
  raise HTTPException(status_code=500, detail=str(e))
94
+
95
+
96
+ @router.get("/similar/{product_id}")
97
+ async def get_similar_products(product_id: str, top_k: int = 5):
98
+ """
99
+ Get similar products based on a product ID.
100
+ """
101
+ try:
102
+ logger.info(f"🔍 Finding similar products for: {product_id}")
103
+
104
+ # Get the product
105
+ product = data_loader.get_product_by_id(product_id)
106
+ if not product:
107
+ raise HTTPException(status_code=404, detail=f"Product {product_id} not found")
108
+
109
+ # Get product embedding
110
+ product_vector = embedding_service.encode_product(product)
111
+
112
+ # Query for similar products (top_k + 1 to exclude the product itself)
113
+ results = vector_db.query_vectors(product_vector, top_k=top_k + 1)
114
+
115
+ # Build similar products list (skip first result as it's the same product)
116
+ similar_products = []
117
+ for match in results[1:top_k + 1]:
118
+ metadata = match.get("metadata", {}) if isinstance(match, dict) else getattr(match, "metadata", {})
119
+
120
+ similar_product = Product(
121
+ uniq_id=metadata.get("uniq_id", ""),
122
+ title=metadata.get("title", "Unknown Product"),
123
+ brand=metadata.get("brand", None),
124
+ description=metadata.get("description", None),
125
+ price=metadata.get("price", None),
126
+ categories=metadata.get("categories", "").split(",") if isinstance(metadata.get("categories"), str) else [],
127
+ images=metadata.get("images", []),
128
+ manufacturer=metadata.get("manufacturer", None),
129
+ package_dimensions=metadata.get("package_dimensions", None),
130
+ country_of_origin=metadata.get("country_of_origin", None),
131
+ material=metadata.get("material", None),
132
+ color=metadata.get("color", None),
133
+ )
134
+
135
+ similar_products.append(
136
+ RecommendedProduct(
137
+ product=similar_product,
138
+ score=float(match.get("score", 0.0) if isinstance(match, dict) else getattr(match, "score", 0.0)),
139
+ generated_description=None,
140
+ )
141
+ )
142
+
143
+ logger.info(f"✅ Found {len(similar_products)} similar products")
144
+ return {
145
+ "product_id": product_id,
146
+ "similar_products": similar_products,
147
+ "total": len(similar_products)
148
+ }
149
+
150
+ except HTTPException:
151
+ raise
152
+ except Exception as e:
153
+ logger.error(f"❌ Error finding similar products: {e}", exc_info=True)
154
+ raise HTTPException(status_code=500, detail=str(e))
app/services/embedding_service.py CHANGED
@@ -31,6 +31,51 @@ class EmbeddingService:
31
  self.model = None
32
  else:
33
  logger.info("Gemini embedding not available; using local fallback.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def _text_to_vector(text: str, dim: int = 1536):
36
  """Deterministic pseudo-embedding using SHA256 hashed chunks -> vector of floats in [-1,1]."""
 
31
  self.model = None
32
  else:
33
  logger.info("Gemini embedding not available; using local fallback.")
34
+
35
+ async def embed_text(self, text: str) -> List[float]:
36
+ """Generate embedding for text query."""
37
+ try:
38
+ if self.model:
39
+ try:
40
+ response = genai.embed_content(model=self.model, content=text, task_type="retrieval_query")
41
+ emb = None
42
+ if isinstance(response, dict) and "embedding" in response:
43
+ emb = response["embedding"]
44
+ else:
45
+ emb = getattr(response, "embedding", None) or getattr(response, "embeddings", None)
46
+ if emb:
47
+ return list(emb)
48
+ except Exception as e:
49
+ logger.warning("Provider embedding failed, falling back to local: %s", e)
50
+ return _text_to_vector(text, dim=settings.PINECONE_DIMENSION)
51
+ except Exception as e:
52
+ logger.exception("Error embedding text: %s", e)
53
+ raise
54
+
55
+ def encode_product(self, product) -> List[float]:
56
+ """Encode product for embedding. (Synchronous wrapper)"""
57
+ from app.models import Product
58
+ text = " ".join(filter(None, [
59
+ getattr(product, "title", "") or "",
60
+ getattr(product, "brand", "") or "",
61
+ getattr(product, "description", "") or "",
62
+ getattr(product, "material", "") or "",
63
+ getattr(product, "color", "") or "",
64
+ ",".join(getattr(product, "categories", []) if getattr(product, "categories", None) else []),
65
+ ]))
66
+ if self.model:
67
+ try:
68
+ response = genai.embed_content(model=self.model, content=text, task_type="retrieval_document")
69
+ emb = None
70
+ if isinstance(response, dict) and "embedding" in response:
71
+ emb = response["embedding"]
72
+ else:
73
+ emb = getattr(response, "embedding", None) or getattr(response, "embeddings", None)
74
+ if emb:
75
+ return list(emb)
76
+ except Exception as e:
77
+ logger.warning("Provider embedding failed, falling back to local: %s", e)
78
+ return _text_to_vector(text, dim=settings.PINECONE_DIMENSION)
79
 
80
  def _text_to_vector(text: str, dim: int = 1536):
81
  """Deterministic pseudo-embedding using SHA256 hashed chunks -> vector of floats in [-1,1]."""
app/services/genai_service.py CHANGED
@@ -6,7 +6,7 @@ suitable for local testing and development.
6
  import logging
7
  from typing import List
8
  from app.config import settings
9
- from app.models import Product, GeneratedDescription
10
 
11
  logger = logging.getLogger(__name__)
12
  try:
@@ -17,15 +17,17 @@ except Exception:
17
 
18
  class GenAIService:
19
  def __init__(self):
20
- self.model = None
 
21
  if _HAS_GENAI and getattr(settings, "GEMINI_API_KEY", None):
22
  try:
23
  genai.configure(api_key=settings.GEMINI_API_KEY)
24
- self.model = getattr(settings, "GEMINI_MODEL", None) or "gpt-4o-mini"
25
- logger.info("GenAI provider initialized with model %s", self.model)
 
26
  except Exception as e:
27
  logger.warning("Failed to initialize GenAI provider, using local fallback: %s", e)
28
- self.model = None
29
  else:
30
  logger.info("GenAI provider not available; using local fallback.")
31
 
@@ -41,37 +43,39 @@ class GenAIService:
41
  color = getattr(product, "color", "") or ""
42
  generated = f"{title} by {brand}. {desc} Material: {material}. Color: {color}."
43
  # Try provider if available
44
- if self.model:
45
  try:
46
  prompt = f"Write a short product description for the following product:\n\nTitle: {title}\nBrand: {brand}\nDescription: {desc}\nMaterial: {material}\nColor: {color}\n\nKeep it under 80 words."
47
- response = genai.generate_text(model=self.model, prompt=prompt) if hasattr(genai, "generate_text") else None
48
- text = None
49
- if isinstance(response, dict):
50
- text = response.get("candidates", [{}])[0].get("content")
51
- elif response is not None:
52
- text = getattr(response, "text", None) or getattr(response, "content", None)
53
- if text:
54
- generated = text
55
  except Exception as e:
56
  logger.warning("External GenAI generation failed, using fallback: %s", e)
57
  # Return a simple GeneratedDescription-like object (dict) to avoid tight coupling
58
- return GeneratedDescription(text=generated)
59
  except Exception as e:
60
  logger.exception("Error generating product description: %s", e)
61
  return GeneratedDescription(text=str(e))
 
 
 
 
 
 
 
 
 
62
 
63
  def conversational_response(self, user_query: str, products: List[Product]) -> str:
64
  """Return a conversational assistant message. Fallback to simple template if provider unavailable."""
65
  try:
66
- if self.model:
67
  # Try provider chat style if available
68
  try:
69
  prompt = f"User asked: {user_query}\nReturn a short helpful message and summarize top {min(3,len(products))} product titles."
70
- response = genai.generate_text(model=self.model, prompt=prompt) if hasattr(genai, "generate_text") else None
71
- if isinstance(response, dict):
72
- return response.get("candidates", [{}])[0].get("content", "") or ""
73
- elif response is not None:
74
- return getattr(response, "text", "") or getattr(response, "content", "") or ""
75
  except Exception as e:
76
  logger.warning("External GenAI conversational failed: %s", e)
77
  # Local fallback
@@ -82,6 +86,56 @@ class GenAIService:
82
  except Exception as e:
83
  logger.exception("Error building conversational response: %s", e)
84
  return "Sorry, something went wrong."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  # Global instance
87
  genai_service = GenAIService()
 
6
  import logging
7
  from typing import List
8
  from app.config import settings
9
+ from app.models import Product, GeneratedDescription, ChatMessage
10
 
11
  logger = logging.getLogger(__name__)
12
  try:
 
17
 
18
  class GenAIService:
19
  def __init__(self):
20
+ self.client = None
21
+ self.model_name = None
22
  if _HAS_GENAI and getattr(settings, "GEMINI_API_KEY", None):
23
  try:
24
  genai.configure(api_key=settings.GEMINI_API_KEY)
25
+ self.model_name = getattr(settings, "GEMINI_MODEL", "gemini-2.5-flash")
26
+ self.client = genai.GenerativeModel(self.model_name)
27
+ logger.info("GenAI provider initialized with model %s", self.model_name)
28
  except Exception as e:
29
  logger.warning("Failed to initialize GenAI provider, using local fallback: %s", e)
30
+ self.client = None
31
  else:
32
  logger.info("GenAI provider not available; using local fallback.")
33
 
 
43
  color = getattr(product, "color", "") or ""
44
  generated = f"{title} by {brand}. {desc} Material: {material}. Color: {color}."
45
  # Try provider if available
46
+ if self.client:
47
  try:
48
  prompt = f"Write a short product description for the following product:\n\nTitle: {title}\nBrand: {brand}\nDescription: {desc}\nMaterial: {material}\nColor: {color}\n\nKeep it under 80 words."
49
+ response = self.client.generate_content(prompt)
50
+ if response and hasattr(response, 'text'):
51
+ generated = response.text
 
 
 
 
 
52
  except Exception as e:
53
  logger.warning("External GenAI generation failed, using fallback: %s", e)
54
  # Return a simple GeneratedDescription-like object (dict) to avoid tight coupling
55
+ return GeneratedDescription(text=generated, original=desc)
56
  except Exception as e:
57
  logger.exception("Error generating product description: %s", e)
58
  return GeneratedDescription(text=str(e))
59
+
60
+ async def generate_description(self, product: Product) -> str:
61
+ """Generate a product description and return just the text."""
62
+ try:
63
+ desc = self.generate_product_description(product)
64
+ return desc.text
65
+ except Exception as e:
66
+ logger.error(f"Error generating description: {e}")
67
+ return f"{product.title} - {product.description or 'No description available'}"
68
 
69
  def conversational_response(self, user_query: str, products: List[Product]) -> str:
70
  """Return a conversational assistant message. Fallback to simple template if provider unavailable."""
71
  try:
72
+ if self.client:
73
  # Try provider chat style if available
74
  try:
75
  prompt = f"User asked: {user_query}\nReturn a short helpful message and summarize top {min(3,len(products))} product titles."
76
+ response = self.client.generate_content(prompt)
77
+ if response and hasattr(response, 'text'):
78
+ return response.text
 
 
79
  except Exception as e:
80
  logger.warning("External GenAI conversational failed: %s", e)
81
  # Local fallback
 
86
  except Exception as e:
87
  logger.exception("Error building conversational response: %s", e)
88
  return "Sorry, something went wrong."
89
+
90
+ async def chat_response(self, message: str, conversation_history: List[ChatMessage]) -> str:
91
+ """Generate a chat response based on user message and conversation history."""
92
+ try:
93
+ if self.client:
94
+ try:
95
+ # Build context from conversation history
96
+ context = "\n".join([f"{msg.role}: {msg.content}" for msg in conversation_history[-5:]])
97
+ prompt = f"""You are a helpful furniture shopping assistant. Based on the conversation history, respond to the user's query.
98
+
99
+ Conversation History:
100
+ {context}
101
+
102
+ User: {message}
103
+
104
+ Assistant:"""
105
+ response = self.client.generate_content(prompt)
106
+ if response and hasattr(response, 'text'):
107
+ return response.text
108
+ except Exception as e:
109
+ logger.warning(f"GenAI chat failed, using fallback: {e}")
110
+
111
+ # Fallback response
112
+ return f"I understand you're looking for furniture. Let me find some recommendations for you based on: {message}"
113
+ except Exception as e:
114
+ logger.error(f"Error generating chat response: {e}")
115
+ return "I'm here to help you find furniture. What are you looking for?"
116
+
117
+ def enhance_query(self, query: str) -> str:
118
+ """Enhance user query for better semantic search."""
119
+ try:
120
+ if self.client:
121
+ try:
122
+ prompt = f"""Expand this furniture search query to include related terms and synonyms for better search results. Keep it concise (under 50 words).
123
+
124
+ Query: {query}
125
+
126
+ Expanded query:"""
127
+ response = self.client.generate_content(prompt)
128
+ if response and hasattr(response, 'text'):
129
+ enhanced = response.text.strip()
130
+ if enhanced and len(enhanced) < 200:
131
+ return enhanced
132
+ except Exception as e:
133
+ logger.warning(f"Query enhancement failed: {e}")
134
+ # Fallback: return original query
135
+ return query
136
+ except Exception as e:
137
+ logger.error(f"Error enhancing query: {e}")
138
+ return query
139
 
140
  # Global instance
141
  genai_service = GenAIService()
app/services/recommendation_service.py CHANGED
@@ -26,15 +26,21 @@ class RecommendationService:
26
 
27
  for product in products:
28
  try:
29
- embedding = await embedding_service.encode_product(product)
 
30
  metadata = {
31
  "uniq_id": product.uniq_id,
32
  "title": product.title,
33
  "brand": product.brand or "",
 
34
  "price": product.price or "",
35
  "categories": ",".join(product.categories) if product.categories else "",
 
36
  "material": product.material or "",
37
  "color": product.color or "",
 
 
 
38
  }
39
  vectors.append((product.uniq_id, embedding, metadata))
40
  except Exception as e:
@@ -120,7 +126,8 @@ class RecommendationService:
120
  logger.warning(f"Product not found: {product_id}")
121
  return []
122
 
123
- product_embedding = await embedding_service.encode_product(product)
 
124
  results = vector_db.query_vectors(product_embedding, top_k=top_k + 1)
125
 
126
  recommendations = []
 
26
 
27
  for product in products:
28
  try:
29
+ # Use synchronous encode_product method
30
+ embedding = embedding_service.encode_product(product)
31
  metadata = {
32
  "uniq_id": product.uniq_id,
33
  "title": product.title,
34
  "brand": product.brand or "",
35
+ "description": product.description or "",
36
  "price": product.price or "",
37
  "categories": ",".join(product.categories) if product.categories else "",
38
+ "images": product.images or [],
39
  "material": product.material or "",
40
  "color": product.color or "",
41
+ "manufacturer": product.manufacturer or "",
42
+ "package_dimensions": product.package_dimensions or "",
43
+ "country_of_origin": product.country_of_origin or "",
44
  }
45
  vectors.append((product.uniq_id, embedding, metadata))
46
  except Exception as e:
 
126
  logger.warning(f"Product not found: {product_id}")
127
  return []
128
 
129
+ # Use synchronous encode_product method
130
+ product_embedding = embedding_service.encode_product(product)
131
  results = vector_db.query_vectors(product_embedding, top_k=top_k + 1)
132
 
133
  recommendations = []