Spaces:
Runtime error
Runtime error
File size: 5,672 Bytes
ed216da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | """Streaming chat orchestration utilities for the frontend voicebot."""
from __future__ import annotations
import asyncio
import logging
import os
from queue import Queue
from threading import Lock, Thread
from typing import AsyncGenerator, Dict, Iterator, List, Optional
from dotenv import load_dotenv
from langfuse import Langfuse
from langfuse.decorators import langfuse_context, observe
import sys
sys.path.append(os.path.abspath('./backend'))
from models import LLMFinanceAnalyzer
from functions import MongoHybridSearch
load_dotenv(override=True)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
langfuse = Langfuse(
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
host=os.getenv("LANGFUSE_HOST"),
)
langfuse_context.configure(environment="development")
try:
llm_analyzer = LLMFinanceAnalyzer()
search_engine = MongoHybridSearch()
logger.info("Initialized LLM analyzer and Mongo hybrid search for streaming chat.")
except Exception as exc:
logger.critical("Failed to initialise backend components: %s", exc, exc_info=True)
raise
_stream_loop: Optional[asyncio.AbstractEventLoop] = None
_stream_thread: Optional[Thread] = None
_stream_loop_lock: "Lock" = Lock()
def _loop_worker(loop: asyncio.AbstractEventLoop) -> None:
asyncio.set_event_loop(loop)
loop.run_forever()
def _ensure_stream_loop() -> asyncio.AbstractEventLoop:
global _stream_loop, _stream_thread
with _stream_loop_lock:
if _stream_loop is None or _stream_loop.is_closed():
_stream_loop = asyncio.new_event_loop()
_stream_thread = Thread(target=_loop_worker, args=(_stream_loop,), daemon=True)
_stream_thread.start()
return _stream_loop
def _create_truncated_history(
full_conversation: List[Dict[str, str]],
max_assistant_length: int,
) -> List[Dict[str, str]]:
truncated = []
for msg in full_conversation:
processed = msg.copy()
if processed.get("role") == "assistant" and len(processed.get("content", "")) > max_assistant_length:
processed["content"] = processed["content"][:max_assistant_length] + "..."
truncated.append(processed)
return truncated
def _generate_pseudo_conversation(conversation: List[Dict[str, str]]) -> List[Dict[str, str]]:
pseudo = "".join(f"{msg.get('role', 'unknown')}: {msg.get('content', '')}\n" for msg in conversation)
return [{"role": "user", "content": pseudo.strip()}]
@observe()
async def _stream_chat_async(history: List[Dict[str, str]], message: str) -> AsyncGenerator[str, None]:
full_conversation = [msg.copy() for msg in history] + [{"role": "user", "content": message}]
truncated_history = _create_truncated_history(full_conversation, 300)
pseudo_conversation = _generate_pseudo_conversation(truncated_history)
rag_decision = "yes"
logger.info("RAG decision: %s", rag_decision)
if rag_decision == "yes":
query = await llm_analyzer.generate_subquery(pseudo_conversation)
if query is None:
yield "ขออภัยค่ะ ไม่สามารถวิเคราะห์คำถามเพื่อดึงข้อมูลได้"
return
retrieved_data = ""
if query:
try:
docs = await search_engine.search_documents(query)
retrieved_data = "\n-------\n".join(docs)
logger.info("Retrieved %d documents for streaming response.", len(docs))
except Exception as search_err:
logger.error("Error during document search: %s", search_err, exc_info=True)
yield "ขออภัยค่ะ เกิดข้อผิดพลาดขณะค้นหาข้อมูล"
return
limited_conversation = full_conversation[-7:] if len(full_conversation) > 7 else full_conversation
response_generator = llm_analyzer.generate_normal_response(retrieved_data, limited_conversation)
async for chunk in response_generator:
if chunk:
yield chunk
await asyncio.sleep(0.05)
else:
limited_conversation = full_conversation[-9:] if len(full_conversation) > 9 else full_conversation
final_response = await llm_analyzer.generate_non_rag_response(limited_conversation)
if final_response:
yield final_response
else:
yield "ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลคำถามของคุณ"
def stream_chat_response(history: List[Dict[str, str]], message: str) -> Iterator[str]:
"""Synchronously iterate over streaming LLM chunks."""
loop = _ensure_stream_loop()
output_queue: "Queue[Optional[str]]" = Queue()
async def runner() -> None:
try:
async for chunk in _stream_chat_async(history, message):
output_queue.put_nowait(str(chunk))
except Exception as exc: # noqa: BLE001
logger.error("Unhandled error in async chat stream: %s", exc, exc_info=True)
output_queue.put_nowait(f"[Error: {exc}]")
finally:
output_queue.put_nowait(None)
future = asyncio.run_coroutine_threadsafe(runner(), loop)
while True:
chunk = output_queue.get()
if chunk is None:
break
yield chunk
# Propagate any exception that was not handled in runner().
future.result()
__all__ = ["stream_chat_response"]
|