|
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
from contextlib import asynccontextmanager
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
|
|
-from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, get_user_profile, get_food_by_id, get_foods_by_ids, save_user_meal, update_user_meal, delete_user_meal, get_user_meals
|
|
|
+from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, delete_user_chat_history, get_user_profile, get_food_by_id, get_foods_by_ids, save_user_meal, update_user_meal, delete_user_meal, get_user_meals
|
|
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from pydantic import BaseModel
|
|
|
@@ -22,7 +22,7 @@ async def lifespan(app: FastAPI):
|
|
|
create_tables()
|
|
|
try:
|
|
|
async with httpx.AsyncClient() as client:
|
|
|
- await client.post("http://localhost:11434/api/generate",
|
|
|
+ await client.post("http://127.0.0.1:11434/api/generate",
|
|
|
json={"model": MODEL_NAME, "prompt": "", "keep_alive": "10m"},
|
|
|
timeout=60.0)
|
|
|
logger.info(f"Modèle {MODEL_NAME} pré-chargé en mémoire")
|
|
|
@@ -96,7 +96,7 @@ def extract_food_context(messages: list) -> str | None:
|
|
|
if not keywords:
|
|
|
return None
|
|
|
|
|
|
- recipe_keywords = ["recipe", "recette", "cook", "cuisiner", "plat", "dish", "make", "prepare"]
|
|
|
+ recipe_keywords = ["recipe", "recette", "cook", "cuisiner", "plat", "dish", "make", "prepare", "meal", "repas", "menu"]
|
|
|
is_recipe_request = any(kw in last_user_msg.lower() for kw in recipe_keywords)
|
|
|
|
|
|
# Try each keyword against the local food database, collect unique results
|
|
|
@@ -129,15 +129,17 @@ def extract_food_context(messages: list) -> str | None:
|
|
|
"2. **Total nutritional values of the dish** : a markdown table with Cal, Protein, Fat, Carbs, Fiber.",
|
|
|
"3. **Recipe** : simple and clear steps.",
|
|
|
"4. **Suggestions** : 0 to 2 tips such as 'if you add X, you will get more Y' or 'you can also cook X this way'.",
|
|
|
+ "CRITICAL: Do NOT list the nutritional values of each individual ingredient! You MUST ONLY provide the TOTAL combined nutritional values for the entire meal.",
|
|
|
"Use ONLY the nutritional data provided below to calculate the final values.",
|
|
|
]
|
|
|
else:
|
|
|
lines += [
|
|
|
- "For each food discussed, you MUST follow this structure:",
|
|
|
+ "STRICT INSTRUCTION: The user is asking for nutritional data. You MUST ONLY return nutritional data.",
|
|
|
+ "DO NOT include any recipes, cooking instructions, meal ideas, or extra commentary. This is strictly forbidden.",
|
|
|
+ "For each food discussed, you MUST follow this exact structure and provide NOTHING ELSE:",
|
|
|
"1. Header: ### 🥗 [Name] (per 100g)",
|
|
|
"2. Macros: A markdown table for Cal, P, F, C, Fib, Sug, Chol.",
|
|
|
"3. Micros: A bulleted list for Na, Ca, Fe, K, VitA, VitC.",
|
|
|
- "4. Insight: A 1-sentence analysis of the food's nutritional profile.",
|
|
|
]
|
|
|
|
|
|
lines += [
|
|
|
@@ -181,6 +183,7 @@ class MealSaveRequest(BaseModel):
|
|
|
|
|
|
class MealUpdateRequest(BaseModel):
|
|
|
name: str
|
|
|
+ items: Optional[List[MealItemInput]] = None
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
async def read_root():
|
|
|
@@ -254,7 +257,7 @@ async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_c
|
|
|
"""
|
|
|
# Keep only the last 6 messages for context window performance on CPU
|
|
|
all_messages = [msg.model_dump() for msg in request.messages]
|
|
|
- messages = all_messages[-6:]
|
|
|
+ messages = all_messages[-3:]
|
|
|
|
|
|
if messages and messages[-1]['role'] == 'user':
|
|
|
asyncio.get_event_loop().run_in_executor(
|
|
|
@@ -294,27 +297,53 @@ async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_c
|
|
|
yield f"data: {json.dumps({'error': f'LLM Error ({response.status_code})'})}\n\n"
|
|
|
return
|
|
|
|
|
|
+ bot_full_response = ""
|
|
|
+ started_thinking = False
|
|
|
+
|
|
|
async for line in response.aiter_lines():
|
|
|
if line:
|
|
|
try:
|
|
|
data = json.loads(line)
|
|
|
- if "message" in data and "content" in data["message"]:
|
|
|
- content = data["message"]["content"]
|
|
|
- bot_full_response += content
|
|
|
- yield f"data: {json.dumps({'content': content})}\n\n"
|
|
|
+ msg = data.get("message", {}).get("content", "")
|
|
|
+ thinking = data.get("message", {}).get("thinking", "")
|
|
|
+
|
|
|
+ chunk = ""
|
|
|
+ # Stream the thinking process as gray italic text
|
|
|
+ if thinking:
|
|
|
+ if not started_thinking:
|
|
|
+ chunk += "<div style='color: #888; font-style: italic; font-size: 0.9em; padding-left: 10px; border-left: 3px solid #ccc; margin-bottom: 10px;'>"
|
|
|
+ started_thinking = True
|
|
|
+
|
|
|
+ # Replace newlines with <br> for HTML streaming
|
|
|
+ chunk += thinking.replace('\n', '<br>')
|
|
|
+
|
|
|
+ if msg:
|
|
|
+ # Close the thinking div once the actual message starts
|
|
|
+ if started_thinking and not bot_full_response:
|
|
|
+ chunk += "</div><br>"
|
|
|
+ started_thinking = False
|
|
|
+
|
|
|
+ chunk += msg
|
|
|
+ bot_full_response += msg
|
|
|
+
|
|
|
+ if chunk:
|
|
|
+ yield f"data: {json.dumps({'content': chunk, 'is_thinking': started_thinking})}\n\n"
|
|
|
+
|
|
|
if data.get("done"):
|
|
|
break
|
|
|
except json.JSONDecodeError:
|
|
|
continue
|
|
|
+
|
|
|
+ yield "data: [DONE]\n\n"
|
|
|
|
|
|
+ except Exception as e:
|
|
|
+ logger.exception(f"Unexpected error during chat stream: {e}")
|
|
|
+ yield f"data: {json.dumps({'error': 'A technical error occurred while generating the response.'})}\n\n"
|
|
|
+ finally:
|
|
|
if bot_full_response.strip():
|
|
|
asyncio.get_event_loop().run_in_executor(
|
|
|
executor, save_chat_message, current_user['id'], 'assistant', bot_full_response
|
|
|
)
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- logger.exception(f"Unexpected error during chat stream: {e}")
|
|
|
- yield f"data: {json.dumps({'error': 'A technical error occurred while generating the response.'})}\n\n"
|
|
|
|
|
|
return StreamingResponse(generate_response(), media_type="text/event-stream")
|
|
|
|
|
|
@@ -324,6 +353,14 @@ async def get_history(current_user: dict = Depends(get_current_user)):
|
|
|
history = get_user_chat_history(current_user['id'])
|
|
|
return {"history": history}
|
|
|
|
|
|
+@app.delete("/api/chat/history")
|
|
|
+async def delete_history(current_user: dict = Depends(get_current_user)):
|
|
|
+ """Clear the chat history for the authenticated user"""
|
|
|
+ success = delete_user_chat_history(current_user['id'])
|
|
|
+ if not success:
|
|
|
+ raise HTTPException(status_code=500, detail="Failed to delete chat history")
|
|
|
+ return {"status": "success", "message": "Chat history cleared"}
|
|
|
+
|
|
|
@app.get("/api/food/search")
|
|
|
async def search_food(q: str, current_user: dict = Depends(get_current_user)):
|
|
|
"""API endpoint to search for food items securely using token authentication"""
|
|
|
@@ -470,15 +507,19 @@ async def save_meal(request: MealSaveRequest, current_user: dict = Depends(get_c
|
|
|
|
|
|
@app.put("/api/meals/{meal_id}")
|
|
|
async def rename_meal(meal_id: int, request: MealUpdateRequest, current_user: dict = Depends(get_current_user)):
|
|
|
- """Securely rename a saved meal for the authenticated user"""
|
|
|
+ """Securely update a saved meal for the authenticated user"""
|
|
|
if not request.name.strip():
|
|
|
raise HTTPException(status_code=400, detail="Meal name cannot be empty")
|
|
|
|
|
|
- success = update_user_meal(current_user['id'], meal_id, request.name.strip())
|
|
|
+ items_list = None
|
|
|
+ if request.items is not None:
|
|
|
+ items_list = [item.model_dump() for item in request.items]
|
|
|
+
|
|
|
+ success = update_user_meal(current_user['id'], meal_id, request.name.strip(), items_list)
|
|
|
if not success:
|
|
|
raise HTTPException(status_code=404, detail="Meal not found or unauthorized")
|
|
|
|
|
|
- return {"status": "success", "message": "Meal renamed"}
|
|
|
+ return {"status": "success", "message": "Meal updated"}
|
|
|
|
|
|
@app.delete("/api/meals/{meal_id}")
|
|
|
async def delete_meal(meal_id: int, current_user: dict = Depends(get_current_user)):
|
|
|
@@ -491,4 +532,4 @@ async def delete_meal(meal_id: int, current_user: dict = Depends(get_current_use
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
import uvicorn
|
|
|
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
|
|
+ uvicorn.run("main:app", host="0.0.0.0", port=8000)
|