Skip to content

Commit 09d2965

Browse files
Merge pull request #903 from Kavurubuvanesh/main
[GSSoC'26] Feature: Add Gemini vibe-check AI endpoint
2 parents e3e0e7d + 2a022bb commit 09d2965

2 files changed

Lines changed: 193 additions & 1 deletion

File tree

‎backend/ai_service.py‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# AI service logic with LLM integration (OpenAI/Gemini)
22
# Implements 'generate_book_note' and 'get_ai_recommendations'. All recommendations MUST be AI-based.
3+
34
# Enhanced with comprehensive caching for expensive operations
45

56

@@ -219,6 +220,25 @@ def get_category_books_prompt(category: str, vibe_description: str, count: int =
219220
- Output the JSON array only.
220221
"""
221222

223+
@staticmethod
224+
def get_vibe_check_prompt(vibe_prompt: str, count: int = 3) -> str:
225+
"""
226+
Prompt for generating hyper-personalized vibe-based book recommendations.
227+
"""
228+
return f"""You are Elara, an emotionally intuitive bookseller.
229+
A reader is looking for books matching this exact hyper-specific vibe or mood: "{vibe_prompt}"
230+
231+
Return exactly {count} real, verifiable books that perfectly capture this energy.
232+
Output ONLY a valid JSON array. No markdown fences. No text before or after.
233+
Schema:
234+
[
235+
{{
236+
"title": "Exact book title",
237+
"author": "Author full name",
238+
"reason": "One brilliant, atmospheric sentence explaining why it fits the requested vibe perfectly."
239+
}}
240+
]
241+
"""
222242

223243
class LLMService:
224244
"""
@@ -779,6 +799,46 @@ def get_category_books(category: str, vibe_description: str, count: int = 5) ->
779799
logger.info("get_category_books: %d books returned for '%s'", len(valid_books), category)
780800
return valid_books
781801

802+
def get_vibe_recommendations(vibe_prompt: str, count: int = 3) -> list:
803+
"""
804+
Generate a list of real books based on a highly specific natural-language vibe.
805+
Returns a list of dicts: [{"title": ..., "author": ..., "reason": ...}, ...]
806+
"""
807+
if not llm_service.is_available():
808+
logger.warning("get_vibe_recommendations: no LLM configured")
809+
return []
810+
811+
prompt = PromptTemplates.get_vibe_check_prompt(vibe_prompt, count)
812+
813+
# Utilizing the existing LLMService pipeline
814+
raw = llm_service.generate_text(
815+
prompt,
816+
max_tokens=llm_service.config.get('category_books_max_tokens', 600),
817+
)
818+
819+
if not raw:
820+
logger.error("get_vibe_recommendations: LLM returned None")
821+
return []
822+
823+
# Use the file's existing robust JSON extractor
824+
parsed = _extract_json(raw)
825+
826+
if not isinstance(parsed, list):
827+
logger.error("get_vibe_recommendations: expected JSON array")
828+
return []
829+
830+
# Validate and clean the output
831+
valid_books = []
832+
for item in parsed:
833+
if isinstance(item, dict) and "title" in item and "author" in item:
834+
valid_books.append({
835+
"title": item["title"],
836+
"author": item["author"],
837+
"reason": item.get("reason", "This perfectly matches your requested vibe."),
838+
})
839+
840+
logger.info("get_vibe_recommendations: %d books returned for vibe '%s'", len(valid_books), vibe_prompt)
841+
return valid_books
782842

783843
@cache_mood_tags
784844
def get_book_mood_tags_safe(title: str, author: str = "") -> list:
@@ -902,3 +962,96 @@ def generate_chat_response(user_message: str, conversation_history: list = []) -
902962
]
903963
import random
904964
return random.choice(variations)
965+
966+
# =========================================================================
967+
# VIBE-CHECK ENDPOINT LOGIC
968+
# Implements strict validation and caching per PR #903 feedback.
969+
# =========================================================================
970+
971+
import hashlib
972+
from cache_service import cache_service
973+
974+
def generate_vibe_check(raw_vibe_prompt: str, raw_count: int = 5) -> Optional[dict]:
975+
"""
976+
Generate an AI vibe check with strict server-side validation and caching.
977+
978+
Args:
979+
raw_vibe_prompt: The raw emotional/vibe query from the user.
980+
raw_count: The requested number of book recommendations.
981+
982+
Returns:
983+
Dict containing the AI response and recommendations, or None on failure.
984+
"""
985+
986+
# ---------------------------------------------------------
987+
# 1. VALIDATION: Clamp the Count (Security & Cost Control)
988+
# ---------------------------------------------------------
989+
try:
990+
count = int(raw_count)
991+
if count < 1:
992+
count = 1
993+
elif count > 10: # Hard limit to prevent API abuse
994+
count = 10
995+
except (ValueError, TypeError):
996+
count = 5 # Safe default
997+
998+
# ---------------------------------------------------------
999+
# 2. VALIDATION: Vibe Prompt Length (Prompt Injection Prevention)
1000+
# ---------------------------------------------------------
1001+
if not raw_vibe_prompt or not isinstance(raw_vibe_prompt, str):
1002+
logger.warning("generate_vibe_check: Empty or invalid prompt received.")
1003+
return None
1004+
1005+
safe_vibe_prompt = raw_vibe_prompt.strip()
1006+
1007+
# Restrict prompt to 400 characters to prevent huge payload bills
1008+
if len(safe_vibe_prompt) > 400:
1009+
logger.warning(f"generate_vibe_check: Prompt truncated from {len(safe_vibe_prompt)} chars.")
1010+
safe_vibe_prompt = safe_vibe_prompt[:400]
1011+
1012+
# ---------------------------------------------------------
1013+
# 3. CACHING: Prevent Expensive Redundant LLM Calls
1014+
# ---------------------------------------------------------
1015+
# Create a deterministic hash of the sanitized inputs
1016+
prompt_hash = hashlib.md5(f"{safe_vibe_prompt}_{count}".encode('utf-8')).hexdigest()
1017+
cache_key = f"vibe_check_{prompt_hash}"
1018+
1019+
cached_result = cache_service.get(cache_key)
1020+
if cached_result:
1021+
logger.info(f"generate_vibe_check: Serving cached response for key {cache_key}")
1022+
return cached_result
1023+
1024+
# ---------------------------------------------------------
1025+
# 4. LLM GENERATION
1026+
# ---------------------------------------------------------
1027+
if not llm_service.is_available():
1028+
logger.error("generate_vibe_check: No LLM service available.")
1029+
return None
1030+
1031+
# Construct a specific prompt for the vibe check
1032+
system_prompt = (
1033+
f"You are a literary vibe-checker. The user has described their current mood: '{safe_vibe_prompt}'. "
1034+
f"Give them a short, poetic reading diagnosis (under 50 words) and recommend exactly {count} books "
1035+
"that perfectly match this specific emotional frequency. Format your response clearly."
1036+
)
1037+
1038+
try:
1039+
response_text = llm_service.generate_text(
1040+
prompt=system_prompt,
1041+
max_tokens=llm_service.config.get('recommendation_max_tokens', 250)
1042+
)
1043+
1044+
if response_text:
1045+
result = {
1046+
"vibe_diagnosis": response_text,
1047+
"sanitized_query": safe_vibe_prompt,
1048+
"count_returned": count
1049+
}
1050+
1051+
# Cache the successful result for 24 hours
1052+
cache_service.set(cache_key, result, timeout=86400)
1053+
return result
1054+
1055+
except Exception as e:
1056+
logger.error(f"generate_vibe_check failed: {str(e)}", exc_info=True)
1057+
return None

‎backend/app.py‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242

4343
# Environment variables are now loaded centrally in backend/config.py
4444
from config import app_config, setup_logging, validate_required_env_vars
45-
from ai_service import generate_book_note, get_ai_recommendations, get_category_books, get_book_mood_tags_safe, generate_chat_response, llm_service
45+
from ai_service import generate_book_note, get_ai_recommendations, get_category_books, get_book_mood_tags_safe, generate_chat_response, llm_service, get_vibe_recommendations
4646
from models import db, User, Book, ShelfItem, BookNote, ReadingGoal, ReadingStats, Collection, CollectionItem, PriceHistory, PriceAlert, Review, register_user, login_user
4747
from price_tracker import get_price_tracker
4848
from cache_service import cache_service
@@ -912,6 +912,45 @@ def handle_mood_search():
912912
return internal_error(str(e))
913913

914914

915+
@app.route('/api/v1/vibe-check', methods=['POST'])
916+
@rate_limit('vibe_check')
917+
def handle_vibe_check():
918+
"""
919+
Generate hyper-personalized book recommendations based on a specific vibe.
920+
"""
921+
try:
922+
data = request.get_json()
923+
924+
# Safely validate input
925+
if not data or 'vibe_prompt' not in data:
926+
return missing_fields_error(["vibe_prompt"])
927+
928+
vibe_prompt = data['vibe_prompt']
929+
count = data.get('count', 3)
930+
931+
# Call the Gemini pipeline
932+
books = get_vibe_recommendations(
933+
vibe_prompt=vibe_prompt,
934+
count=count
935+
)
936+
937+
if not books:
938+
return service_unavailable_error(
939+
"Could not generate vibe recommendations right now. Please try again shortly."
940+
)
941+
942+
return success_response(
943+
data={
944+
"vibe_prompt": vibe_prompt,
945+
"books": books,
946+
}
947+
)
948+
949+
except Exception as e:
950+
logger.error(f"Error in handle_vibe_check: {str(e)}", exc_info=True)
951+
return internal_error(str(e))
952+
953+
915954
@app.route('/api/v1/category-books', methods=['POST'])
916955
@limiter.limit("10 per minute")
917956
def handle_category_books():

0 commit comments

Comments
 (0)