chore: AI Chat improved
Browse files- Backend/app/api/v1/endpoints/notes.py +6 -4
- Backend/app/api/v1/endpoints/quiz.py +42 -5
- Backend/app/llm.py +38 -13
Backend/app/api/v1/endpoints/notes.py
CHANGED
|
@@ -2,13 +2,12 @@ from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile
|
|
| 2 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
from app.models import User
|
| 4 |
from app.models.tables import PDFData
|
| 5 |
-
from app.api.deps import get_db, get_current_user
|
| 6 |
from app.schema import AI_chat_input
|
| 7 |
from app.llm import stream_chat
|
| 8 |
import uuid
|
| 9 |
from fastapi.responses import StreamingResponse
|
| 10 |
from chromadb.api.models.Collection import Collection
|
| 11 |
-
from app.api.deps import get_chroma_collection
|
| 12 |
from pathlib import Path
|
| 13 |
from llama_index.readers.file import PyMuPDFReader
|
| 14 |
from llama_index.core.node_parser import SentenceSplitter
|
|
@@ -16,7 +15,7 @@ from typing import Annotated
|
|
| 16 |
import shutil
|
| 17 |
import os
|
| 18 |
from sentence_transformers import SentenceTransformer
|
| 19 |
-
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/notes")
|
| 22 |
|
|
@@ -28,12 +27,15 @@ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
|
|
| 28 |
@router.post("/stream_chat", response_class=StreamingResponse)
|
| 29 |
async def ai_chat(
|
| 30 |
Input_model: AI_chat_input,
|
|
|
|
| 31 |
current_user: User = Depends(get_current_user)
|
| 32 |
):
|
| 33 |
messages_dict = [msg.model_dump() for msg in Input_model.messages]
|
|
|
|
|
|
|
| 34 |
|
| 35 |
return StreamingResponse(
|
| 36 |
-
stream_chat(messages_dict, Input_model.context),
|
| 37 |
media_type="text/plain"
|
| 38 |
)
|
| 39 |
|
|
|
|
| 2 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
from app.models import User
|
| 4 |
from app.models.tables import PDFData
|
| 5 |
+
from app.api.deps import get_db, get_current_user, get_chroma_collection
|
| 6 |
from app.schema import AI_chat_input
|
| 7 |
from app.llm import stream_chat
|
| 8 |
import uuid
|
| 9 |
from fastapi.responses import StreamingResponse
|
| 10 |
from chromadb.api.models.Collection import Collection
|
|
|
|
| 11 |
from pathlib import Path
|
| 12 |
from llama_index.readers.file import PyMuPDFReader
|
| 13 |
from llama_index.core.node_parser import SentenceSplitter
|
|
|
|
| 15 |
import shutil
|
| 16 |
import os
|
| 17 |
from sentence_transformers import SentenceTransformer
|
| 18 |
+
from .quiz import search_logic
|
| 19 |
|
| 20 |
router = APIRouter(prefix="/notes")
|
| 21 |
|
|
|
|
| 27 |
@router.post("/stream_chat", response_class=StreamingResponse)
|
| 28 |
async def ai_chat(
|
| 29 |
Input_model: AI_chat_input,
|
| 30 |
+
collection: Collection = Depends(get_chroma_collection),
|
| 31 |
current_user: User = Depends(get_current_user)
|
| 32 |
):
|
| 33 |
messages_dict = [msg.model_dump() for msg in Input_model.messages]
|
| 34 |
+
query = f"{Input_model.context};{Input_model.messages[-1].content}"
|
| 35 |
+
retrieved_docs: str | None = await search_logic(query, collection)
|
| 36 |
|
| 37 |
return StreamingResponse(
|
| 38 |
+
stream_chat(messages_dict, Input_model.context, retrieved_docs),
|
| 39 |
media_type="text/plain"
|
| 40 |
)
|
| 41 |
|
Backend/app/api/v1/endpoints/quiz.py
CHANGED
|
@@ -9,15 +9,52 @@ from chromadb.api.models.Collection import Collection
|
|
| 9 |
from app.api.deps import get_chroma_collection
|
| 10 |
from app.llm import call_llm
|
| 11 |
import uuid
|
|
|
|
|
|
|
| 12 |
|
| 13 |
router = APIRouter(prefix="/quiz")
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
async def search_logic(query: str, collection: Collection):
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
@router.get("/search_docs")
|
| 23 |
async def search_documents(
|
|
|
|
| 9 |
from app.api.deps import get_chroma_collection
|
| 10 |
from app.llm import call_llm
|
| 11 |
import uuid
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
|
| 15 |
router = APIRouter(prefix="/quiz")
|
| 16 |
|
| 17 |
+
|
| 18 |
+
# 1. Set up a logger (if you haven't already globally)
|
| 19 |
+
logger = logging.getLogger("uvicorn.error") # reusing uvicorn's logger ensures it shows up in your terminal
|
| 20 |
+
|
| 21 |
async def search_logic(query: str, collection: Collection):
|
| 22 |
+
# Log the incoming query
|
| 23 |
+
logger.info(f"🔍 [Search Logic] Starting search for query: '{query}'")
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
results = await collection.query(
|
| 27 |
+
query_texts=[query],
|
| 28 |
+
n_results=5
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Log the raw results to see exactly what ChromaDB returned (helps spot NoneTypes)
|
| 32 |
+
logger.info(f"📄 [Search Logic] Raw results from DB: {results}")
|
| 33 |
+
|
| 34 |
+
if results and results.get('documents') and len(results['documents']) > 0:
|
| 35 |
+
raw_docs = results['documents'][0]
|
| 36 |
+
|
| 37 |
+
# Filter None values and Log how many were found vs valid
|
| 38 |
+
valid_docs = [str(doc) for doc in raw_docs if doc is not None]
|
| 39 |
+
|
| 40 |
+
logger.info(f"✅ [Search Logic] Processing: Found {len(raw_docs)} items. Valid text items: {len(valid_docs)}")
|
| 41 |
+
|
| 42 |
+
if len(raw_docs) != len(valid_docs):
|
| 43 |
+
logger.warning("⚠️ [Search Logic] Warning: Some documents contained NoneType and were skipped.")
|
| 44 |
+
|
| 45 |
+
# Join with a space (safer than empty string)
|
| 46 |
+
final_context = " ".join(valid_docs)
|
| 47 |
+
return final_context
|
| 48 |
+
|
| 49 |
+
else:
|
| 50 |
+
logger.warning("⚠️ [Search Logic] No documents found for this query.")
|
| 51 |
+
return ""
|
| 52 |
+
|
| 53 |
+
except Exception as e:
|
| 54 |
+
# Log the full error if something crashes
|
| 55 |
+
logger.error(f"❌ [Search Logic] CRITICAL ERROR: {str(e)}")
|
| 56 |
+
# You might want to re-raise the error or return empty depending on your needs
|
| 57 |
+
return ""
|
| 58 |
|
| 59 |
@router.get("/search_docs")
|
| 60 |
async def search_documents(
|
Backend/app/llm.py
CHANGED
|
@@ -37,36 +37,61 @@ async def call_llm(prompt:str):
|
|
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
system_instruction = {
|
| 42 |
"role": "system",
|
| 43 |
-
"content": "You are a helpful AI assistant. Answer the user's question
|
| 44 |
}
|
| 45 |
|
| 46 |
conversation_history = [msg.copy() for msg in messages]
|
| 47 |
|
| 48 |
if conversation_history and conversation_history[-1]['role'] == 'user':
|
| 49 |
last_user_msg = conversation_history[-1]
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
else:
|
| 59 |
# Fallback: If for some reason there is no user message, add one.
|
|
|
|
|
|
|
| 60 |
conversation_history.append({
|
| 61 |
"role": "user",
|
| 62 |
-
"content": f"Context:\n{
|
| 63 |
})
|
| 64 |
|
| 65 |
-
#
|
| 66 |
full_history = [system_instruction] + conversation_history
|
| 67 |
|
| 68 |
try:
|
| 69 |
-
|
| 70 |
stream = await client.chat.completions.create(
|
| 71 |
model="openai/gpt-oss-20b",
|
| 72 |
messages=full_history,
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
+
from typing import List
|
| 41 |
+
|
| 42 |
+
async def stream_chat(messages: List[dict], context: str, retrieved_docs: str | None):
|
| 43 |
system_instruction = {
|
| 44 |
"role": "system",
|
| 45 |
+
"content": "You are a helpful AI assistant. Answer the user's question based on the provided context and retrieved documents."
|
| 46 |
}
|
| 47 |
|
| 48 |
conversation_history = [msg.copy() for msg in messages]
|
| 49 |
|
| 50 |
if conversation_history and conversation_history[-1]['role'] == 'user':
|
| 51 |
last_user_msg = conversation_history[-1]
|
| 52 |
+
original_question = last_user_msg['content']
|
| 53 |
+
|
| 54 |
+
# Start constructing the augmented prompt
|
| 55 |
+
augmented_content = ""
|
| 56 |
+
|
| 57 |
+
# 1. Add Manual Context
|
| 58 |
+
if context:
|
| 59 |
+
augmented_content += (
|
| 60 |
+
f"Here is the context/notes you must use:\n"
|
| 61 |
+
f"---------------------\n"
|
| 62 |
+
f"{context}\n"
|
| 63 |
+
f"---------------------\n\n"
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# 2. Add Retrieved Documents (New Logic)
|
| 67 |
+
if retrieved_docs:
|
| 68 |
+
augmented_content += (
|
| 69 |
+
f"Here is background information/retrieved documents:\n"
|
| 70 |
+
f"---------------------\n"
|
| 71 |
+
f"{retrieved_docs}\n"
|
| 72 |
+
f"---------------------\n\n"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# 3. Add the User Question
|
| 76 |
+
augmented_content += f"User Question: {original_question}"
|
| 77 |
+
|
| 78 |
+
# Update the message content
|
| 79 |
+
last_user_msg['content'] = augmented_content
|
| 80 |
+
|
| 81 |
else:
|
| 82 |
# Fallback: If for some reason there is no user message, add one.
|
| 83 |
+
# We combine context and docs here too just in case.
|
| 84 |
+
combined_context = f"{context}\n\n{retrieved_docs or ''}"
|
| 85 |
conversation_history.append({
|
| 86 |
"role": "user",
|
| 87 |
+
"content": f"Context:\n{combined_context}\n\nPlease analyze this."
|
| 88 |
})
|
| 89 |
|
| 90 |
+
# 4. Combine System + Modified User History
|
| 91 |
full_history = [system_instruction] + conversation_history
|
| 92 |
|
| 93 |
try:
|
| 94 |
+
# Ensure 'client' is initialized before this function in your code
|
| 95 |
stream = await client.chat.completions.create(
|
| 96 |
model="openai/gpt-oss-20b",
|
| 97 |
messages=full_history,
|