@@ -508,6 +508,113 @@ def _build_flat_prompt(system: str, msgs: list) -> str:
508508 logger .error (f"generate_chat failed: { type (e ).__name__ } : { e } " , exc_info = True )
509509
510510 return None
511+
512+ def generate_chat_stream (self , system_prompt : str , messages : list , max_tokens : Optional [int ] = None ):
513+ """Yield chunks of text from the LLM for streaming."""
514+ if not self .is_available ():
515+ yield "The candles are flickering and my connection to the ether seems troubled today. "
516+ return
517+
518+ if max_tokens is None :
519+ max_tokens = self .config .get ('gemini_max_tokens' , 600 )
520+
521+ # Per-model context window limits (conservative estimates)
522+ _MODEL_CONTEXT_LIMITS = {
523+ 'llama-3.1-8b-instant' : 8192 ,
524+ 'llama-3.3-70b-versatile' : 32768 ,
525+ 'gpt-3.5-turbo' : 4096 ,
526+ 'gpt-4' : 8192 ,
527+ 'gpt-4o' : 16384 ,
528+ 'models/gemini-2.0-flash-lite' : 32768 ,
529+ 'models/gemini-1.5-flash' : 32768 ,
530+ }
531+
532+ def _get_context_limit (model_name : str ) -> int :
533+ return _MODEL_CONTEXT_LIMITS .get (model_name , 4096 )
534+
535+ try :
536+ # --- Gemini ---
537+ if self .gemini_client and (self .preferred_llm == 'gemini' or not self .groq_client ):
538+ try :
539+ from google .genai import types
540+ context_limit = _get_context_limit (self .config ['gemini_model' ])
541+ trimmed = self .trim_history_to_token_budget (
542+ system_prompt , messages , max_tokens , context_limit
543+ )
544+
545+ flat_prompt = system_prompt + "\n \n "
546+ for m in trimmed :
547+ role = "Elara" if m .get ("role" ) == "assistant" else "Customer"
548+ flat_prompt += f"{ role } : { m .get ('content' , '' )} \n "
549+ flat_prompt += "Elara:"
550+
551+ response = self .gemini_client .models .generate_content_stream (
552+ model = self .config ['gemini_model' ],
553+ contents = flat_prompt ,
554+ )
555+ for chunk in response :
556+ if chunk .text :
557+ yield chunk .text
558+ return
559+ except Exception as e :
560+ logger .warning (f"Gemini streaming chat failed, falling back: { e } " )
561+
562+ # --- Groq ---
563+ if self .groq_client :
564+ try :
565+ context_limit = _get_context_limit (self .config ['groq_model' ])
566+ trimmed = self .trim_history_to_token_budget (
567+ system_prompt , messages , max_tokens , context_limit
568+ )
569+
570+ groq_messages = [{"role" : "system" , "content" : system_prompt }] + [
571+ {"role" : m .get ("role" , "user" ), "content" : m .get ("content" , "" )}
572+ for m in trimmed
573+ ]
574+ response = self .groq_client .chat .completions .create (
575+ model = self .config ['groq_model' ],
576+ messages = groq_messages ,
577+ max_tokens = min (max_tokens , self .config ['groq_max_tokens' ]),
578+ temperature = self .config ['groq_temperature' ],
579+ stream = True
580+ )
581+ for chunk in response :
582+ content = chunk .choices [0 ].delta .content
583+ if content :
584+ yield content
585+ return
586+ except Exception as e :
587+ logger .warning (f"Groq streaming chat failed, falling back: { e } " )
588+
589+ # --- OpenAI ---
590+ if self .openai_client :
591+ from openai import OpenAI
592+ client = OpenAI (api_key = os .getenv ('OPENAI_API_KEY' ))
593+ context_limit = _get_context_limit (self .config ['openai_model' ])
594+ trimmed = self .trim_history_to_token_budget (
595+ system_prompt , messages , max_tokens , context_limit
596+ )
597+
598+ oai_messages = [{"role" : "system" , "content" : system_prompt }] + [
599+ {"role" : m .get ("role" , "user" ), "content" : m .get ("content" , "" )}
600+ for m in trimmed
601+ ]
602+ response = client .chat .completions .create (
603+ model = self .config ['openai_model' ],
604+ messages = oai_messages ,
605+ max_tokens = min (max_tokens , self .config ['openai_max_tokens' ]),
606+ temperature = self .config ['openai_temperature' ],
607+ stream = True
608+ )
609+ for chunk in response :
610+ content = chunk .choices [0 ].delta .content
611+ if content :
612+ yield content
613+ return
614+
615+ except Exception as e :
616+ logger .error (f"generate_chat_stream failed: { type (e ).__name__ } : { e } " , exc_info = True )
617+ yield "An error occurred while peering into the books. Please ask again."
511618
512619 def generate_text (self , prompt : str , max_tokens : Optional [int ] = None , retry_count : int = 0 ) -> Optional [str ]:
513620 """Generate text using available LLM service with retry logic."""
@@ -653,7 +760,7 @@ def _generate_with_gemini(self, prompt: str, max_tokens: int) -> Optional[str]:
653760llm_service = LLMService ()
654761
655762__all__ = ['generate_book_note' , 'get_ai_recommendations' , 'get_category_books' ,
656- 'get_book_mood_tags_safe' , 'generate_chat_response' , 'llm_service' ,
763+ 'get_book_mood_tags_safe' , 'generate_chat_response' , 'generate_chat_response_stream' , ' llm_service' ,
657764 'LLMService' , 'PromptTemplates' ]
658765
659766
@@ -944,6 +1051,40 @@ def generate_chat_response(user_message: str, conversation_history: list = []) -
9441051 # SMART FALLBACK: A variety of poetic responses that adapt to keywords
9451052 msg_lower = user_message .lower ()
9461053
1054+ if any (k in msg_lower for k in ['rain' , 'melancholy' , 'sad' , 'quiet' ]):
1055+ return reply or "The spirits are quiet... Let's talk again when they wake."
1056+
1057+ def generate_chat_response_stream (user_message : str , conversation_history : list = []):
1058+ """
1059+ Yield an emotionally rich, persona-driven chat response from Elara.
1060+ """
1061+ if not llm_service .is_available ():
1062+ logger .warning ("generate_chat_response_stream: No LLM available" )
1063+ yield "The candles are flickering and my connection to the ether seems troubled today. "
1064+ yield "Come back in a moment — the books are waiting, and so am I."
1065+ return
1066+
1067+ messages = []
1068+ for msg in (conversation_history or []):
1069+ role = msg .get ("type" , msg .get ("role" , "user" ))
1070+ if role in ("bookseller" , "assistant" ):
1071+ role = "assistant"
1072+ else :
1073+ role = "user"
1074+ content = msg .get ("content" , "" )
1075+ if content :
1076+ messages .append ({"role" : role , "content" : content })
1077+
1078+ messages .append ({"role" : "user" , "content" : user_message })
1079+
1080+ yield from llm_service .generate_chat_stream (
1081+ system_prompt = _WISE_BOOKSELLER_SYSTEM_PROMPT ,
1082+ messages = messages ,
1083+ )
1084+
1085+ # SMART FALLBACK: A variety of poetic responses that adapt to keywords
1086+ msg_lower = user_message .lower ()
1087+
9471088 if any (k in msg_lower for k in ['rain' , 'melancholy' , 'sad' , 'quiet' ]):
9481089 return "The rain has a way of turning the heart into a library of its own. I've gathered these quiet, thoughtful volumes for your pensive mood."
9491090 elif any (k in msg_lower for k in ['adventure' , 'journey' , 'travel' , 'exciting' ]):
0 commit comments