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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +35 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove duplicated CORS_ALLOW_ALL.

The key is declared twice, which makes configuration precedence ambiguous during audits/debugging.

🧹 Proposed fix
 CORS_ALLOW_ALL=true
@@
 OLLAMA_BASE_URL=http://localhost:11434
-CORS_ALLOW_ALL=true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 40-40: [UnorderedKey] The GEMINI_API_KEY key should go before the LLM_PROVIDER key

(UnorderedKey)


[warning] 42-42: [UnorderedKey] The CLAUDE_API_KEY key should go before the GEMINI_API_KEY key

(UnorderedKey)


[warning] 43-43: [UnorderedKey] The OLLAMA_BASE_URL key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 44-44: [DuplicatedKey] The CORS_ALLOW_ALL key is duplicated

(DuplicatedKey)


[warning] 44-44: [UnorderedKey] The CORS_ALLOW_ALL key should go before the GEMINI_API_KEY key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/.env.example` around lines 35 - 44, Remove the duplicate
CORS_ALLOW_ALL configuration key from the .env.example file. Keep the first
occurrence of CORS_ALLOW_ALL=true that appears at the beginning of the
configuration section, and delete the second duplicate occurrence that appears
at the end of the LLM Provider section. This ensures each configuration key is
declared only once, removing any ambiguity about which value takes precedence.

Source: Linters/SAST tools

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()
Comment on lines +62 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow SQLite write errors.

Both functions print exceptions and return normally, so API handlers can report success even when writes fail.

🛠️ Proposed fix
-    except Exception as e:
-        print(f"ChatLogger Error logging message: {e}")
+    except sqlite3.Error as e:
+        conn.rollback()
+        raise RuntimeError("Failed to log chat message") from e
@@
-    except Exception as e:
-        print(f"ChatLogger Error updating feedback: {e}")
+    except sqlite3.Error as e:
+        conn.rollback()
+        raise RuntimeError("Failed to update chat feedback") from e

Also applies to: 83-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/chat_logger.py` around lines 62 - 65, The exception handling in
ChatLogger is swallowing SQLite write errors by catching exceptions, printing
them, and then returning normally, which causes API handlers to report success
when writes actually fail. Instead of just printing the exception in the except
blocks (around lines 62-65 and 83-86), re-raise the exception after logging it
so that the error properly propagates to the caller and API handlers can detect
and report the failure appropriately.


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()
Comment on lines +74 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate that feedback updates actually match a message ID.

UPDATE can affect 0 rows (unknown msg_id) and still commit; downstream currently returns {success: true}.

✅ Proposed fix
         cursor.execute(
@@
             (feedback, msg_id)
         )
+        if cursor.rowcount == 0:
+            raise ValueError(f"Message ID not found: {msg_id}")
         conn.commit()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cursor.execute(
"""
UPDATE chat_logs
SET feedback = ?
WHERE id = ?
""",
(feedback, msg_id)
)
conn.commit()
cursor.execute(
"""
UPDATE chat_logs
SET feedback = ?
WHERE id = ?
""",
(feedback, msg_id)
)
if cursor.rowcount == 0:
raise ValueError(f"Message ID not found: {msg_id}")
conn.commit()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/chat_logger.py` around lines 74 - 82, The UPDATE statement in the
chat_logs feedback update logic does not validate whether the update actually
affected any rows. After the cursor.execute() call that updates feedback, check
cursor.rowcount to determine if any rows were actually modified. Only proceed
with conn.commit() if cursor.rowcount is greater than 0, and return an
appropriate response indicating failure or success based on whether the msg_id
matched an existing record in the database.

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):
"""
Comment on lines +43 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify async route + sync call chain
rg -n -C2 'async def chat_message|async def chat_feedback|get_retriever\(|generate_response\(|log_chat_message\(|update_chat_feedback\(' backend/chat_router.py
rg -n -C2 'httpx\.post\(' backend/llm_provider.py
rg -n -C2 'sqlite3\.connect\(' backend/chat_logger.py

Repository: jpdevhub/FreshScanAi

Length of output: 2340


Remove async from blocking I/O handlers.

These routes execute synchronous LLM HTTP and SQLite calls that block the event loop under concurrent load. Converting to synchronous handlers allows proper concurrent request handling.

Proposed fix
-async def chat_message(request: ChatMessageRequest):
+def chat_message(request: ChatMessageRequest):

Also applies to chat_feedback() (line 151).

Also applies to: 115-117, 151-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/chat_router.py` around lines 43 - 44, The chat_message function and
chat_feedback function are defined as async but perform blocking synchronous I/O
operations (LLM HTTP calls and SQLite operations) that block the event loop
under concurrent load. Remove the async keyword from the function definitions of
chat_message and chat_feedback to convert them to synchronous handlers, which
allows the ASGI framework to properly schedule and execute these blocking
operations on thread pools without blocking concurrent requests.

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