Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
3 changes: 3 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
TAVILY_API_KEY="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"
GOOGLE_API_KEY=AIzaSyDv2BzEVQY463VX-fHX3F7PYH6HdZdvIBU
GEMINI_MODEL=gemini-2.5-flash
TEMPERATURE=0.7
Binary file added backend/app/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added backend/app/__pycache__/main.cpython-312.pyc
Binary file not shown.
Binary file added backend/app/api/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added backend/app/api/__pycache__/api.cpython-312.pyc
Binary file not shown.
Binary file modified backend/app/api/__pycache__/api.cpython-313.pyc
Binary file not shown.
201 changes: 27 additions & 174 deletions backend/app/api/api.py
Original file line number Diff line number Diff line change
@@ -1,177 +1,30 @@
from fastapi import APIRouter, UploadFile, File, HTTPException, Query
from fastapi.responses import FileResponse
from fastapi import APIRouter, Query
from pydantic import BaseModel
from typing import List, Optional
import os
import shutil
from typing import Union
from app.services.chat_handler import get_intent_and_execute

from app.services.data_loader import data_loader
from app.services.search_service import search_service
from app.services.voice_service import voice_service
from app.services.image_service import image_service
from app.services.navigation_service import navigation_service
from app.core.config import settings

router = APIRouter()

# --- Models ---
class AskRequest(BaseModel):
question: str

class AskResponse(BaseModel):
answer: str
source: str # 'local', 'online', 'combined'
images: List[str] = []

class ProductSearchResponse(BaseModel):
class ItemSearchResponse(BaseModel):
query: str
results: List[dict]

class LineInfoResponse(BaseModel):
line_name: str
items_sold: List[str]
layout: dict
image_url: Optional[str]

class NavigateResponse(BaseModel):
line_name: str
directions: str
layout: dict

@router.post("/ask", response_model=AskResponse)
async def ask_question(request: AskRequest):
"""
Answer general questions using local history, market data, and online search.
"""
question = request.question.lower()

# 1. Check local data (simple keyword match for now)
local_context = ""
local_answer_parts = []
found_images = []

# Check history
if "history" in question or "built" in question or "old" in question:
history_snippet = data_loader.get_history()[:500]
if history_snippet:
local_context += f"History Context: {history_snippet}... "
local_answer_parts.append(f"According to market history: {history_snippet[:200]}...")

# Check lines/products
products = data_loader.search_products(question)
if products:
# Deduplicate line names
line_names = sorted(list(set([p['line_name'] for p in products])))
product_info = f"Found '{question}' in the following lines: {', '.join(line_names)}."
local_context += product_info + " "
local_answer_parts.append(product_info)

# Find images for these lines
if os.path.exists(settings.IMAGES_DIR):
all_images = os.listdir(settings.IMAGES_DIR)
for line_name in line_names:
# Find matching image (case-insensitive)
for img_file in all_images:
if img_file.lower().startswith(line_name.lower()):
found_images.append(f"/images/{img_file}")
break

# 2. Construct Answer
# If we have a strong local answer (products found), prioritize it.
if local_answer_parts:
full_local_answer = " ".join(local_answer_parts)
return AskResponse(
answer=full_local_answer,
source="local",
images=found_images
)

# 3. Use Tavily for comprehensive answer if no local data found
search_query = f"{question}. Context: {local_context}" if local_context else question
answer = search_service.search(search_query)

return AskResponse(
answer=answer,
source="online" if not local_context else "combined",
images=[]
)

@router.get("/product/search", response_model=ProductSearchResponse)
async def search_product(q: str = Query(..., description="Product name to search for")):
results = data_loader.search_products(q)
return ProductSearchResponse(query=q, results=results)

@router.get("/line/info/{line_name}", response_model=LineInfoResponse)
async def get_line_info(line_name: str):
line = data_loader.get_line_by_name(line_name)
if not line:
raise HTTPException(status_code=404, detail="Line not found")

# Construct image URL
# Assuming images are served at /images/{Line Name}.jpg
# We need to find the matching file since extension/casing might vary
image_filename = None
if os.path.exists(settings.IMAGES_DIR):
for f in os.listdir(settings.IMAGES_DIR):
if f.lower().startswith(line_name.lower()):
image_filename = f
break

image_url = f"/images/{image_filename}" if image_filename else None

return LineInfoResponse(
line_name=line["line_name"],
items_sold=line["items_sold"],
layout=line["layout"],
image_url=image_url
)

@router.get("/history")
async def get_history():
history = data_loader.get_history()
return {"history": history}

@router.post("/voice/query")
async def voice_query(file: UploadFile = File(...)):
# Save temp file
temp_path = f"temp_{file.filename}"
with open(temp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

# STT
text = voice_service.speech_to_text(temp_path)

# Cleanup input
os.remove(temp_path)

if not text:
raise HTTPException(status_code=400, detail="Could not understand audio")

# Process as a question (reuse logic or just return text)
# For this endpoint, let's return the text and a spoken response to a simple search

# Search for products
products = data_loader.search_products(text)
if products:
response_text = f"I found {text} in {len(products)} lines: {', '.join([p['line_name'] for p in products[:3]])}"
else:
# Fallback to general search
response_text = search_service.search(text)

# TTS
audio_response_path = voice_service.text_to_speech(response_text)

return FileResponse(audio_response_path, media_type="audio/mpeg", filename="response.mp3")

@router.post("/image/identify")
async def identify_image(file: UploadFile = File(...)):
contents = await file.read()
result = image_service.identify_product(contents)
return result

@router.get("/navigate", response_model=NavigateResponse)
async def navigate(line_name: str = Query(..., description="Target line name")):
result = navigation_service.get_directions(line_name)
if "error" in result:
raise HTTPException(status_code=404, detail=result["error"])
return NavigateResponse(**result)
direction: str
name: str

class InfoSearchResponse(BaseModel):
info: str

class ChatInterface:
def __init__(self):
self.router = APIRouter()

@self.router.get("/chat", response_model=Union[ItemSearchResponse, InfoSearchResponse])
async def chat(q: str = Query(..., description="The user query")):
result = get_intent_and_execute(q)
if 'direction' in result:
return ItemSearchResponse(
query=q,
direction=result.get("direction"),
name=result.get("name")
)
return InfoSearchResponse(info=result.get("info"))

# Instantiate the class and store in a variable named api
api = ChatInterface()
Binary file not shown.
Binary file not shown.
5 changes: 4 additions & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dotenv import load_dotenv

class Settings:
PROJECT_NAME: str = "MarketWay Navigator API"
PROJECT_NAME: str = "Sabi Market Navigator API"
PROJECT_VERSION: str = "1.0.0"

# Base path for data
Expand All @@ -18,5 +18,8 @@ class Settings:

# External APIs
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")
google_api_key = os.getenv("GOOGLE_API_KEY", "")
gemini_model = os.getenv("GEMINI_MODEL", "gemini-1.5-flash")
temperature = os.getenv("TEMPERATURE")

settings = Settings()
6 changes: 3 additions & 3 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.PROJECT_VERSION,
description="Backend API for MarketWay Navigator"
description="Backend API for Sabi Market Navigator"
)

# CORS Middleware
Expand All @@ -26,11 +26,11 @@
@app.get("/")
async def root():
return {
"message": "Welcome to MarketWay Navigator API",
"message": "Welcome to Sabi Market Navigator API",
"docs": "/docs",
"version": settings.PROJECT_VERSION
}

# Include routers
from app.api import api
from app.api.api import api
app.include_router(api.router)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified backend/app/services/__pycache__/data_loader.cpython-313.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
22 changes: 22 additions & 0 deletions backend/app/services/chat_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .router_service import router_service
from .data_loader import data_loader
from .info_service import info_service
from typing import Dict


def get_intent_and_execute(message: str) -> Dict[str, str]:
return execute(router_service.route(message))


def execute(router_info: dict) -> dict:
action = router_info.get("action")
if action == "search":
# Perform search action
query = router_info.get("query", "")
return data_loader.search_products(query)
elif action == "info":
topic = router_info.get("original_message", "")
answer = info_service.search(topic)
return {"info": answer}

# get_intent("Where can I get an umbrella?")
Loading