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
223243class 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
784844def 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
0 commit comments