Skip to content
Closed
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# FreshScanAi - Backend error resolution

## Plan (approved)
- Fix build-time errors and likely import/module conflicts in `backend/main.py`.
- Ensure `backend/main.py` correctly imports local modules when run as a package.
- Make DB insert failures visible (no silent success) for scan endpoints.

## Steps
1. Update `backend/main.py` to use package-relative imports (`from .auth ...`, `from .turnstile ...`, `from .rate_limiter ...`, etc.).
2. Fix vendor router registration import to use relative imports.
3. Update scan endpoints (`/api/v1/scan` and `/api/v1/scan-auto`) so DB write failures raise HTTP 500 (instead of printing and continuing success).
4. Run backend import check (e.g., `python -c "from backend.main import app"`) and run unit tests if available.

11 changes: 10 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ TURNSTILE_SECRET_KEY=
# MODEL_TOKEN=hf_xxxxxxxxxxxxxxxxxxxx
=========
# ── CORS ──────────────────────────────────────────────────────────────────────
CORS_ALLOW_ALL=true
CORS_ALLOW_ALL=true

# ── LLM Provider (for AI Chat Assistant) ──────────────────────────────────────
# Options: gemini (default), openai, claude, ollama
LLM_PROVIDER=gemini
GEMINI_API_KEY=
OPENAI_API_KEY=
CLAUDE_API_KEY=
OLLAMA_BASE_URL=http://localhost:11434
CORS_ALLOW_ALL=true
86 changes: 86 additions & 0 deletions backend/chat_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import sqlite3
from pathlib import Path
from datetime import datetime, timezone

DB_DIR = Path(__file__).parent / "data"
DB_PATH = DB_DIR / "chat_logs.db"

def init_db():
"""Initializes the SQLite database and creates chat_logs table if it doesn't exist."""
DB_DIR.mkdir(parents=True, exist_ok=True)

conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS chat_logs (
id TEXT PRIMARY KEY,
question TEXT NOT NULL,
response TEXT NOT NULL,
current_page TEXT,
current_feature TEXT,
timestamp TEXT NOT NULL,
feedback TEXT
)
""")
conn.commit()
conn.close()

def log_chat_message(
msg_id: str,
question: str,
response: str,
current_page: str = None,
current_feature: str = None
):
"""Logs a generated Q&A exchange to the database."""
init_db() # Ensure DB and table are initialized

timestamp = datetime.now(timezone.utc).isoformat()

conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
try:
cursor.execute(
"""
INSERT INTO chat_logs (
id, question, response, current_page, current_feature,
timestamp, feedback
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
msg_id,
question,
response,
current_page,
current_feature,
timestamp,
None,
)
)
conn.commit()
except Exception as e:
print(f"ChatLogger Error logging message: {e}")
finally:
conn.close()

def update_chat_feedback(msg_id: str, feedback: str):
"""Updates feedback (e.g., 'up' or 'down') for a specific message ID."""
init_db() # Ensure DB and table are initialized

conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
try:
cursor.execute(
"""
UPDATE chat_logs
SET feedback = ?
WHERE id = ?
""",
(feedback, msg_id)
)
conn.commit()
except Exception as e:
print(f"ChatLogger Error updating feedback: {e}")
finally:
conn.close()
164 changes: 164 additions & 0 deletions backend/chat_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import uuid
import logging
from typing import List, Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from llm_provider import get_llm_provider
from rag_retriever import get_retriever
from chat_logger import log_chat_message, update_chat_feedback

logger = logging.getLogger("freshscan.chat")
router = APIRouter(prefix="/api/v1/chat", tags=["chat"])

# ── Pydantic Request/Response Models ──────────────────────────────────────────

class ChatHistoryItem(BaseModel):
role: str = Field(..., description="Either 'user' or 'assistant'")
content: str = Field(..., description="The message content")

class ChatMessageRequest(BaseModel):
question: str = Field(..., min_length=1, description="User question")
currentPage: Optional[str] = Field(
None, description="Active page user is viewing"
)
currentFeature: Optional[str] = Field(
None, description="Feature area user is interacting with"
)
history: Optional[List[ChatHistoryItem]] = Field(
default_factory=list, description="Recent conversation history"
)

class ChatMessageResponse(BaseModel):
message_id: str = Field(..., description="Unique ID for this response")
response: str = Field(..., description="Generated markdown text answer")

class ChatFeedbackRequest(BaseModel):
message_id: str = Field(..., description="ID of the message being rated")
feedback: str = Field(..., description="Feedback direction: 'up' or 'down'")

# ── Endpoints ─────────────────────────────────────────────────────────────────

@router.post("/message", response_model=ChatMessageResponse)
async def chat_message(request: ChatMessageRequest):
"""
Main Chat Assistant endpoint.
Retrieves local RAG context, merges page context & history, sends to LLM, and logs analytics.
"""
try:
# 1. Retrieve local documentation context (RAG)
context = ""
try:
retriever = get_retriever()
context = retriever.retrieve_relevant_context(request.question)
except Exception as e:
logger.error(f"RAG retrieval error: {e}")
# Non-blocking, continue with empty context

# 2. Build system prompt
system_prompt = (
"You are the official FreshScanAI Assistant.\n"
"Your primary purpose is helping users understand and navigate FreshScanAI.\n"
"Answer questions related to platform features, workflows, onboarding, "
"reports, dashboards, uploads, analysis processes, and troubleshooting.\n"
"Use retrieved documentation whenever available.\n"
"Never invent product features that do not exist.\n"
"If documentation does not contain the answer, politely explain that the "
"information is unavailable."
)

# 3. Incorporate page and feature context if provided
context_details = []
if request.currentPage:
context_details.append(f"- Active Page: {request.currentPage}")
if request.currentFeature:
context_details.append(f"- Active Feature/Section: {request.currentFeature}")

context_info = ""
if context_details:
context_info = "\nUser Current App Context:\n" + "\n".join(context_details) + "\n"

# 4. Integrate RAG documentation into LLM input
prompt = ""
if context:
prompt += (
f"Retrieved Documentation:\n{context}\n\n"
f"Instructions:\n"
"Use the retrieved documentation to answer the user's question. "
"Be factual, concise, and helpful. "
"If the information to answer is not present in the retrieved documentation, "
"state that the information is not available in the platform's documentation.\n\n"
)
else:
prompt += (
"No documentation was retrieved for this question. "
"Answer using only verified platform information if you are certain, "
"or politely state that the info is unavailable.\n\n"
)

if context_info:
prompt += context_info + "\n"

prompt += f"User Question: {request.question}"

# 5. Format history for provider
history_list = []
if request.history:
# Limit history to last 5 turns to prevent token bloat
for item in request.history[-10:]:
history_list.append({
"role": "user" if item.role == "user" else "assistant",
"content": item.content
})

# 6. Generate answer via provider
try:
provider = get_llm_provider()
response_text = provider.generate_response(system_prompt, prompt, history_list)
except Exception as provider_err:
logger.error(f"LLM Provider execution failed: {provider_err}")
response_text = (
"I'm sorry, I encountered a temporary connection issue "
"while trying to reach the AI model. "
"Please ensure LLM_PROVIDER and API keys are set correctly "
"in the environment configuration."
)

# 7. Log exchange to SQLite for analytics
msg_id = str(uuid.uuid4())
try:
log_chat_message(
msg_id=msg_id,
question=request.question,
response=response_text,
current_page=request.currentPage,
current_feature=request.currentFeature
)
except Exception as log_err:
logger.error(f"Failed to log chat interaction: {log_err}")

return ChatMessageResponse(message_id=msg_id, response=response_text)

except Exception as e:
logger.error(f"Unhandled error in chat message handler: {e}")
# Always return a user-friendly error envelope, never expose stack traces
raise HTTPException(
status_code=500,
detail="An unexpected error occurred in the chat assistant. Please try again."
)

@router.post("/feedback")
async def chat_feedback(request: ChatFeedbackRequest):
"""Logs thumbs up/down user feedback for a given message ID."""
if request.feedback not in ("up", "down"):
raise HTTPException(status_code=400, detail="Feedback must be either 'up' or 'down'")

try:
update_chat_feedback(request.message_id, request.feedback)
return {"success": True}
except Exception as e:
logger.error(f"Failed to record feedback for message {request.message_id}: {e}")
raise HTTPException(
status_code=500,
detail="Could not submit feedback due to an internal logger issue."
)
Loading
Loading