Spaces:
Sleeping
Sleeping
0504ankitsharma commited on
Commit ·
afdcbba
1
Parent(s): b26daf4
final changes
Browse files- .env.example +1 -0
- app/config.py +1 -0
- app/main.py +15 -21
- app/models.py +1 -1
- app/routes/recommendations.py +63 -2
- app/services/embedding_service.py +45 -0
- app/services/genai_service.py +75 -21
- app/services/recommendation_service.py +9 -2
.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.
|
| 6 |
-
from app.models import Product
|
| 7 |
|
| 8 |
# Setup logging
|
| 9 |
|
|
@@ -25,31 +24,26 @@ allow_methods=["*"],
|
|
| 25 |
allow_headers=["*"],
|
| 26 |
)
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 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.
|
|
|
|
| 21 |
if _HAS_GENAI and getattr(settings, "GEMINI_API_KEY", None):
|
| 22 |
try:
|
| 23 |
genai.configure(api_key=settings.GEMINI_API_KEY)
|
| 24 |
-
self.
|
| 25 |
-
|
|
|
|
| 26 |
except Exception as e:
|
| 27 |
logger.warning("Failed to initialize GenAI provider, using local fallback: %s", e)
|
| 28 |
-
self.
|
| 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.
|
| 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 =
|
| 48 |
-
|
| 49 |
-
|
| 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.
|
| 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 =
|
| 71 |
-
if
|
| 72 |
-
return response.
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 = []
|