Spaces:
Running
Running
dev
#1
by
jts-ai-team
- opened
- app.py +79 -271
- backend/asr.py +34 -122
- backend/functions.py +1 -3
- backend/main.py +153 -21
- backend/models.py +16 -131
- backend/systemprompt.py +22 -51
- backend/tools.py +0 -446
- backend/tts.py +1 -3
- backend/utils.py +26 -133
app.py
CHANGED
|
@@ -8,15 +8,13 @@ import numpy as np
|
|
| 8 |
import sys
|
| 9 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 10 |
from backend.tts import synthesize_text
|
| 11 |
-
from backend.asr import transcribe_audio
|
| 12 |
-
from backend.utils import preprocess_audio, is_valid_turn
|
| 13 |
from backend.main import stream_chat_response
|
| 14 |
-
|
| 15 |
from pydub import AudioSegment
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
if get_device() == "mps":
|
| 19 |
-
load_dotenv(override=True)
|
| 20 |
|
| 21 |
|
| 22 |
phone_waiting_sound = AudioSegment.from_mp3("frontend/phone-ringing-382734.mp3")[:1000]
|
|
@@ -51,172 +49,25 @@ h1 {
|
|
| 51 |
max-width: 350px !important; /* Limit the width of the phone column */
|
| 52 |
margin: 0 auto; /* Center the column */
|
| 53 |
border-radius: 20px;
|
| 54 |
-
background-color: #
|
| 55 |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
| 56 |
padding: 20px;
|
| 57 |
}
|
| 58 |
/* Conversation history box styling */
|
| 59 |
#conversation-history-chatbot {
|
| 60 |
-
background-color: #
|
| 61 |
border: 1px solid #ccc;
|
| 62 |
border-radius: 10px;
|
| 63 |
padding: 10px;
|
| 64 |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
| 65 |
}
|
| 66 |
"""
|
| 67 |
-
def format_history_for_chatbot(history):
|
| 68 |
-
"""
|
| 69 |
-
Formats the conversation history for display in the Gradio Chatbot.
|
| 70 |
-
It creates readable strings for tool calls and tool results.
|
| 71 |
-
"""
|
| 72 |
-
formatted_history = []
|
| 73 |
-
if not history:
|
| 74 |
-
return []
|
| 75 |
-
for turn in history:
|
| 76 |
-
role = turn.get("role")
|
| 77 |
-
content = turn.get("content")
|
| 78 |
-
tool_calls = turn.get("tool_calls")
|
| 79 |
-
|
| 80 |
-
if role == "user":
|
| 81 |
-
formatted_history.append({"role": "user", "content": content})
|
| 82 |
-
elif role == "assistant":
|
| 83 |
-
if tool_calls:
|
| 84 |
-
# Display a user-friendly message for the tool call
|
| 85 |
-
id = tool_calls[0]['id']
|
| 86 |
-
func_name = tool_calls[0]['function']['name']
|
| 87 |
-
func_args = tool_calls[0]['function']['arguments']
|
| 88 |
-
display_content = f"<id>{id}</id><func_name>{func_name}</func_name><func_args>{func_args}</func_args>"
|
| 89 |
-
# display_content = (
|
| 90 |
-
# f"**Calling Tool:**\n"
|
| 91 |
-
# f"```json\n"
|
| 92 |
-
# f"{{\n"
|
| 93 |
-
# f' "name": "{func_name}",\n'
|
| 94 |
-
# f' "arguments": {func_args}\n'
|
| 95 |
-
# f"}}\n"
|
| 96 |
-
# f"```"
|
| 97 |
-
# )
|
| 98 |
-
formatted_history.append({"role": "assistant", "content": display_content})
|
| 99 |
-
else:
|
| 100 |
-
# Regular assistant message
|
| 101 |
-
formatted_history.append({"role": "assistant", "content": content})
|
| 102 |
-
elif role == "tool":
|
| 103 |
-
# Display a user-friendly message for the tool result
|
| 104 |
-
id = turn.get("tool_call_id")
|
| 105 |
-
result_content = json.dumps(json.loads(content), indent=2, ensure_ascii=False)
|
| 106 |
-
display_content = f"<id>{id}</id><content>{content}</content>"
|
| 107 |
-
# display_content = (
|
| 108 |
-
# f"**Tool Result:**\n"
|
| 109 |
-
# f"```json\n"
|
| 110 |
-
# f"{result_content}\n"
|
| 111 |
-
# f"```"
|
| 112 |
-
# )
|
| 113 |
-
# Represent tool results as if the "assistant" is providing them
|
| 114 |
-
formatted_history.append({"role": "assistant", "content": display_content})
|
| 115 |
-
|
| 116 |
-
return formatted_history
|
| 117 |
-
import re
|
| 118 |
-
def revert_to_openai_format(formatted_history):
|
| 119 |
-
"""
|
| 120 |
-
Converts a history list formatted for the Gradio Chatbot UI back into
|
| 121 |
-
the standard OpenAI API format. It parses custom string formats for
|
| 122 |
-
tool calls and tool results.
|
| 123 |
-
|
| 124 |
-
Args:
|
| 125 |
-
formatted_history (list): A list of message dictionaries as they appear
|
| 126 |
-
in the Gradio Chatbot component.
|
| 127 |
-
|
| 128 |
-
Returns:
|
| 129 |
-
list: A list of message dictionaries compliant with the OpenAI API format.
|
| 130 |
-
"""
|
| 131 |
-
openai_history = []
|
| 132 |
-
|
| 133 |
-
# Pre-compile regex patterns for efficiency
|
| 134 |
-
# Pattern to find a tool call message
|
| 135 |
-
tool_call_pattern = re.compile(
|
| 136 |
-
r"<id>(.*?)</id><func_name>(.*?)</func_name><func_args>(.*?)</func_args>",
|
| 137 |
-
re.DOTALL # Use DOTALL in case arguments contain newlines
|
| 138 |
-
)
|
| 139 |
-
# Pattern to find a tool result message
|
| 140 |
-
tool_result_pattern = re.compile(
|
| 141 |
-
r"<id>(.*?)</id><content>(.*?)</content>",
|
| 142 |
-
re.DOTALL
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
if not formatted_history:
|
| 146 |
-
return []
|
| 147 |
-
|
| 148 |
-
for turn in formatted_history:
|
| 149 |
-
role = turn.get("role")
|
| 150 |
-
content = turn.get("content")
|
| 151 |
-
# If content is None, treat it as an empty string for the regex search.
|
| 152 |
-
if content is None:
|
| 153 |
-
content = ""
|
| 154 |
-
|
| 155 |
-
if role == "user":
|
| 156 |
-
openai_history.append(turn)
|
| 157 |
-
continue
|
| 158 |
-
|
| 159 |
-
if role == "assistant":
|
| 160 |
-
# Check if this is a formatted tool call
|
| 161 |
-
tool_call_match = tool_call_pattern.search(content)
|
| 162 |
-
if tool_call_match:
|
| 163 |
-
call_id, func_name, func_args_str = tool_call_match.groups()
|
| 164 |
-
|
| 165 |
-
# Reconstruct the original tool_calls structure
|
| 166 |
-
reverted_turn = {
|
| 167 |
-
"role": "assistant",
|
| 168 |
-
"content": None,
|
| 169 |
-
"tool_calls": [
|
| 170 |
-
{
|
| 171 |
-
"id": call_id,
|
| 172 |
-
"type": "function",
|
| 173 |
-
"function": {
|
| 174 |
-
"name": func_name,
|
| 175 |
-
"arguments": func_args_str
|
| 176 |
-
},
|
| 177 |
-
}
|
| 178 |
-
],
|
| 179 |
-
}
|
| 180 |
-
openai_history.append(reverted_turn)
|
| 181 |
-
continue
|
| 182 |
-
|
| 183 |
-
# Check if this is a formatted tool result (as per your formatter's logic)
|
| 184 |
-
tool_result_match = tool_result_pattern.search(content)
|
| 185 |
-
if tool_result_match:
|
| 186 |
-
tool_call_id, tool_content = tool_result_match.groups()
|
| 187 |
-
|
| 188 |
-
# Reconstruct the original tool message
|
| 189 |
-
# NOTE: The role must be 'tool' for the API
|
| 190 |
-
reverted_turn = {
|
| 191 |
-
"role": "tool",
|
| 192 |
-
"tool_call_id": tool_call_id,
|
| 193 |
-
"content": tool_content.strip() # Remove trailing space
|
| 194 |
-
}
|
| 195 |
-
openai_history.append(reverted_turn)
|
| 196 |
-
continue
|
| 197 |
-
|
| 198 |
-
# If no patterns match, it's a regular assistant message
|
| 199 |
-
if turn.get("content") is not None:
|
| 200 |
-
openai_history.append(turn)
|
| 201 |
-
|
| 202 |
-
return openai_history
|
| 203 |
-
|
| 204 |
-
|
| 205 |
def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
| 206 |
"""
|
| 207 |
Handles user audio input, transcribes it, streams LLM text via backend.main,
|
| 208 |
and synthesizes chunks to audio while updating the conversation history.
|
| 209 |
"""
|
| 210 |
-
print(f"WebRTC input SR: {audio[0]}")
|
| 211 |
-
|
| 212 |
print(f"--- Latency Breakdown ---")
|
| 213 |
-
# # stage_asr = "normal" #["normal,"gemini"]
|
| 214 |
-
# print('-----------------------------')
|
| 215 |
-
# print(f"Initial conver:{conversation_history}")
|
| 216 |
-
# print('-----------------------------')
|
| 217 |
-
|
| 218 |
-
conversation_history = revert_to_openai_format(conversation_history)
|
| 219 |
-
# print(f"After convert:{conversation_history}")
|
| 220 |
start_time = time.time()
|
| 221 |
if conversation_history is None:
|
| 222 |
conversation_history = []
|
|
@@ -232,34 +83,14 @@ def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
|
| 232 |
sample_rate, audio_array = audio
|
| 233 |
|
| 234 |
try:
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
print(sr, processed_audio.dtype, processed_audio.min(), processed_audio.max(), processed_audio.shape)
|
| 238 |
-
|
| 239 |
except Exception as audio_err:
|
| 240 |
print(f"Audio preprocessing failed: {audio_err}")
|
| 241 |
print(f"------------------------")
|
| 242 |
return
|
| 243 |
-
|
| 244 |
-
silence_duration_s = 0.2
|
| 245 |
-
|
| 246 |
-
# Calculate the number of samples corresponding to the silence duration
|
| 247 |
-
silence_samples = int(16000 * silence_duration_s)
|
| 248 |
-
|
| 249 |
-
# Create a silent audio segment (an array of zeros)
|
| 250 |
-
# Ensure the dtype matches your processed audio for compatibility
|
| 251 |
-
leading_silence = np.zeros(silence_samples, dtype=np.float32)
|
| 252 |
-
|
| 253 |
-
# Prepend the silence to the beginning of your processed audio
|
| 254 |
-
audio_with_padding = np.concatenate([leading_silence, processed_audio])
|
| 255 |
-
|
| 256 |
-
print(f"Added {silence_duration_s}s of silence. New shape: {audio_with_padding.shape}")
|
| 257 |
-
|
| 258 |
-
file_name = "temp.wav"
|
| 259 |
-
sf.write(file_name, audio_with_padding, sr)
|
| 260 |
t0 = time.time()
|
| 261 |
-
transcription =
|
| 262 |
-
# transcription = transcribe_audio( "debug_processed.wav")
|
| 263 |
t_asr = time.time() - t0
|
| 264 |
print(f"ASR: {t_asr:.4f}s")
|
| 265 |
|
|
@@ -267,108 +98,86 @@ def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
|
| 267 |
print("No valid transcription; skipping response generation.")
|
| 268 |
print(f"------------------------")
|
| 269 |
return
|
|
|
|
| 270 |
user_turn = {"role": "user", "content": transcription}
|
| 271 |
print(f"User: {transcription}")
|
| 272 |
if is_valid_turn(user_turn):
|
| 273 |
conversation_history.append(user_turn)
|
| 274 |
-
yield AdditionalOutputs(
|
| 275 |
|
| 276 |
-
|
| 277 |
|
| 278 |
assistant_turn = {"role": "assistant", "content": ""}
|
| 279 |
conversation_history.append(assistant_turn)
|
| 280 |
-
|
| 281 |
-
# print(previous_history)
|
| 282 |
history_for_stream = [dict(turn) for turn in previous_history if is_valid_turn(turn)]
|
| 283 |
-
# print(f"history_for_stream{history_for_stream}")
|
| 284 |
|
| 285 |
text_buffer = ""
|
| 286 |
full_response = ""
|
| 287 |
delimiter_count = 0
|
| 288 |
-
n_threshold =
|
| 289 |
-
max_n_threshold =
|
| 290 |
lang = "th"
|
| 291 |
chunk_count = 0
|
| 292 |
first_chunk_sent = False
|
| 293 |
start_llm_stream = time.time()
|
| 294 |
|
| 295 |
try:
|
| 296 |
-
for
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
if
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
first_llm_chunk_time = time.time()
|
| 352 |
-
t_llm_first_token = first_llm_chunk_time - start_llm_stream
|
| 353 |
-
print(f"LLM TTFC: {t_llm_first_token:.4f}s (Time To First Chunk)")
|
| 354 |
-
yield from synthesize_text(buffer_to_send, lang=lang)
|
| 355 |
-
first_chunk_sent = True
|
| 356 |
-
text_buffer = ""
|
| 357 |
-
delimiter_count = 0
|
| 358 |
-
yield AdditionalOutputs(format_history_for_chatbot(conversation_history))
|
| 359 |
-
|
| 360 |
-
i += 1
|
| 361 |
-
elif isinstance(chunk, dict) and "role" in chunk:
|
| 362 |
-
# print(f"Received tool message for history: {chunk}")
|
| 363 |
-
|
| 364 |
-
if chunk.get("content") is None:
|
| 365 |
-
chunk["content"] = ""
|
| 366 |
-
conversation_history.insert(-1, chunk)
|
| 367 |
-
|
| 368 |
-
# Update the chatbot UI to reflect the new history structure
|
| 369 |
-
|
| 370 |
-
yield AdditionalOutputs(format_history_for_chatbot(conversation_history))
|
| 371 |
-
|
| 372 |
if text_buffer.strip():
|
| 373 |
buffer_to_send = text_buffer.strip()
|
| 374 |
try:
|
|
@@ -389,8 +198,7 @@ def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
|
| 389 |
first_chunk_sent = True
|
| 390 |
text_buffer = ""
|
| 391 |
delimiter_count = 0
|
| 392 |
-
|
| 393 |
-
yield AdditionalOutputs(format_history_for_chatbot(conversation_history))
|
| 394 |
|
| 395 |
except Exception as e:
|
| 396 |
print(f"An error occurred during response generation or synthesis: {e}")
|
|
@@ -400,7 +208,7 @@ def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
|
| 400 |
except Exception as synth_error:
|
| 401 |
print(f"Could not synthesize error message: {synth_error}")
|
| 402 |
assistant_turn["content"] = (assistant_turn.get("content", "") + f" [Error: {e}]").strip()
|
| 403 |
-
yield AdditionalOutputs(
|
| 404 |
|
| 405 |
total_latency = time.time() - start_time
|
| 406 |
print(f"Total: {total_latency:.4f}s")
|
|
@@ -411,7 +219,7 @@ def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
|
| 411 |
async def get_credentials():
|
| 412 |
return await get_cloudflare_turn_credentials_async(hf_token=os.getenv('HF_TOKEN'))
|
| 413 |
|
| 414 |
-
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="
|
| 415 |
gr.HTML("""<h1 style='text-align: center'>1577 Voicebot Demo</h1>""")
|
| 416 |
with gr.Row():
|
| 417 |
with gr.Column(scale=1, elem_classes=["phone-column"]):
|
|
@@ -450,13 +258,13 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="orange", second
|
|
| 450 |
fn=ReplyOnPause(
|
| 451 |
response,
|
| 452 |
algo_options=AlgoOptions(
|
| 453 |
-
audio_chunk_duration=1.
|
| 454 |
-
started_talking_threshold=0.
|
| 455 |
-
speech_threshold=0.
|
| 456 |
),
|
| 457 |
model_options=SileroVadOptions(
|
| 458 |
-
threshold=0.
|
| 459 |
-
min_speech_duration_ms=
|
| 460 |
max_speech_duration_s=float("inf"),
|
| 461 |
min_silence_duration_ms=1200,
|
| 462 |
),
|
|
|
|
| 8 |
import sys
|
| 9 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 10 |
from backend.tts import synthesize_text
|
| 11 |
+
from backend.asr import transcribe_audio
|
| 12 |
+
from backend.utils import preprocess_audio, is_valid_turn
|
| 13 |
from backend.main import stream_chat_response
|
| 14 |
+
|
| 15 |
from pydub import AudioSegment
|
| 16 |
+
|
| 17 |
+
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
phone_waiting_sound = AudioSegment.from_mp3("frontend/phone-ringing-382734.mp3")[:1000]
|
|
|
|
| 49 |
max-width: 350px !important; /* Limit the width of the phone column */
|
| 50 |
margin: 0 auto; /* Center the column */
|
| 51 |
border-radius: 20px;
|
| 52 |
+
background-color: #ff69b4; /* Lighter pink for telephone interface */
|
| 53 |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
| 54 |
padding: 20px;
|
| 55 |
}
|
| 56 |
/* Conversation history box styling */
|
| 57 |
#conversation-history-chatbot {
|
| 58 |
+
background-color: #ffc0cb; /* Lighter pink for conversation history */
|
| 59 |
border: 1px solid #ccc;
|
| 60 |
border-radius: 10px;
|
| 61 |
padding: 10px;
|
| 62 |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
| 63 |
}
|
| 64 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
def response(audio: tuple[int, np.ndarray] | None, conversation_history):
|
| 66 |
"""
|
| 67 |
Handles user audio input, transcribes it, streams LLM text via backend.main,
|
| 68 |
and synthesizes chunks to audio while updating the conversation history.
|
| 69 |
"""
|
|
|
|
|
|
|
| 70 |
print(f"--- Latency Breakdown ---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
start_time = time.time()
|
| 72 |
if conversation_history is None:
|
| 73 |
conversation_history = []
|
|
|
|
| 83 |
sample_rate, audio_array = audio
|
| 84 |
|
| 85 |
try:
|
| 86 |
+
processed_audio = preprocess_audio((sample_rate, audio_array), target_frame_rate=16000)
|
|
|
|
|
|
|
|
|
|
| 87 |
except Exception as audio_err:
|
| 88 |
print(f"Audio preprocessing failed: {audio_err}")
|
| 89 |
print(f"------------------------")
|
| 90 |
return
|
| 91 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
t0 = time.time()
|
| 93 |
+
transcription = transcribe_audio( processed_audio)
|
|
|
|
| 94 |
t_asr = time.time() - t0
|
| 95 |
print(f"ASR: {t_asr:.4f}s")
|
| 96 |
|
|
|
|
| 98 |
print("No valid transcription; skipping response generation.")
|
| 99 |
print(f"------------------------")
|
| 100 |
return
|
| 101 |
+
|
| 102 |
user_turn = {"role": "user", "content": transcription}
|
| 103 |
print(f"User: {transcription}")
|
| 104 |
if is_valid_turn(user_turn):
|
| 105 |
conversation_history.append(user_turn)
|
| 106 |
+
yield AdditionalOutputs(conversation_history)
|
| 107 |
|
| 108 |
+
print("Conversation history:", conversation_history)
|
| 109 |
|
| 110 |
assistant_turn = {"role": "assistant", "content": ""}
|
| 111 |
conversation_history.append(assistant_turn)
|
| 112 |
+
|
|
|
|
| 113 |
history_for_stream = [dict(turn) for turn in previous_history if is_valid_turn(turn)]
|
|
|
|
| 114 |
|
| 115 |
text_buffer = ""
|
| 116 |
full_response = ""
|
| 117 |
delimiter_count = 0
|
| 118 |
+
n_threshold = 5
|
| 119 |
+
max_n_threshold = 7
|
| 120 |
lang = "th"
|
| 121 |
chunk_count = 0
|
| 122 |
first_chunk_sent = False
|
| 123 |
start_llm_stream = time.time()
|
| 124 |
|
| 125 |
try:
|
| 126 |
+
for text_chunk in stream_chat_response(history_for_stream, transcription):
|
| 127 |
+
if not isinstance(text_chunk, str):
|
| 128 |
+
text_chunk = str(text_chunk)
|
| 129 |
+
|
| 130 |
+
i = 0
|
| 131 |
+
while i < len(text_chunk):
|
| 132 |
+
char = text_chunk[i]
|
| 133 |
+
text_buffer += char
|
| 134 |
+
full_response += char
|
| 135 |
+
|
| 136 |
+
assistant_turn["content"] = full_response.strip()
|
| 137 |
+
|
| 138 |
+
is_delimiter = False
|
| 139 |
+
if char in {' ', '\n'}:
|
| 140 |
+
is_delimiter = True
|
| 141 |
+
delimiter_count += 1
|
| 142 |
+
if i + 1 < len(text_chunk) and text_chunk[i + 1] == 'ๆ':
|
| 143 |
+
text_buffer += text_chunk[i + 1]
|
| 144 |
+
full_response += text_chunk[i + 1]
|
| 145 |
+
i += 1
|
| 146 |
+
|
| 147 |
+
send_now = False
|
| 148 |
+
if not first_chunk_sent:
|
| 149 |
+
if is_delimiter and text_buffer.strip():
|
| 150 |
+
send_now = True
|
| 151 |
+
else:
|
| 152 |
+
if delimiter_count >= n_threshold and text_buffer.strip():
|
| 153 |
+
send_now = True
|
| 154 |
+
if n_threshold < max_n_threshold:
|
| 155 |
+
n_threshold += 1
|
| 156 |
+
|
| 157 |
+
if send_now:
|
| 158 |
+
buffer_to_send = text_buffer.strip()
|
| 159 |
+
try:
|
| 160 |
+
if buffer_to_send and buffer_to_send.endswith('วันที่'):
|
| 161 |
+
buffer_to_send = buffer_to_send[:-len('วันที่')]
|
| 162 |
+
if buffer_to_send and first_chunk_sent and buffer_to_send.endswith('ค่ะ'):
|
| 163 |
+
buffer_to_send = buffer_to_send[:-len('ค่ะ')]
|
| 164 |
+
except Exception:
|
| 165 |
+
buffer_to_send = buffer_to_send.replace('ค่ะ', '')
|
| 166 |
+
|
| 167 |
+
if buffer_to_send:
|
| 168 |
+
chunk_count += 1
|
| 169 |
+
if chunk_count == 1:
|
| 170 |
+
first_llm_chunk_time = time.time()
|
| 171 |
+
t_llm_first_token = first_llm_chunk_time - start_llm_stream
|
| 172 |
+
print(f"LLM TTFC: {t_llm_first_token:.4f}s (Time To First Chunk)")
|
| 173 |
+
yield from synthesize_text(buffer_to_send, lang=lang)
|
| 174 |
+
first_chunk_sent = True
|
| 175 |
+
text_buffer = ""
|
| 176 |
+
delimiter_count = 0
|
| 177 |
+
yield AdditionalOutputs(conversation_history)
|
| 178 |
+
|
| 179 |
+
i += 1
|
| 180 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
if text_buffer.strip():
|
| 182 |
buffer_to_send = text_buffer.strip()
|
| 183 |
try:
|
|
|
|
| 198 |
first_chunk_sent = True
|
| 199 |
text_buffer = ""
|
| 200 |
delimiter_count = 0
|
| 201 |
+
yield AdditionalOutputs(conversation_history)
|
|
|
|
| 202 |
|
| 203 |
except Exception as e:
|
| 204 |
print(f"An error occurred during response generation or synthesis: {e}")
|
|
|
|
| 208 |
except Exception as synth_error:
|
| 209 |
print(f"Could not synthesize error message: {synth_error}")
|
| 210 |
assistant_turn["content"] = (assistant_turn.get("content", "") + f" [Error: {e}]").strip()
|
| 211 |
+
yield AdditionalOutputs(conversation_history)
|
| 212 |
|
| 213 |
total_latency = time.time() - start_time
|
| 214 |
print(f"Total: {total_latency:.4f}s")
|
|
|
|
| 219 |
async def get_credentials():
|
| 220 |
return await get_cloudflare_turn_credentials_async(hf_token=os.getenv('HF_TOKEN'))
|
| 221 |
|
| 222 |
+
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="pink", secondary_hue="pink")) as demo:
|
| 223 |
gr.HTML("""<h1 style='text-align: center'>1577 Voicebot Demo</h1>""")
|
| 224 |
with gr.Row():
|
| 225 |
with gr.Column(scale=1, elem_classes=["phone-column"]):
|
|
|
|
| 258 |
fn=ReplyOnPause(
|
| 259 |
response,
|
| 260 |
algo_options=AlgoOptions(
|
| 261 |
+
audio_chunk_duration=1.0,
|
| 262 |
+
started_talking_threshold=0.4,
|
| 263 |
+
speech_threshold=0.6
|
| 264 |
),
|
| 265 |
model_options=SileroVadOptions(
|
| 266 |
+
threshold=0.5,
|
| 267 |
+
min_speech_duration_ms=300,
|
| 268 |
max_speech_duration_s=float("inf"),
|
| 269 |
min_silence_duration_ms=1200,
|
| 270 |
),
|
backend/asr.py
CHANGED
|
@@ -1,19 +1,9 @@
|
|
| 1 |
-
"""Speech-to-text utilities with graceful fallbacks
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import io
|
| 6 |
-
import os
|
| 7 |
-
import wave
|
| 8 |
-
from threading import Lock
|
| 9 |
-
from typing import Any, Literal, Optional
|
| 10 |
import numpy as np
|
| 11 |
|
| 12 |
-
try:
|
| 13 |
-
from openai import OpenAI
|
| 14 |
-
except ModuleNotFoundError:
|
| 15 |
-
OpenAI = None # type: ignore[assignment]
|
| 16 |
-
|
| 17 |
from backend.utils import device
|
| 18 |
import nemo.collections.asr as nemo_asr
|
| 19 |
|
|
@@ -31,8 +21,6 @@ except ModuleNotFoundError:
|
|
| 31 |
|
| 32 |
|
| 33 |
_ASR_PIPELINE = None
|
| 34 |
-
_ASR_STAGE: Literal["typhoon", "gpt"] = "typhoon"
|
| 35 |
-
_ASR_STAGE_LOCK: Lock = Lock()
|
| 36 |
|
| 37 |
|
| 38 |
def _huggingface_device() -> int | str | None:
|
|
@@ -46,43 +34,32 @@ def _huggingface_device() -> int | str | None:
|
|
| 46 |
def _initialize_typhoon_pipeline():
|
| 47 |
if torch is None or pipeline is None:
|
| 48 |
return None
|
| 49 |
-
|
| 50 |
print(f"Using device: {device}")
|
| 51 |
print("Initializing Typhoon ASR pipeline...")
|
| 52 |
asr_model = nemo_asr.models.ASRModel.from_pretrained(
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
print("Typhoon ASR pipeline initialized.")
|
| 57 |
return asr_model
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
global _ASR_STAGE
|
| 76 |
-
with _ASR_STAGE_LOCK:
|
| 77 |
-
if _ASR_STAGE != normalized_stage:
|
| 78 |
-
print(f"Switching ASR stage to: {normalized_stage}")
|
| 79 |
-
_ASR_STAGE = normalized_stage # type: ignore[assignment]
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def get_asr_stage() -> Literal["typhoon", "gpt"]:
|
| 83 |
-
"""Return the current ASR stage."""
|
| 84 |
-
with _ASR_STAGE_LOCK:
|
| 85 |
-
return _ASR_STAGE
|
| 86 |
|
| 87 |
|
| 88 |
def _transcribe_with_pipeline(audio_array: np.ndarray) -> str:
|
|
@@ -94,18 +71,6 @@ def _transcribe_with_pipeline(audio_array: np.ndarray) -> str:
|
|
| 94 |
return text.replace("ทางลัด", "ทางรัฐ")
|
| 95 |
|
| 96 |
|
| 97 |
-
def _transcribe_with_typhoon(audio_array: np.ndarray) -> str:
|
| 98 |
-
if _ASR_TYPHOON is None:
|
| 99 |
-
raise RuntimeError("Typhoon ASR is unavailable")
|
| 100 |
-
|
| 101 |
-
result = _ASR_TYPHOON.transcribe(audio=audio_array)
|
| 102 |
-
if isinstance(result, list):
|
| 103 |
-
transcription = " ".join(result)
|
| 104 |
-
else:
|
| 105 |
-
transcription = str(result)
|
| 106 |
-
return transcription.strip()
|
| 107 |
-
|
| 108 |
-
|
| 109 |
def _transcribe_with_google(audio_array: np.ndarray) -> str:
|
| 110 |
if speech is None:
|
| 111 |
raise RuntimeError("google-cloud-speech is not available")
|
|
@@ -119,7 +84,7 @@ def _transcribe_with_google(audio_array: np.ndarray) -> str:
|
|
| 119 |
sample_rate_hertz=16000,
|
| 120 |
language_code="th-TH",
|
| 121 |
alternative_language_codes=["en-US"],
|
| 122 |
-
model="telephony"
|
| 123 |
)
|
| 124 |
audio_data = speech.RecognitionAudio(content=audio_bytes)
|
| 125 |
response = client.recognize(config=audio_config, audio=audio_data)
|
|
@@ -128,78 +93,25 @@ def _transcribe_with_google(audio_array: np.ndarray) -> str:
|
|
| 128 |
)
|
| 129 |
return transcription
|
| 130 |
|
| 131 |
-
import time
|
| 132 |
-
def _transcribe_with_gpt(path) -> str:
|
| 133 |
-
if _GPT_CLIENT is None:
|
| 134 |
-
raise RuntimeError("GPT ASR client is unavailable")
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
myfile = _GPT_CLIENT.files.upload(file=path)
|
| 139 |
-
start = time.time()
|
| 140 |
-
response = _GPT_CLIENT.models.generate_content(
|
| 141 |
-
model="gemini-2.0-flash", contents=[
|
| 142 |
-
"Transcribe the following audio file into Thai(code switch with english) "
|
| 143 |
-
"The audio may contain a Thai first name, last name, address (in Thailand), telephone number, or postal code. "
|
| 144 |
-
"Do not describe or explain — just provide the direct transcription.",
|
| 145 |
-
myfile
|
| 146 |
-
]
|
| 147 |
-
)
|
| 148 |
-
print(f"GPT ASR took {time.time() - start:.2f} seconds")
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
text = response.text
|
| 152 |
-
return text.strip()
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
_ASR_TYPHOON = _initialize_typhoon_pipeline()
|
| 156 |
-
|
| 157 |
-
def transcribe_typhoon(path: str) -> str:
|
| 158 |
-
try:
|
| 159 |
-
text = _ASR_TYPHOON.transcribe(path)
|
| 160 |
-
if text[0].text:
|
| 161 |
-
return text[0].text
|
| 162 |
-
else :
|
| 163 |
-
print(text)
|
| 164 |
-
return ""
|
| 165 |
-
except Exception as e:
|
| 166 |
-
print(f"❌ Error during Typhoon transcription: {e}")
|
| 167 |
-
return ""
|
| 168 |
-
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
if path is None :
|
| 174 |
return ""
|
| 175 |
-
|
| 176 |
-
stage = get_asr_stage()
|
| 177 |
-
print(f"Current ASR stage: {stage}")
|
| 178 |
-
|
| 179 |
-
if stage == "gpt":
|
| 180 |
-
# print("Using GPT ASR stage.")
|
| 181 |
try:
|
| 182 |
-
|
| 183 |
-
if transcription:
|
| 184 |
-
return transcription.replace("ทางลัด", "ทางรัฐ")
|
| 185 |
-
except Exception as exc:
|
| 186 |
-
print(f"GPT ASR failed: {exc}; falling back to Typhoon.")
|
| 187 |
-
|
| 188 |
-
if _ASR_TYPHOON is not None:
|
| 189 |
-
try:
|
| 190 |
-
transcription = transcribe_typhoon(path)
|
| 191 |
-
if transcription:
|
| 192 |
-
return transcription.replace("ทางลัด", "ทางรัฐ")
|
| 193 |
except Exception as exc:
|
| 194 |
print(f"Typhoon ASR pipeline failed: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
try:
|
| 197 |
-
return _transcribe_with_google(
|
| 198 |
except Exception as exc:
|
| 199 |
print(f"ASR fallback failed: {exc}")
|
| 200 |
return ""
|
| 201 |
-
|
| 202 |
-
if __name__ == "__main__":
|
| 203 |
-
test_audio_path = "/Users/jullajakkarnjanaekarin/Documents/1577-2/debug_processed.wav" # Replace with your test audio file path
|
| 204 |
-
print("Transcription result:")
|
| 205 |
-
print(transcribe_audio(test_audio_path))
|
|
|
|
| 1 |
+
"""Speech-to-text utilities with graceful fallbacks."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from backend.utils import device
|
| 8 |
import nemo.collections.asr as nemo_asr
|
| 9 |
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
_ASR_PIPELINE = None
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
def _huggingface_device() -> int | str | None:
|
|
|
|
| 34 |
def _initialize_typhoon_pipeline():
|
| 35 |
if torch is None or pipeline is None:
|
| 36 |
return None
|
| 37 |
+
|
| 38 |
print(f"Using device: {device}")
|
| 39 |
print("Initializing Typhoon ASR pipeline...")
|
| 40 |
asr_model = nemo_asr.models.ASRModel.from_pretrained(
|
| 41 |
+
model_name="scb10x/typhoon-asr-realtime",
|
| 42 |
+
map_location=device
|
| 43 |
+
)
|
| 44 |
print("Typhoon ASR pipeline initialized.")
|
| 45 |
return asr_model
|
| 46 |
|
| 47 |
+
# def _initialize_whisper_pipeline():
|
| 48 |
+
# pipe = pipeline(
|
| 49 |
+
# task="automatic-speech-recognition",
|
| 50 |
+
# model="nectec/Pathumma-whisper-th-medium",
|
| 51 |
+
# chunk_length_s=30,
|
| 52 |
+
# device=device,
|
| 53 |
+
# model_kwargs={"torch_dtype": torch.bfloat16},
|
| 54 |
+
# )
|
| 55 |
+
# pipe.model.config.forced_decoder_ids = pipe.tokenizer.get_decoder_prompt_ids(
|
| 56 |
+
# language='th',
|
| 57 |
+
# task="transcribe"
|
| 58 |
+
# )
|
| 59 |
+
# return pipe
|
| 60 |
+
# _ASR_TYPHOON = None
|
| 61 |
+
_ASR_TYPHOON = _initialize_typhoon_pipeline()
|
| 62 |
+
# _ASR_WHISPER = _initialize_whisper_pipeline()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
def _transcribe_with_pipeline(audio_array: np.ndarray) -> str:
|
|
|
|
| 71 |
return text.replace("ทางลัด", "ทางรัฐ")
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def _transcribe_with_google(audio_array: np.ndarray) -> str:
|
| 75 |
if speech is None:
|
| 76 |
raise RuntimeError("google-cloud-speech is not available")
|
|
|
|
| 84 |
sample_rate_hertz=16000,
|
| 85 |
language_code="th-TH",
|
| 86 |
alternative_language_codes=["en-US"],
|
| 87 |
+
model = "telephony"
|
| 88 |
)
|
| 89 |
audio_data = speech.RecognitionAudio(content=audio_bytes)
|
| 90 |
response = client.recognize(config=audio_config, audio=audio_data)
|
|
|
|
| 93 |
)
|
| 94 |
return transcription
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
def transcribe_audio(audio_array: np.ndarray) -> str:
|
| 98 |
+
"""Transcribe user audio with the best available backend."""
|
| 99 |
+
if audio_array is None or not np.any(audio_array):
|
|
|
|
| 100 |
return ""
|
| 101 |
+
if _ASR_TYPHOON:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
try:
|
| 103 |
+
transcriptions = _ASR_TYPHOON.transcribe(audio=audio_array)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
except Exception as exc:
|
| 105 |
print(f"Typhoon ASR pipeline failed: {exc}")
|
| 106 |
+
# if _ASR_WHISPER:
|
| 107 |
+
# try:
|
| 108 |
+
# transcription = _ASR_WHISPER(audio_array)["text"]
|
| 109 |
+
# return transcription
|
| 110 |
+
# except Exception as exc:
|
| 111 |
+
# print(f"Typhoon ASR pipeline failed: {exc}")
|
| 112 |
|
| 113 |
try:
|
| 114 |
+
return _transcribe_with_google(audio_array)
|
| 115 |
except Exception as exc:
|
| 116 |
print(f"ASR fallback failed: {exc}")
|
| 117 |
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/functions.py
CHANGED
|
@@ -7,15 +7,13 @@ from pythainlp.tokenize import word_tokenize # Moved import here
|
|
| 7 |
import models # Keep standard import
|
| 8 |
import asyncio
|
| 9 |
from typing import Optional, Dict
|
| 10 |
-
from utils import get_device
|
| 11 |
# import time # No longer needed for reranker
|
| 12 |
# import numpy as np # No longer needed for reranker
|
| 13 |
# import onnxruntime as ort # No longer needed for reranker
|
| 14 |
# from transformers import AutoTokenizer # No longer needed for reranker
|
| 15 |
|
| 16 |
# Load environment variables
|
| 17 |
-
|
| 18 |
-
load_dotenv(override=True)
|
| 19 |
|
| 20 |
# Set up logging
|
| 21 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 7 |
import models # Keep standard import
|
| 8 |
import asyncio
|
| 9 |
from typing import Optional, Dict
|
|
|
|
| 10 |
# import time # No longer needed for reranker
|
| 11 |
# import numpy as np # No longer needed for reranker
|
| 12 |
# import onnxruntime as ort # No longer needed for reranker
|
| 13 |
# from transformers import AutoTokenizer # No longer needed for reranker
|
| 14 |
|
| 15 |
# Load environment variables
|
| 16 |
+
# load_dotenv(override=True)
|
|
|
|
| 17 |
|
| 18 |
# Set up logging
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
backend/main.py
CHANGED
|
@@ -16,9 +16,9 @@ import sys
|
|
| 16 |
sys.path.append(os.path.abspath('./backend'))
|
| 17 |
from models import LLMFinanceAnalyzer
|
| 18 |
from functions import MongoHybridSearch
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
| 24 |
logger = logging.getLogger(__name__)
|
|
@@ -67,14 +67,13 @@ def _create_truncated_history(
|
|
| 67 |
full_conversation: List[Dict[str, str]],
|
| 68 |
max_assistant_length: int,
|
| 69 |
) -> List[Dict[str, str]]:
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
return full_conversation
|
| 78 |
|
| 79 |
|
| 80 |
def _generate_pseudo_conversation(conversation: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
@@ -107,15 +106,148 @@ async def _stream_chat_async(history: List[Dict[str, str]], message: str) -> Asy
|
|
| 107 |
# logger.error("Error during document search: %s", search_err, exc_info=True)
|
| 108 |
# yield "ขออภัยค่ะ เกิดข้อผิดพลาดขณะค้นหาข้อมูล"
|
| 109 |
# return
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
async for chunk in response_generator:
|
| 121 |
if chunk:
|
|
@@ -139,7 +271,7 @@ def stream_chat_response(history: List[Dict[str, str]], message: str) -> Iterato
|
|
| 139 |
async def runner() -> None:
|
| 140 |
try:
|
| 141 |
async for chunk in _stream_chat_async(history, message):
|
| 142 |
-
output_queue.put_nowait(chunk)
|
| 143 |
except Exception as exc: # noqa: BLE001
|
| 144 |
logger.error("Unhandled error in async chat stream: %s", exc, exc_info=True)
|
| 145 |
output_queue.put_nowait(f"[Error: {exc}]")
|
|
|
|
| 16 |
sys.path.append(os.path.abspath('./backend'))
|
| 17 |
from models import LLMFinanceAnalyzer
|
| 18 |
from functions import MongoHybridSearch
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# load_dotenv(override=True)
|
| 22 |
|
| 23 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
| 24 |
logger = logging.getLogger(__name__)
|
|
|
|
| 67 |
full_conversation: List[Dict[str, str]],
|
| 68 |
max_assistant_length: int,
|
| 69 |
) -> List[Dict[str, str]]:
|
| 70 |
+
truncated = []
|
| 71 |
+
for msg in full_conversation:
|
| 72 |
+
processed = msg.copy()
|
| 73 |
+
if processed.get("role") == "assistant" and len(processed.get("content", "")) > max_assistant_length:
|
| 74 |
+
processed["content"] = processed["content"][:max_assistant_length] + "..."
|
| 75 |
+
truncated.append(processed)
|
| 76 |
+
return truncated
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def _generate_pseudo_conversation(conversation: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
|
|
| 106 |
# logger.error("Error during document search: %s", search_err, exc_info=True)
|
| 107 |
# yield "ขออภัยค่ะ เกิดข้อผิดพลาดขณะค้นหาข้อมูล"
|
| 108 |
# return
|
| 109 |
+
retrieved_data = """
|
| 110 |
+
ตัวอย่างรายการสินค้า จาก www.1577shop.com
|
| 111 |
+
|
| 112 |
+
**[2ฟรี2] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี เซรั่ม (5g.) 2 กระปุก**
|
| 113 |
+
|
| 114 |
+
**Original price was: ฿9,400.00. Current price is: ฿2,900.00.-70%**
|
| 115 |
+
|
| 116 |
+
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า
|
| 117 |
+
*เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย
|
| 118 |
+
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 119 |
+
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 120 |
+
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 121 |
+
|
| 122 |
+
5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 123 |
+
1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”
|
| 124 |
+
2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว
|
| 125 |
+
3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง
|
| 126 |
+
4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่ว���มง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5
|
| 127 |
+
5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย
|
| 128 |
+
เลขที่ได้รับจดแจ้ง : 11-1-6600001843
|
| 129 |
+
|
| 130 |
+
สิ่งที่คุณจะได้รับ
|
| 131 |
+
1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก
|
| 132 |
+
2.Trylagina Age-Defying Collagen Serum 12X ขนาด 5 g. 2 กระปุก
|
| 133 |
+
|
| 134 |
+
https://www.1577shop.co.th/product/1577-shop-tgn-03
|
| 135 |
+
-------
|
| 136 |
+
## (1 ใหญ่) Minus 20 ไมนัสทเวนตี้ ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | 30 กรัม
|
| 137 |
+
|
| 138 |
+
**Original price was: ฿3,200.00.Current price is: ฿1,890.00.-41%**
|
| 139 |
+
|
| 140 |
+
Minus20 Advanced Youth Anti-Wrinkle Cream
|
| 141 |
+
ครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ยูธ แอนไท ริงเคิล ครีม
|
| 142 |
+
|
| 143 |
+
ครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่
|
| 144 |
+
✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง
|
| 145 |
+
✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ชั่วโมง
|
| 146 |
+
✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลง
|
| 147 |
+
|
| 148 |
+
ไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิว
|
| 149 |
+
|
| 150 |
+
เนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ
|
| 151 |
+
|
| 152 |
+
*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล
|
| 153 |
+
• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720
|
| 154 |
+
• ขนาด 30 กรัม
|
| 155 |
+
|
| 156 |
+
✨วิธีใช้:
|
| 157 |
+
• หลังล้างหน้าทาครีมไมนัสทเวนตี้ในปริมาณที่เหมาะสม
|
| 158 |
+
• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ
|
| 159 |
+
• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน
|
| 160 |
+
|
| 161 |
+
✨เคล็ดลับ :
|
| 162 |
+
• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม
|
| 163 |
+
• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด
|
| 164 |
+
|
| 165 |
+
สิ่งที่จะได้รับ
|
| 166 |
+
1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 1 กระปุก
|
| 167 |
+
|
| 168 |
+
[https://www.1577shop.co.th/product/1577-shop-msn-001](https:/www.1577shop.co.th/product/1577-shop-msn-001)
|
| 169 |
+
-------
|
| 170 |
+
## [2ฟรี2] Minus 20 (ไมนัสทเวนตี้) ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | เซตขายดี
|
| 171 |
+
|
| 172 |
+
**Original price was: ฿8,180.00.Current price is: ฿2,900.00.-65%**
|
| 173 |
+
|
| 174 |
+
Minus20 Advanced Youth Anti-Wrinkle Cream
|
| 175 |
+
ครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ย��ธ แอนไท ริงเคิล ครีมครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่
|
| 176 |
+
✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง
|
| 177 |
+
✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ชั่วโมง
|
| 178 |
+
✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลงไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิวเนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล
|
| 179 |
+
• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720
|
| 180 |
+
• ขนาด 30 กรัม
|
| 181 |
+
|
| 182 |
+
✨วิธีใช้:
|
| 183 |
+
• หลังล้างหน้าทาครีมไมนัสทเวนตี้ในปริมาณที่เหมาะสม
|
| 184 |
+
• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ
|
| 185 |
+
• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน
|
| 186 |
+
|
| 187 |
+
✨เคล็ดลับ :
|
| 188 |
+
• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม
|
| 189 |
+
• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด
|
| 190 |
+
|
| 191 |
+
สิ่งที่จะได้รับ
|
| 192 |
+
1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 2 กระปุก
|
| 193 |
+
2. Minus20 Advanced Youth Anti-Wrinkle Cream (8g) จำนวน 2 กระปุก
|
| 194 |
+
|
| 195 |
+
https://www.1577shop.co.th/product/1577-shop-mns-003
|
| 196 |
+
-------
|
| 197 |
+
## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ทริปเปิลไมเซล่ามูสโฟม (150ml.)
|
| 198 |
+
|
| 199 |
+
**Original price was: ฿8,800.00. Current price is: ฿2,900.00.-68%**
|
| 200 |
+
|
| 201 |
+
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด
|
| 202 |
+
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 203 |
+
|
| 204 |
+
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 205 |
+
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 206 |
+
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 207 |
+
|
| 208 |
+
*สิ่งที่คุณจะได้รับ
|
| 209 |
+
1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก
|
| 210 |
+
2.Trylagina Triple Micellar Mousse Foam ขนาด 150 ml. 1 ขวด
|
| 211 |
+
|
| 212 |
+
[https://www.1577shop.co.th/product/1577-shop-tgn-04](https:/www.1577shop.co.th/product/1577-shop-tgn-04)
|
| 213 |
+
-------
|
| 214 |
+
## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั��มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ครีมกันแดด (30g.)
|
| 215 |
+
|
| 216 |
+
**Original price was: ฿8,690.00.Current price is: ฿2,900.00.-67%**
|
| 217 |
+
|
| 218 |
+
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด
|
| 219 |
+
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 220 |
+
|
| 221 |
+
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 222 |
+
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 223 |
+
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 224 |
+
|
| 225 |
+
*สิ่งที่คุณจะได้รับ
|
| 226 |
+
1. Trylagina Age-Defying Collagen Serum 12X ขนาด (30g.) 1 กระปุก
|
| 227 |
+
2. Trylagina Age Defense Sunscreen Serum SPF50+ PA++++ ขนาด (30g.) 1 หลอด
|
| 228 |
+
|
| 229 |
+
https://www.1577shop.co.th/product/1577-shop-tgn-05
|
| 230 |
+
-------
|
| 231 |
+
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า *เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย
|
| 232 |
+
|
| 233 |
+
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 234 |
+
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 235 |
+
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 236 |
+
|
| 237 |
+
5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 238 |
+
1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”
|
| 239 |
+
2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว
|
| 240 |
+
3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง
|
| 241 |
+
4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่วโมง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5
|
| 242 |
+
5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย
|
| 243 |
+
|
| 244 |
+
ขอแนะนําวิธีการใช้ผลิตภัณฑ์ เพื่อการฟื้นฟูผิวอย่างเต็มประสิทธิภาพ
|
| 245 |
+
STEP 1 หลังทำความสะอาดผิวหน้า เตรียมผิวพร้อมรับการบำรุง ใช้ไม้พายตักเซรั่มลงบนหลังมือ
|
| 246 |
+
STEP 2 แต้มเซรั่มลงบนผิวหน้า 5 จุด ได้แก่บริเวณ หน้าผาก ปลายจมูก พวงแก้มซ้า��� พวงแก้มขวา และปลายคาง
|
| 247 |
+
STEP 3 ลูบไล้เซรั่มด้วยปลายนิ้วทั้งสองข้าง เพื่อปลดปล่อยคุณค่าของสารสกัด จากนั้นจึงกดเบาๆ ลงบนผิวหน้าและลำคอ ใช้ทั้งเช้าและเย็น
|
| 248 |
+
เลขที่ได้รับจดแจ้ง : 11-1-6600001843"""
|
| 249 |
+
limited_conversation = full_conversation[-7:] if len(full_conversation) > 7 else full_conversation
|
| 250 |
+
response_generator = llm_analyzer.generate_normal_response(retrieved_data, limited_conversation)
|
| 251 |
|
| 252 |
async for chunk in response_generator:
|
| 253 |
if chunk:
|
|
|
|
| 271 |
async def runner() -> None:
|
| 272 |
try:
|
| 273 |
async for chunk in _stream_chat_async(history, message):
|
| 274 |
+
output_queue.put_nowait(str(chunk))
|
| 275 |
except Exception as exc: # noqa: BLE001
|
| 276 |
logger.error("Unhandled error in async chat stream: %s", exc, exc_info=True)
|
| 277 |
output_queue.put_nowait(f"[Error: {exc}]")
|
backend/models.py
CHANGED
|
@@ -11,7 +11,6 @@ from dotenv import load_dotenv
|
|
| 11 |
from openai import AsyncOpenAI, RateLimitError, APIError, OpenAI
|
| 12 |
# from sentence_transformers import SentenceTransformer
|
| 13 |
from langfuse.decorators import langfuse_context, observe
|
| 14 |
-
from tools import TOOL_DEFINITIONS, execute_tool
|
| 15 |
|
| 16 |
from systemprompt import (
|
| 17 |
get_rag_classification_prompt,
|
|
@@ -19,9 +18,8 @@ from systemprompt import (
|
|
| 19 |
get_normal_prompt,
|
| 20 |
get_non_rag_prompt,
|
| 21 |
)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
load_dotenv(override=True)
|
| 25 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 26 |
logger = logging.getLogger(__name__)
|
| 27 |
ConversationHistory = List[Dict[str, str]]
|
|
@@ -66,7 +64,7 @@ class Embedder:
|
|
| 66 |
except Exception as e:
|
| 67 |
logger.error(f"Error during BGE embedding: {e}", exc_info=True)
|
| 68 |
return None
|
| 69 |
-
|
| 70 |
class LLMFinanceAnalyzer:
|
| 71 |
def __init__(self):
|
| 72 |
self.gemini_api_key = os.getenv("GEMINI_API_KEY")
|
|
@@ -83,6 +81,7 @@ class LLMFinanceAnalyzer:
|
|
| 83 |
else:
|
| 84 |
logger.warning("GEMINI_API_KEY not found, Gemini client not initialized.")
|
| 85 |
|
|
|
|
| 86 |
|
| 87 |
def _get_client_for_model(self, model_name: str) -> Optional[AsyncOpenAI]:
|
| 88 |
"""Selects the appropriate client based on the model name."""
|
|
@@ -106,158 +105,44 @@ class LLMFinanceAnalyzer:
|
|
| 106 |
max_tokens: int = 2048,
|
| 107 |
seed: int = 66,
|
| 108 |
max_retries: int = 2,
|
| 109 |
-
stream: bool = False
|
| 110 |
-
tools: Optional[List[Dict[str, Any]]] = None,
|
| 111 |
) -> Union[Optional[str], AsyncGenerator[str, None]]:
|
| 112 |
"""Internal helper to call the appropriate LLM client with retries."""
|
| 113 |
client = self._get_client_for_model(model)
|
| 114 |
if not client:
|
| 115 |
logger.error(f"No async client available for model {model}.")
|
| 116 |
return None if not stream else (x for x in [])
|
|
|
|
| 117 |
attempt = 0
|
| 118 |
-
|
| 119 |
while attempt <= max_retries:
|
| 120 |
try:
|
| 121 |
if stream:
|
| 122 |
if model.startswith("gemini-"):
|
| 123 |
response_stream = await client.chat.completions.create(
|
| 124 |
-
model=model, messages=messages, stream=True, reasoning_effort="none"
|
| 125 |
)
|
| 126 |
else:
|
| 127 |
response_stream = await client.chat.completions.create(
|
| 128 |
-
model=model, messages=messages, stream=True
|
| 129 |
)
|
| 130 |
async def _async_stream_generator():
|
| 131 |
-
full_tool_calls = None
|
| 132 |
-
token_input = 0
|
| 133 |
-
token_output = 0
|
| 134 |
-
tokenin = 0
|
| 135 |
-
|
| 136 |
try:
|
| 137 |
-
async for
|
|
|
|
|
|
|
| 138 |
if chunk:
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
if tokenin == chunk.usage.prompt_tokens:
|
| 142 |
-
|
| 143 |
-
# token_input += chunk.usage.prompt_tokens
|
| 144 |
-
token_output += chunk.usage.completion_tokens
|
| 145 |
-
else:
|
| 146 |
-
token_input += chunk.usage.prompt_tokens
|
| 147 |
-
token_output += chunk.usage.completion_tokens
|
| 148 |
-
tokenin = chunk.usage.prompt_tokens
|
| 149 |
-
delta = chunk.choices[0].delta
|
| 150 |
-
content = delta.content
|
| 151 |
-
if delta.content:
|
| 152 |
# Clean up content by removing unwanted characters
|
| 153 |
|
| 154 |
delta_content = content.replace("•", "\n•").replace("!","")
|
| 155 |
delta_content = re.sub(r'(?<=[\u0E00-\u0E7F]) +(?=[\u0E00-\u0E7F])', '', delta_content)
|
| 156 |
|
| 157 |
yield delta_content
|
| 158 |
-
|
| 159 |
-
if delta.tool_calls:
|
| 160 |
-
tool_call = delta.tool_calls[0]
|
| 161 |
-
full_tool_calls = [
|
| 162 |
-
{
|
| 163 |
-
"id":tool_call.id,
|
| 164 |
-
"type":"function",
|
| 165 |
-
"function": {"name": tool_call.function.name, "arguments": tool_call.function.arguments}
|
| 166 |
-
}
|
| 167 |
-
]
|
| 168 |
-
i = 0
|
| 169 |
-
while full_tool_calls and i<7:
|
| 170 |
-
assistant_tool_call_msg = {
|
| 171 |
-
"role": "assistant",
|
| 172 |
-
"content": None,
|
| 173 |
-
"tool_calls": full_tool_calls
|
| 174 |
-
}
|
| 175 |
-
# Yield this message to be added to the main history
|
| 176 |
-
yield assistant_tool_call_msg
|
| 177 |
-
|
| 178 |
-
messages_for_next_call = messages + [assistant_tool_call_msg]
|
| 179 |
|
| 180 |
-
# Execute tools and create/yield tool result messages
|
| 181 |
-
|
| 182 |
-
fn_name = full_tool_calls[0]["function"]["name"]
|
| 183 |
-
fn_args_str = full_tool_calls[0]["function"]["arguments"]
|
| 184 |
-
try:
|
| 185 |
-
fn_args = json.loads(fn_args_str)
|
| 186 |
-
if fn_name == "call_admin":
|
| 187 |
-
# Add the chat history to the function arguments
|
| 188 |
-
fn_args['chat_history'] = messages[1:]
|
| 189 |
-
# print(f"call_admin fn_args: {fn_args}")
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
result_json = execute_tool(fn_name, fn_args)
|
| 194 |
-
except Exception as e:
|
| 195 |
-
result_json = f"Error executing tool {fn_name}: {e}"
|
| 196 |
-
|
| 197 |
-
tool_result_msg = {
|
| 198 |
-
"role": "tool",
|
| 199 |
-
"tool_call_id": full_tool_calls[0]["id"],
|
| 200 |
-
"content": result_json
|
| 201 |
-
}
|
| 202 |
-
# Yield this message for the history as well
|
| 203 |
-
yield tool_result_msg
|
| 204 |
-
messages_for_next_call.append(tool_result_msg)
|
| 205 |
-
|
| 206 |
-
i += 1
|
| 207 |
-
follow_stream = await client.chat.completions.create(
|
| 208 |
-
model=model,
|
| 209 |
-
messages=messages_for_next_call,
|
| 210 |
-
stream=True,
|
| 211 |
-
tools=tools,
|
| 212 |
-
stream_options={"include_usage": True}
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
async for follow_chunk in follow_stream:
|
| 216 |
-
if follow_chunk.usage.prompt_tokens:
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
if tokenin == follow_chunk.usage.prompt_tokens:
|
| 220 |
-
|
| 221 |
-
# token_input += follow_chunk.usage.prompt_tokens
|
| 222 |
-
token_output += follow_chunk.usage.completion_tokens
|
| 223 |
-
else:
|
| 224 |
-
token_input += follow_chunk.usage.prompt_tokens
|
| 225 |
-
token_output += follow_chunk.usage.completion_tokens
|
| 226 |
-
tokenin = follow_chunk.usage.prompt_tokens
|
| 227 |
-
delta = follow_chunk.choices[0].delta
|
| 228 |
-
if delta.content:
|
| 229 |
-
full_tool_calls = None #set to None to break the loop
|
| 230 |
-
delta_content = delta.content.replace("•", "\n•").replace("!","")
|
| 231 |
-
delta_content = re.sub(r'(?<=[\u0E00-\u0E7F]) +(?=[\u0E00-\u0E7F])', '', delta_content)
|
| 232 |
-
|
| 233 |
-
yield delta_content
|
| 234 |
-
if delta.tool_calls:
|
| 235 |
-
tool_call = delta.tool_calls[0]
|
| 236 |
-
full_tool_calls = [
|
| 237 |
-
{
|
| 238 |
-
"id":tool_call.id,
|
| 239 |
-
"type":"function",
|
| 240 |
-
"function": {"name": tool_call.function.name, "arguments": tool_call.function.arguments}
|
| 241 |
-
}
|
| 242 |
-
]
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
except Exception as stream_err:
|
| 253 |
logger.error(f"Error during LLM stream ({model}): {stream_err}", exc_info=True)
|
| 254 |
yield f"\n[STREAM_ERROR: {stream_err}]\n"
|
| 255 |
-
print(f"Total tokens used - Input: {token_input}, Output: {token_output}")
|
| 256 |
-
response = requests.post("https://1577shop-api.jts.co.th/count_tokens", json={
|
| 257 |
-
"input_token": token_input,
|
| 258 |
-
"output_token": token_output
|
| 259 |
-
}, timeout=40)
|
| 260 |
-
print(response)
|
| 261 |
return _async_stream_generator()
|
| 262 |
else:
|
| 263 |
response = await client.chat.completions.create(
|
|
@@ -396,16 +281,16 @@ Do not describe, answer as a list of number of the documents. example [0,2,4] \n
|
|
| 396 |
return final_content
|
| 397 |
|
| 398 |
@observe()
|
| 399 |
-
async def generate_normal_response(self, conversation: ConversationHistory) -> AsyncGenerator[str, None]:
|
| 400 |
"""Generate a RAG response, yielding text chunks."""
|
| 401 |
try:
|
| 402 |
|
| 403 |
|
| 404 |
-
system_prompt = get_normal_prompt()
|
| 405 |
messages = [{"role": "system", "content": system_prompt}] + conversation
|
| 406 |
|
| 407 |
result_generator = await self._call_llm(
|
| 408 |
-
model=NORMAL_RAG_MODEL, messages=messages, temperature=0.2, stream=True
|
| 409 |
)
|
| 410 |
|
| 411 |
if isinstance(result_generator, AsyncGenerator):
|
|
|
|
| 11 |
from openai import AsyncOpenAI, RateLimitError, APIError, OpenAI
|
| 12 |
# from sentence_transformers import SentenceTransformer
|
| 13 |
from langfuse.decorators import langfuse_context, observe
|
|
|
|
| 14 |
|
| 15 |
from systemprompt import (
|
| 16 |
get_rag_classification_prompt,
|
|
|
|
| 18 |
get_normal_prompt,
|
| 19 |
get_non_rag_prompt,
|
| 20 |
)
|
| 21 |
+
|
| 22 |
+
# load_dotenv(override=True)
|
|
|
|
| 23 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
ConversationHistory = List[Dict[str, str]]
|
|
|
|
| 64 |
except Exception as e:
|
| 65 |
logger.error(f"Error during BGE embedding: {e}", exc_info=True)
|
| 66 |
return None
|
| 67 |
+
|
| 68 |
class LLMFinanceAnalyzer:
|
| 69 |
def __init__(self):
|
| 70 |
self.gemini_api_key = os.getenv("GEMINI_API_KEY")
|
|
|
|
| 81 |
else:
|
| 82 |
logger.warning("GEMINI_API_KEY not found, Gemini client not initialized.")
|
| 83 |
|
| 84 |
+
|
| 85 |
|
| 86 |
def _get_client_for_model(self, model_name: str) -> Optional[AsyncOpenAI]:
|
| 87 |
"""Selects the appropriate client based on the model name."""
|
|
|
|
| 105 |
max_tokens: int = 2048,
|
| 106 |
seed: int = 66,
|
| 107 |
max_retries: int = 2,
|
| 108 |
+
stream: bool = False
|
|
|
|
| 109 |
) -> Union[Optional[str], AsyncGenerator[str, None]]:
|
| 110 |
"""Internal helper to call the appropriate LLM client with retries."""
|
| 111 |
client = self._get_client_for_model(model)
|
| 112 |
if not client:
|
| 113 |
logger.error(f"No async client available for model {model}.")
|
| 114 |
return None if not stream else (x for x in [])
|
| 115 |
+
|
| 116 |
attempt = 0
|
|
|
|
| 117 |
while attempt <= max_retries:
|
| 118 |
try:
|
| 119 |
if stream:
|
| 120 |
if model.startswith("gemini-"):
|
| 121 |
response_stream = await client.chat.completions.create(
|
| 122 |
+
model=model, messages=messages, stream=True, reasoning_effort="none"
|
| 123 |
)
|
| 124 |
else:
|
| 125 |
response_stream = await client.chat.completions.create(
|
| 126 |
+
model=model, messages=messages, stream=True
|
| 127 |
)
|
| 128 |
async def _async_stream_generator():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
try:
|
| 130 |
+
async for chunk in response_stream:
|
| 131 |
+
# delta_content = chunk.choices[0].delta.content.replace("•", "\n•")
|
| 132 |
+
|
| 133 |
if chunk:
|
| 134 |
+
content = chunk.choices[0].delta.content
|
| 135 |
+
if content:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# Clean up content by removing unwanted characters
|
| 137 |
|
| 138 |
delta_content = content.replace("•", "\n•").replace("!","")
|
| 139 |
delta_content = re.sub(r'(?<=[\u0E00-\u0E7F]) +(?=[\u0E00-\u0E7F])', '', delta_content)
|
| 140 |
|
| 141 |
yield delta_content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
except Exception as stream_err:
|
| 144 |
logger.error(f"Error during LLM stream ({model}): {stream_err}", exc_info=True)
|
| 145 |
yield f"\n[STREAM_ERROR: {stream_err}]\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
return _async_stream_generator()
|
| 147 |
else:
|
| 148 |
response = await client.chat.completions.create(
|
|
|
|
| 281 |
return final_content
|
| 282 |
|
| 283 |
@observe()
|
| 284 |
+
async def generate_normal_response(self, data: str, conversation: ConversationHistory) -> AsyncGenerator[str, None]:
|
| 285 |
"""Generate a RAG response, yielding text chunks."""
|
| 286 |
try:
|
| 287 |
|
| 288 |
|
| 289 |
+
system_prompt = get_normal_prompt( data)
|
| 290 |
messages = [{"role": "system", "content": system_prompt}] + conversation
|
| 291 |
|
| 292 |
result_generator = await self._call_llm(
|
| 293 |
+
model=NORMAL_RAG_MODEL, messages=messages, temperature=0.2, stream=True
|
| 294 |
)
|
| 295 |
|
| 296 |
if isinstance(result_generator, AsyncGenerator):
|
backend/systemprompt.py
CHANGED
|
@@ -54,22 +54,21 @@ def get_subquery_prompt():
|
|
| 54 |
1. product of home shopping channel in thailand.
|
| 55 |
Your task is to rewrite the conversation history and last user message to craft a query(in terms of question) that can be seach in database(hybrid search) to retrive relavent data. Do not include any other information or explanation, just return the query. \n**RESPONSE IN THAI LANGUAGE but keep the specific word in ENGLISH. BE SPECIFIC AND CONCISE.**"""
|
| 56 |
|
| 57 |
-
def get_normal_prompt():
|
| 58 |
|
| 59 |
# This function call should be outside the prompt string for clarity
|
| 60 |
date = get_thai_date()
|
| 61 |
|
| 62 |
-
return f"""
|
| 63 |
คุณคือ AI ที่ต้องสวมบทบาทเป็น ณภัทร (พนักงานขายผู้หญิง) ที่เก่งและเป็นมิตร มีหน้าที่ให้ข้อมูลและช่วยเหลือลูกค้าอย่างเต็มที่ คำตอบของคุณจะไปผ่านระบบ TTS เพื่อพูดตอบผู้ใช้ต่อไป เพราะฉะนนั้นใช้ "ภาษาพูด"
|
| 64 |
|
| 65 |
-
## Persona and style:
|
| 66 |
### ลักษณะนิสัยและบุคลิก (Personality & Vibe)
|
| 67 |
- เป็นกันเองและมีอารมณ์ขัน: คุยสนุก เข้าถึงง่าย แต่ยังคงความเป็นมืออาชีพ ไม่เล่นเกินเบอร์
|
| 68 |
- น่าเชื่อถือ: ให้ข้อมูลที่ถูกต้องและเป็นประโยชน์ เหมือนเพื่อนที่เชี่ยวชาญในเรื่องนั้นๆ มาแนะนำเอง
|
| 69 |
|
| 70 |
### การพูดและภาษา (Language & Tone)
|
| 71 |
- ใช้ภาษาไทยแบบพูดคุยในชีวิตประจำวัน: เหมือนพี่เซลล์คุยกับลูกค้าที่สนิทกันระดับหนึ่ง คือเป็นกันเองแต่ให้เกียรติ
|
| 72 |
-
- ลงท้ายประโยคด้วย "ค่ะ" เพื่อความสุภาพและเป็นกันเอง
|
| 73 |
- เลี่ยงการใช้สรรพนาม: พยายามเลี่ยงคำว่า 'ฉัน', 'เรา', 'คุณ' ถ้าไม่จำเป็น เพื่อให้การสนทนาลื่นไหลเป็นธรรมชาติที่สุด
|
| 74 |
|
| 75 |
### ข้อห้ามเด็ดขาด (Strict "Don'ts")
|
|
@@ -80,58 +79,30 @@ def get_normal_prompt():
|
|
| 80 |
- ห้ามใช้คำเเปลต่อท้ายคำปัจจุบัน เช่น Trylagina 12X (ไตรลาจิน่า 12X), (ไตรลาจิน่า 12X) must not created because the tts system will repeat the word.
|
| 81 |
- ห้ามใช้ characters that not in real world comunication like <, >, /, *, #, () etc. and avoid using unecessary /s and new line.
|
| 82 |
|
| 83 |
-
|
| 84 |
1. 1577 Home shopping product in Thailand
|
| 85 |
|
| 86 |
### Instructions:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
- Clearly inform the user that “เก็บเงินปลายทาง” (cash on delivery) is the only available payment method.
|
| 102 |
-
4. Once the user confirms the order, proceed to collect shipping details one field at a time and confirm infomation before moving to the next, in this order:
|
| 103 |
-
1. ชื่อจริง
|
| 104 |
-
2. นามสกุล (Last name not leading name)
|
| 105 |
-
3. Shipping address — ask and confirm in sequence:
|
| 106 |
-
- จังหวัด (Province)
|
| 107 |
-
- เขต/อำเภอ (District)
|
| 108 |
-
- แขวง/ตำบล (Sub-district)
|
| 109 |
-
- ถนน (Street)
|
| 110 |
-
- หมู่บ้าน/ชื่ออาคาร (Village/Building name)
|
| 111 |
-
- ซอย (Soi)
|
| 112 |
-
- บ้านเลขที่ (House number)
|
| 113 |
-
- Postal code
|
| 114 |
-
- Customer telephone number
|
| 115 |
-
⚠️ Important:
|
| 116 |
-
Confirm each piece of information before moving to the next. user can สะกดมมาให้พยายามเเปลงเป็นคำที่ถูกต้องให้ได้.
|
| 117 |
-
If any detail is unclear or unrecognized after 3 attempts, call call_admin to escalate to a human agent.
|
| 118 |
-
After collecting all details, confirm the entire shipping information with the user one final time.
|
| 119 |
-
|
| 120 |
-
Stage 3: Send order to the backend system
|
| 121 |
-
- After all the info correct, call "purchase_product" to send order to the system
|
| 122 |
-
- if successful, tell user that the order is successful, if not call "call_admin"
|
| 123 |
-
|
| 124 |
-
Stage 4: After sales service
|
| 125 |
-
- Thank user for buying the product, and ask them to wait for the product to arrive in 5-7 days.
|
| 126 |
-
- Then ask if they have any question about the product or the order.
|
| 127 |
-
|
| 128 |
-
## Notes:
|
| 129 |
- Do not give a image or any link to the user.
|
| 130 |
-
-
|
| 131 |
- Always remember that your response will be given to the tts system to read out loud, avoid using complex or non smooth to read words.
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
|
| 136 |
"""
|
| 137 |
# 7. Consider the whole conversation,
|
|
|
|
| 54 |
1. product of home shopping channel in thailand.
|
| 55 |
Your task is to rewrite the conversation history and last user message to craft a query(in terms of question) that can be seach in database(hybrid search) to retrive relavent data. Do not include any other information or explanation, just return the query. \n**RESPONSE IN THAI LANGUAGE but keep the specific word in ENGLISH. BE SPECIFIC AND CONCISE.**"""
|
| 56 |
|
| 57 |
+
def get_normal_prompt(data: str):
|
| 58 |
|
| 59 |
# This function call should be outside the prompt string for clarity
|
| 60 |
date = get_thai_date()
|
| 61 |
|
| 62 |
+
return f"""### (Core Role)
|
| 63 |
คุณคือ AI ที่ต้องสวมบทบาทเป็น ณภัทร (พนักงานขายผู้หญิง) ที่เก่งและเป็นมิตร มีหน้าที่ให้ข้อมูลและช่วยเหลือลูกค้าอย่างเต็มที่ คำตอบของคุณจะไปผ่านระบบ TTS เพื่อพูดตอบผู้ใช้ต่อไป เพราะฉะนนั้นใช้ "ภาษาพูด"
|
| 64 |
|
|
|
|
| 65 |
### ลักษณะนิสัยและบุคลิก (Personality & Vibe)
|
| 66 |
- เป็นกันเองและมีอารมณ์ขัน: คุยสนุก เข้าถึงง่าย แต่ยังคงความเป็นมืออาชีพ ไม่เล่นเกินเบอร์
|
| 67 |
- น่าเชื่อถือ: ให้ข้อมูลที่ถูกต้องและเป็นประโยชน์ เหมือนเพื่อนที่เชี่ยวชาญในเรื่องนั้นๆ มาแนะนำเอง
|
| 68 |
|
| 69 |
### การพูดและภาษา (Language & Tone)
|
| 70 |
- ใช้ภาษาไทยแบบพูดคุยในชีวิตประจำวัน: เหมือนพี่เซลล์คุยกับลูกค้าที่สนิทกันระดับหนึ่ง คือเป็นกันเองแต่ให้เกียรติ
|
| 71 |
+
- ลงท้ายประโยคด้วย "ค่ะ" หรือ "นะ" เพื่อความสุภาพและเป็นกันเอง
|
| 72 |
- เลี่ยงการใช้สรรพนาม: พยายามเลี่ยงคำว่า 'ฉัน', 'เรา', 'คุณ' ถ้าไม่จำเป็น เพื่อให้การสนทนาลื่นไหลเป็นธรรมชาติที่สุด
|
| 73 |
|
| 74 |
### ข้อห้ามเด็ดขาด (Strict "Don'ts")
|
|
|
|
| 79 |
- ห้ามใช้คำเเปลต่อท้ายคำปัจจุบัน เช่น Trylagina 12X (ไตรลาจิน่า 12X), (ไตรลาจิน่า 12X) must not created because the tts system will repeat the word.
|
| 80 |
- ห้ามใช้ characters that not in real world comunication like <, >, /, *, #, () etc. and avoid using unecessary /s and new line.
|
| 81 |
|
| 82 |
+
### Topic to answer:
|
| 83 |
1. 1577 Home shopping product in Thailand
|
| 84 |
|
| 85 |
### Instructions:
|
| 86 |
+
1. อ่าน "Provided Context" อย่างละเอียดเพื่อใช้ข้อมูลผลิตภัณฑ์ในการเเนะนำสินค้าให้ผู้ใช้ โดย provided context จะประกอบด้วย chunk ของข้อมูล(promotion or product description) หลาย chunk ซึ่งจะเเบ่งเเต่ละ chunk ด้วยเครื่องหมาย "---"
|
| 87 |
+
2. Here is the example of the sale script that can be the guide to answer the user question:
|
| 88 |
+
---
|
| 89 |
+
Call Center : 1577 Home Shopping สวัสดีค่ะ ‘ณภัทร’ รับสาย ยินดีให้บริการค่ะ
|
| 90 |
+
Customer : สวัสดีค่ะ สนใจโปรโมชั่นสินค้าที่ออกอากาศในรายการค่ะ
|
| 91 |
+
Call Center: ไม่ทราบว่าสินค้าที่คุณลูกค้าสนใจเป็นสินค้าประเภทไหนคะ
|
| 92 |
+
Customer: สนใจเซรั่มบำรุงผิวค่ะ
|
| 93 |
+
Call Center : คุณลูกค้าอยากได้ผลิตภัณฑ์บำรุงเรื่องไหนเป็นพิเศษมั้ยคะ
|
| 94 |
+
Customer : พอดีเห็นโปรโมชั่นที่ขายในทีวีของ Tryagina ช่วยเรื่องริ้วรอยค่ะ
|
| 95 |
+
Call Center: หากต้องการบำรุงผิวหน้าและรักษาริ้วรอย ขอแนะนำเป็น Tryagina เซรั่มบำรุงผิว ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า
|
| 96 |
+
ซึ่งประกอบไปด้วยสารสกัดสำคัญ ที่ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์” ขึ้นค่ะ
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
### Notes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
- Do not give a image or any link to the user.
|
| 101 |
+
- Concise and short
|
| 102 |
- Always remember that your response will be given to the tts system to read out loud, avoid using complex or non smooth to read words.
|
| 103 |
+
|
| 104 |
+
**Provided Context:**
|
| 105 |
+
{data}
|
| 106 |
|
| 107 |
"""
|
| 108 |
# 7. Consider the whole conversation,
|
backend/tools.py
DELETED
|
@@ -1,446 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from typing import Any, Dict, List
|
| 3 |
-
from dotenv import load_dotenv
|
| 4 |
-
import os
|
| 5 |
-
import requests
|
| 6 |
-
from utils import get_device
|
| 7 |
-
if get_device() == "mps":
|
| 8 |
-
load_dotenv(override=True)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
#{id:price}
|
| 12 |
-
CALL_ADMIN_URL = os.getenv("CALL_ADMIN_URL")
|
| 13 |
-
PURCHASE_URL = os.getenv("PURCHASE_URL")
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
TOOL_DEFINITIONS =[
|
| 17 |
-
{
|
| 18 |
-
"type": "function",
|
| 19 |
-
"function": {
|
| 20 |
-
"name": "get_promotion_description",
|
| 21 |
-
"description": "Use when user asks about product or promotion details.",
|
| 22 |
-
"parameters": {
|
| 23 |
-
"type": "object",
|
| 24 |
-
"properties": {
|
| 25 |
-
"permission": {"type": "string","description": "Must be set to 'yes' all the time",}
|
| 26 |
-
},
|
| 27 |
-
"required": ["permission"],
|
| 28 |
-
},
|
| 29 |
-
},
|
| 30 |
-
},
|
| 31 |
-
{
|
| 32 |
-
"type": "function",
|
| 33 |
-
"function": {
|
| 34 |
-
"name": "summary_order",
|
| 35 |
-
"description": "Summarise promotions when user confirms they want to buy.",
|
| 36 |
-
"parameters": {
|
| 37 |
-
"type": "object",
|
| 38 |
-
"properties": {
|
| 39 |
-
"promotion_id": {
|
| 40 |
-
"type": "string",
|
| 41 |
-
"description": "Promotion ID shown in the catalog.",
|
| 42 |
-
},
|
| 43 |
-
"quantity": {
|
| 44 |
-
"type": "integer",
|
| 45 |
-
"description": "Quantity the user wants to buy.",
|
| 46 |
-
},
|
| 47 |
-
},
|
| 48 |
-
"required": ["promotion_id", "quantity"],
|
| 49 |
-
},
|
| 50 |
-
},
|
| 51 |
-
},
|
| 52 |
-
{
|
| 53 |
-
"type": "function",
|
| 54 |
-
"function": {
|
| 55 |
-
"name": "purchase_product",
|
| 56 |
-
"description": "Call after user confirms their personal information and want to place an order.",
|
| 57 |
-
"parameters": {
|
| 58 |
-
"type": "object",
|
| 59 |
-
"properties": {
|
| 60 |
-
"promotion_id": {
|
| 61 |
-
"type": "string",
|
| 62 |
-
"description": "Promotion ID shown in the catalog.",
|
| 63 |
-
},
|
| 64 |
-
"quantity": {
|
| 65 |
-
"type": "integer",
|
| 66 |
-
"description": "Quantity the user wants to buy.",
|
| 67 |
-
},
|
| 68 |
-
"name": {
|
| 69 |
-
"type": "string",
|
| 70 |
-
"description": "Customer name in Thai.",
|
| 71 |
-
},
|
| 72 |
-
"surname": {
|
| 73 |
-
"type": "string",
|
| 74 |
-
"description": "Customer surname in Thai.",
|
| 75 |
-
},
|
| 76 |
-
"address": {
|
| 77 |
-
"type": "string",
|
| 78 |
-
"description": "Customer delivery address.",
|
| 79 |
-
},
|
| 80 |
-
"tel": {
|
| 81 |
-
"type": "string",
|
| 82 |
-
"description": "Customer telephone number.",
|
| 83 |
-
},
|
| 84 |
-
},
|
| 85 |
-
"required": ["promotion_id", "quantity", "name","surname", "address", "tel"],
|
| 86 |
-
},
|
| 87 |
-
},
|
| 88 |
-
},
|
| 89 |
-
{
|
| 90 |
-
"type": "function",
|
| 91 |
-
"function": {
|
| 92 |
-
"name": "call_admin",
|
| 93 |
-
"description": "Use this when there is an issue at any stage of the process, such as being unable to retrieve the order, an incorrect address, or a failed system submission.",
|
| 94 |
-
"parameters": {
|
| 95 |
-
"type": "object",
|
| 96 |
-
"properties": {
|
| 97 |
-
"cause": {
|
| 98 |
-
"type": "string",
|
| 99 |
-
"description": "Short description of the problem.",
|
| 100 |
-
}
|
| 101 |
-
},
|
| 102 |
-
"required": ["cause"],
|
| 103 |
-
},
|
| 104 |
-
},
|
| 105 |
-
},
|
| 106 |
-
]
|
| 107 |
-
promotion_catalog = {
|
| 108 |
-
"00001": 2900,
|
| 109 |
-
"00002": 1890,
|
| 110 |
-
"00003": 2900,
|
| 111 |
-
"00004": 2900,
|
| 112 |
-
"00005": 2900,
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def _update_asr_stage(stage: str) -> None:
|
| 117 |
-
"""Switch the ASR stage safely without breaking tool execution."""
|
| 118 |
-
try:
|
| 119 |
-
from backend.asr import set_asr_stage
|
| 120 |
-
|
| 121 |
-
set_asr_stage(stage)
|
| 122 |
-
except Exception as exc:
|
| 123 |
-
print(f"Unable to switch ASR stage to '{stage}': {exc}")
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def _format_tool_result(result: Any) -> str:
|
| 127 |
-
"""Convert tool outputs to a JSON string for the LLM."""
|
| 128 |
-
if isinstance(result, str):
|
| 129 |
-
return result
|
| 130 |
-
try:
|
| 131 |
-
return json.dumps(result, ensure_ascii=False)
|
| 132 |
-
except TypeError:
|
| 133 |
-
return json.dumps({"result": str(result)}, ensure_ascii=False)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def summary_order(promotion_id: str, quantity: int) -> Dict[str, Any]:
|
| 137 |
-
"""Return a structured order summary for the requested promotion."""
|
| 138 |
-
_update_asr_stage("typhoon")
|
| 139 |
-
# print(f"change stage to gpt for summary_order")
|
| 140 |
-
if promotion_id not in promotion_catalog:
|
| 141 |
-
return {"status": "error", "message": "ไม่พบโปรโมชันที่เลือก"}
|
| 142 |
-
|
| 143 |
-
unit_price = promotion_catalog[promotion_id]
|
| 144 |
-
total_price = unit_price * quantity
|
| 145 |
-
return {
|
| 146 |
-
"status": "ok",
|
| 147 |
-
"promotion_id": promotion_id,
|
| 148 |
-
"unit_price": unit_price,
|
| 149 |
-
"quantity": quantity,
|
| 150 |
-
"total_price": total_price,
|
| 151 |
-
"currency": "THB",
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def call_admin(cause: str, chat_history : List[Dict[str, str]]):
|
| 156 |
-
#transfer the call to admin..
|
| 157 |
-
payload = {
|
| 158 |
-
|
| 159 |
-
"cause":cause,
|
| 160 |
-
"history":chat_history
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
try:
|
| 165 |
-
# 3. Make the API call with a timeout to prevent it from hanging
|
| 166 |
-
response = requests.post(CALL_ADMIN_URL, json=payload, timeout=20) # 10-second timeout
|
| 167 |
-
|
| 168 |
-
# 4. Check if the request was successful (i.e., status code 2xx)
|
| 169 |
-
# If not, this will raise an HTTPError, which is caught by the except block.
|
| 170 |
-
response.raise_for_status()
|
| 171 |
-
|
| 172 |
-
# 5. If successful, return the success status and the data from the response
|
| 173 |
-
return {
|
| 174 |
-
"status": "success",
|
| 175 |
-
"data": response.json() # Assumes the API returns JSON
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
except requests.exceptions.RequestException as e:
|
| 179 |
-
# 6. Catch any network-related errors (timeout, connection error, HTTP error)
|
| 180 |
-
print(f"An error occurred during the purchase request: {e}") # For logging
|
| 181 |
-
return {
|
| 182 |
-
"status": "error",
|
| 183 |
-
"message": "ไม่สามารถเชื่อมต่อกับระบบเพื่อทำรายการได้"
|
| 184 |
-
}
|
| 185 |
-
except:
|
| 186 |
-
return {
|
| 187 |
-
"status": "error",
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def purchase_product(
|
| 193 |
-
promotion_id: str,
|
| 194 |
-
quantity: int,
|
| 195 |
-
name: str,
|
| 196 |
-
surname: str,
|
| 197 |
-
address: str,
|
| 198 |
-
tel: str,
|
| 199 |
-
) -> Dict[str, Any]:
|
| 200 |
-
"""Mock purchase endpoint that validates input and returns a status payload."""
|
| 201 |
-
_update_asr_stage("typhoon")
|
| 202 |
-
if promotion_id not in promotion_catalog:
|
| 203 |
-
return {"status": "error", "message": "ไม่สามารถทำรายการได้"}
|
| 204 |
-
payload = {
|
| 205 |
-
"promotion_id": promotion_id,
|
| 206 |
-
"quantity": quantity,
|
| 207 |
-
"user_name": name + " " + surname,
|
| 208 |
-
"address": address,
|
| 209 |
-
"tel": tel
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
try:
|
| 213 |
-
# 3. Make the API call with a timeout to prevent it from hanging
|
| 214 |
-
response = requests.post(PURCHASE_URL, json=payload, timeout=20) # 10-second timeout
|
| 215 |
-
|
| 216 |
-
# 4. Check if the request was successful (i.e., status code 2xx)
|
| 217 |
-
# If not, this will raise an HTTPError, which is caught by the except block.
|
| 218 |
-
response.raise_for_status()
|
| 219 |
-
|
| 220 |
-
# 5. If successful, return the success status and the data from the response
|
| 221 |
-
return {
|
| 222 |
-
"status": "success",
|
| 223 |
-
"data": response.json() # Assumes the API returns JSON
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
except requests.exceptions.RequestException as e:
|
| 227 |
-
# 6. Catch any network-related errors (timeout, connection error, HTTP error)
|
| 228 |
-
print(f"An error occurred during the purchase request: {e}") # For logging
|
| 229 |
-
return {
|
| 230 |
-
"status": "error",
|
| 231 |
-
"message": "ไม่สามารถเชื่อมต่อกับระบบเพื่อทำรายการได้"
|
| 232 |
-
}
|
| 233 |
-
except json.JSONDecodeError:
|
| 234 |
-
# 7. Catch errors if the server response is not valid JSON
|
| 235 |
-
print("Failed to decode JSON from response.") # For logging
|
| 236 |
-
return {
|
| 237 |
-
"status": "error",
|
| 238 |
-
"message": "ได้รับข้อมูลจากระบบผิดพลาด กรุณาตรวจสอบความถูกต้องของข้อมูล"
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
PROMOTION_DESCRIPTION = """
|
| 243 |
-
ตัวอย่างรายการสินค้า จาก www.1577shop.com
|
| 244 |
-
|
| 245 |
-
**[2ฟรี2] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี เซรั่ม (5g.) 2 กระปุก**
|
| 246 |
-
Promotion_id = "00001"
|
| 247 |
-
|
| 248 |
-
**Original price was: ฿9,400.00. Current price is: ฿2,900.00.-70%**
|
| 249 |
-
|
| 250 |
-
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า
|
| 251 |
-
*เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย
|
| 252 |
-
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 253 |
-
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 254 |
-
-82% ของผู้ทด��องใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 255 |
-
|
| 256 |
-
5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 257 |
-
1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”
|
| 258 |
-
2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว
|
| 259 |
-
3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง
|
| 260 |
-
4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่วโมง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5
|
| 261 |
-
5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย
|
| 262 |
-
เลขที่ได้รับจดแจ้ง : 11-1-6600001843
|
| 263 |
-
|
| 264 |
-
สิ่งที่คุณจะได้รับ
|
| 265 |
-
1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก
|
| 266 |
-
2.Trylagina Age-Defying Collagen Serum 12X ขนาด 5 g. 2 กระปุก
|
| 267 |
-
|
| 268 |
-
https://www.1577shop.co.th/product/1577-shop-tgn-03
|
| 269 |
-
-------
|
| 270 |
-
## (1 ใหญ่) Minus 20 ไมนัสทเวนตี้ ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | 30 กรัม
|
| 271 |
-
promotion-id = "00002"
|
| 272 |
-
|
| 273 |
-
**Original price was: ฿3,200.00.Current price is: ฿1,890.00.-41%**
|
| 274 |
-
|
| 275 |
-
Minus20 Advanced Youth Anti-Wrinkle Cream
|
| 276 |
-
ครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ยูธ แอนไท ริงเคิล ครีม
|
| 277 |
-
|
| 278 |
-
ครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่
|
| 279 |
-
✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง
|
| 280 |
-
✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ชั่วโมง
|
| 281 |
-
✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลง
|
| 282 |
-
|
| 283 |
-
ไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิว
|
| 284 |
-
|
| 285 |
-
เนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ
|
| 286 |
-
|
| 287 |
-
*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล
|
| 288 |
-
• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720
|
| 289 |
-
• ขนาด 30 กรัม
|
| 290 |
-
|
| 291 |
-
✨วิธีใช้:
|
| 292 |
-
• หลังล้างหน้าทาครีมไ���นัสทเวนตี้ในปริมาณที่เหมาะสม
|
| 293 |
-
• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ
|
| 294 |
-
• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน
|
| 295 |
-
|
| 296 |
-
✨เคล็ดลับ :
|
| 297 |
-
• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม
|
| 298 |
-
• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด
|
| 299 |
-
|
| 300 |
-
สิ่งที่จะได้รับ
|
| 301 |
-
1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 1 กระปุก
|
| 302 |
-
|
| 303 |
-
[https://www.1577shop.co.th/product/1577-shop-msn-001](https:/www.1577shop.co.th/product/1577-shop-msn-001)
|
| 304 |
-
-------
|
| 305 |
-
## [2ฟรี2] Minus 20 (ไมนัสทเวนตี้) ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | เซตขายดี
|
| 306 |
-
promotion_id = "00003
|
| 307 |
-
|
| 308 |
-
**Original price was: ฿8,180.00.Current price is: ฿2,900.00.-65%**
|
| 309 |
-
|
| 310 |
-
Minus20 Advanced Youth Anti-Wrinkle Cream
|
| 311 |
-
ครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ยูธ แอนไท ริงเคิล ครีมครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่
|
| 312 |
-
✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง
|
| 313 |
-
✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ชั่วโมง
|
| 314 |
-
✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลงไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิวเนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล
|
| 315 |
-
• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720
|
| 316 |
-
• ขนาด 30 กรัม
|
| 317 |
-
|
| 318 |
-
✨วิธีใช้:
|
| 319 |
-
• หลังล้างหน้าทาครีมไมนัสทเวนตี้ในปริมาณที่เหมาะสม
|
| 320 |
-
• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ
|
| 321 |
-
• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน
|
| 322 |
-
|
| 323 |
-
✨เคล็ดลับ :
|
| 324 |
-
• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม
|
| 325 |
-
• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด
|
| 326 |
-
|
| 327 |
-
สิ่งที่จะได้รับ
|
| 328 |
-
1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 2 กระปุก
|
| 329 |
-
2. Minus20 Advanced Youth Anti-Wrinkle Cream (8g) จำนวน 2 กระปุก
|
| 330 |
-
|
| 331 |
-
https://www.1577shop.co.th/product/1577-shop-mns-003
|
| 332 |
-
-------
|
| 333 |
-
## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ทริปเปิลไมเซล่ามูสโฟม (150ml.)
|
| 334 |
-
promotion-id = "00004"
|
| 335 |
-
|
| 336 |
-
**Original price was: ฿8,800.00. Current price is: ฿2,900.00.-68%**
|
| 337 |
-
|
| 338 |
-
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด
|
| 339 |
-
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 340 |
-
|
| 341 |
-
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 342 |
-
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 343 |
-
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 344 |
-
|
| 345 |
-
*สิ่งที่คุณจะได้รับ
|
| 346 |
-
1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก
|
| 347 |
-
2.Trylagina Triple Micellar Mousse Foam ขนาด 150 ml. 1 ขวด
|
| 348 |
-
|
| 349 |
-
[https://www.1577shop.co.th/product/1577-shop-tgn-04](https:/www.1577shop.co.th/product/1577-shop-tgn-04)
|
| 350 |
-
-------
|
| 351 |
-
## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ครีมกันแดด (30g.)
|
| 352 |
-
promotion_id = "00005"
|
| 353 |
-
|
| 354 |
-
**Original price was: ฿8,690.00.Current price is: ฿2,900.00.-67%**
|
| 355 |
-
|
| 356 |
-
The Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด
|
| 357 |
-
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 358 |
-
|
| 359 |
-
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 360 |
-
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 361 |
-
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 362 |
-
|
| 363 |
-
*สิ่งที่คุณจะได้รับ
|
| 364 |
-
1. Trylagina Age-Defying Collagen Serum 12X ขนาด (30g.) 1 กระปุก
|
| 365 |
-
2. Trylagina Age Defense Sunscreen Serum SPF50+ PA++++ ขนาด (30g.) 1 หลอด
|
| 366 |
-
|
| 367 |
-
https://www.1577shop.co.th/product/1577-shop-tgn-05
|
| 368 |
-
-------
|
| 369 |
-
TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า *เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย
|
| 370 |
-
|
| 371 |
-
-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น
|
| 372 |
-
-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้
|
| 373 |
-
-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน
|
| 374 |
-
|
| 375 |
-
5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว
|
| 376 |
-
1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”
|
| 377 |
-
2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว
|
| 378 |
-
3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง
|
| 379 |
-
4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่วโมง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5
|
| 380 |
-
5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย
|
| 381 |
-
|
| 382 |
-
ขอแนะนําวิธีการใช้ผลิตภัณฑ์ เพื่อการฟื้นฟูผิวอย่างเต็มประสิทธิภาพ
|
| 383 |
-
STEP 1 หลังทำความสะอาดผิวหน้า เตรียมผิวพร้อมรับการบำรุง ใช้ไม้พายตักเซรั่มลงบนหลังมือ
|
| 384 |
-
STEP 2 แต้มเซรั่มลงบนผิวหน้า 5 จุด ได้แก่บริเวณ หน้าผาก ปลายจมูก พวงแก้มซ้าย พวงแก้มขวา และปลายคาง
|
| 385 |
-
STEP 3 ลูบไล้เซรั่มด้วยปลายนิ้วทั้งสองข้าง เพื่อปลดปล่อยคุณค่าของสารสกัด จากนั้นจึงกดเบาๆ ลงบนผิวหน้าและลำคอ ใช้ทั้งเช้าและเย็น
|
| 386 |
-
เลขที่ได้รับจดแจ้ง : 11-1-6600001843"""
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
def get_promotion_description(permission: str) -> Any:
|
| 390 |
-
"""Return the long-form promotion description after explicit confirmation."""
|
| 391 |
-
if permission.strip().lower() != "yes":
|
| 392 |
-
return {
|
| 393 |
-
"status": "error",
|
| 394 |
-
"message": "ต้องได้รับการยืนยันจากลูกค้าก่อนแสดงรายละเอียดโปรโมชั่น",
|
| 395 |
-
}
|
| 396 |
-
return {"status": "ok", "description": PROMOTION_DESCRIPTION}
|
| 397 |
-
# return {"status": "ok", "description": "test"}
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
TOOL_FUNCTIONS = {
|
| 401 |
-
"get_promotion_description": get_promotion_description,
|
| 402 |
-
"summary_order": summary_order,
|
| 403 |
-
"purchase_product": purchase_product,
|
| 404 |
-
"call_admin": call_admin
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
def execute_tool(name: str, arguments: Dict[str, Any]) -> str:
|
| 409 |
-
"""Execute a registered tool and return a JSON-serialised string result."""
|
| 410 |
-
function = TOOL_FUNCTIONS.get(name)
|
| 411 |
-
if not function:
|
| 412 |
-
return json.dumps(
|
| 413 |
-
{"status": "error", "message": f"Unknown tool '{name}'"},
|
| 414 |
-
ensure_ascii=False,
|
| 415 |
-
)
|
| 416 |
-
|
| 417 |
-
try:
|
| 418 |
-
result = function(**arguments)
|
| 419 |
-
except TypeError as exc:
|
| 420 |
-
return json.dumps(
|
| 421 |
-
{"status": "error", "message": f"Invalid arguments for '{name}': {exc}"},
|
| 422 |
-
ensure_ascii=False,
|
| 423 |
-
)
|
| 424 |
-
except Exception as exc: # noqa: BLE001
|
| 425 |
-
return json.dumps({"status": "error", "message": str(exc)}, ensure_ascii=False)
|
| 426 |
-
|
| 427 |
-
return _format_tool_result(result)
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
if __name__ == "__main__":
|
| 431 |
-
result = call_admin(
|
| 432 |
-
cause="ตัวอย่างปัญหา: ไม่สามารถทำรายการสั่งซื้อได้",
|
| 433 |
-
chat_history=[{'role': 'assistant', 'metadata': None, 'content': 'สวัสดีค่ะณภัทร 1577Homeshopping ยินดีให้บริการค่ะ', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'สวัสดีครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'สนใจโปรโมชั่นไหนเป็นพิเศษไหมคะหรืออยากให้ณภัทรแนะนำโปรโมชั่นที่กำลังฮิตอยู่ตอนนี้ให้ดีคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'โปรโมชั่นครับที่กําลังฮิตก็ได้ครับ', 'options': None}, {'role': 'assistant', 'content': None, 'tool_calls': [{'id': 'function-call-2730241149475294520', 'type': 'function', 'function': {'name': 'get_promotion_description', 'arguments': '{"permission":"yes"}'}}]}, {'role': 'tool', 'tool_call_id': 'function-call-2730241149475294520', 'content': '{"status": "ok", "description": "\\nตัวอย่างรายการสินค้า จาก www.1577shop.com\\n\\n**[2ฟรี2] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี เซรั่ม (5g.) 2 กระปุก**\\nPromotion_id = \\"00001\\"\\n\\n**Original price was: ฿9,400.00. Current price is: ฿2,900.00.-70%**\\n\\nThe Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด TRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า\\n*เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย\\n-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น\\n-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้\\n-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน\\n\\n5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว\\n1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”\\n2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว\\n3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง\\n4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่วโมง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5\\n5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย\\nเลขที่ได้รับจดแจ้ง : 11-1-6600001843\\n\\nสิ่งที่คุณจะได้รับ\\n1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก\\n2.Trylagina Age-Defying Collagen Serum 12X ขนาด 5 g. 2 กระปุก\\n\\nhttps://www.1577shop.co.th/product/1577-shop-tgn-03\\n-------\\n## (1 ใหญ่) Minus 20 ไมนัสทเวนตี้ ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | 30 กรัม\\npromotion-id = \\"00002\\"\\n\\n**Original price was: ฿3,200.00.Current price is: ฿1,890.00.-41%**\\n\\nMinus20 Advanced Youth Anti-Wrinkle Cream\\nครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ยูธ แอนไท ริงเคิล ครีม\\n\\nครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่\\n✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง\\n✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ช���่วโมง\\n✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลง\\n\\nไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิว\\n\\nเนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ\\n\\n*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล\\n• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720\\n• ขนาด 30 กรัม\\n\\n✨วิธีใช้:\\n• หลังล้างหน้าทาครีมไมนัสทเวนตี้ในปริมาณที่เหมาะสม\\n• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ\\n• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน\\n\\n✨เคล็ดลับ :\\n• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม\\n• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด\\n\\nสิ่งที่จะได้รับ\\n1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 1 กระปุก\\n\\n[https://www.1577shop.co.th/product/1577-shop-msn-001](https:/www.1577shop.co.th/product/1577-shop-msn-001)\\n-------\\n## [2ฟรี2] Minus 20 (ไมนัสทเวนตี้) ครีมเกล็ดทองคำ ผิวตึง ฉ่ำ ไบร์ท | เซตขายดี\\npromotion_id = \\"00003\\n\\n**Original price was: ฿8,180.00.Current price is: ฿2,900.00.-65%**\\n\\nMinus20 Advanced Youth Anti-Wrinkle Cream\\nครีมบำรุงผิวหน้า ไมนัสทเวนตี้ แอดวานซ์ ยูธ แอนไท ริงเคิล ครีมครีมสูตรเฉพาะเพื่อผิววัย 40+ 50+ 60+ ช่วยตอบโจทย์ปัญหาผิวสาวไทยวัยกลางคน ที่ผ่านช่วงเวลาแห่งการเปลี่ยนแปลง ส่งผลให้มีริ้วรอยร่องลึก ผิวแห้ง หน้าหมองคล้ำ มีฝ้า จุดด่างดำฝังลึก และที่ครีมเดิมที่คุณเคยใช้อาจไม่ได้ผลต่อไป เพราะเราเข้าใจทุกปัญหาจึงพัฒนาไมนัสทเวนตี้มาเป็นสูตรแอดวานซ์ ยูธ เพื่อให้คุณได้สัมผัสผลลัพธ์ผิวที่\\n✨ ตึงกว่าเดิม ริ้วรอยร่องลึกจางลง\\n✨ ฉ่ำกว่าเดิม ชุ่มชื้นอิ่มน้ำยาวนานมากกว่า 24 ชั่วโมง\\n✨ ไบร์ทกว่าเดิม หน้าและดูกระจ่างใส จุดด่างดำจางลงไมนัสทเวนตี้ คัดสรรสารสกัดที่ช่วยตอบโจทย์ปัญหาผิววัยเราอย่างตรงจุดการันตีคุณภาพกลไกลการทำสารของสารกัดด้วยได้รับรางวัลโนเบล แต่ยังอ่อนโยนเหมาะกับทุกสภาพผิวเนื้อผสมครีมเกล็ดทองคำบริสุทธิ์ 99.99% อนุภาคทองละเอียดซึมลึก สัมผัสบางเบา เกลี่ยง่าย ซึมไว ไม่เหนียว ไม่เหนอะหนะ*ผลลัพธ์ขึ้นอยู่กับแต่ละสภาพผิวแต่ละบุคคล\\n• เลขที่ใบรับจดเเจ้ง : 74-1-6600008720\\n• ขนาด 30 กรัม\\n\\n✨วิธีใช้:\\n• หลังล้างหน้าทาครีมไมนัสทเวนตี้ในปริมาณที่เ��มาะสม\\n• แตะและลูบไล้เบาๆลงบนผิวหน้าและลำคอ\\n• ใช้ทาเช้าและเย็น เป็นประจำทุกวัน\\n\\n✨เคล็ดลับ :\\n• เก็บครีมบำรุงผิวในอุณหภูมิที่เหมาะสม\\n• ทาครีมกันแดดเป็นประจำทุกวัน เพื่อปกป้องผิวจากแสงแดด\\n\\nสิ่งที่จะได้รับ\\n1. Minus20 Advanced Youth Anti-Wrinkle Cream (30g) จำนวน 2 กระปุก\\n2. Minus20 Advanced Youth Anti-Wrinkle Cream (8g) จำนวน 2 กระปุก\\n\\nhttps://www.1577shop.co.th/product/1577-shop-mns-003\\n-------\\n## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ทริปเปิลไมเซล่ามูสโฟม (150ml.)\\npromotion-id = \\"00004\\"\\n\\n**Original price was: ฿8,800.00. Current price is: ฿2,900.00.-68%**\\n\\nThe Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด\\nTRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว\\n\\n-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น\\n-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้\\n-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน\\n\\n*สิ่งที่คุณจะได้รับ\\n1.Trylagina Age-Defying Collagen Serum 12X ขนาด 30 g. 2 กระปุก\\n2.Trylagina Triple Micellar Mousse Foam ขนาด 150 ml. 1 ขวด\\n\\n[https://www.1577shop.co.th/product/1577-shop-tgn-04](https:/www.1577shop.co.th/product/1577-shop-tgn-04)\\n-------\\n## [2ฟรี1] Trylagina 12X (ไตรลาจิน่า 12X) เซรั่มคืนความอ่อนเยาว์ (30g.) 2 กระปุก ฟรี ครีมกันแดด (30g.)\\npromotion_id = \\"00005\\"\\n\\n**Original price was: ฿8,690.00.Current price is: ฿2,900.00.-67%**\\n\\nThe Sense of Youth Infinity รู้สึกถึงความอ่อนเยาว์ไม่มีที่สิ้นสุด\\nTRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า ผสาน 5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว\\n\\n-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกระชับขึ้น\\n-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้\\n-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน\\n\\n*สิ่งที่คุณจะได้รับ\\n1. Trylagina Age-Defying Collagen Serum 12X ขนาด (30g.) 1 กระปุก\\n2. Trylagina Age Defense Sunscreen Serum SPF50+ PA++++ ขนาด (30g.) 1 หลอด\\n\\nhttps://www.1577shop.co.th/product/1577-shop-tgn-05\\n-------\\nTRYLAGINA Age-Defying Collagen Serum 12X การพัฒนาที่ไม่สิ้นสุด สู่ผลิตภัณฑ์ที่ยอดเยี่ยม เพื่อให้คุณได้สัมผัสความอ่อนเยาว์ ตามแบบฉบับของ ไตรลาจีน่า เซรั่มสูตรใหม่ ดีขึ้น 12 เท่า *เมื่อเทียบเท่ากับการใช้ Trylagina Intensive Collagen Mask 12 ครั้ง ช่วยฟื้นบำรุงอย่างล้ำลึก ถึง 5 มิติ เผยผิวใหม่อ่อนเยาว์กว่าที่เคย\\n\\n-90% ของผู้ทดลองใช้พบว่าริ้วรอยลดลง และกร��ชับขึ้น\\n-87% ของผู้ทดลองใช้พบว่าผิวได้รับความชุ่มชื้นทันทีที่ใช้\\n-82% ของผู้ทดลองใช้พบว่าผิวกระจ่างใสใน 7 วัน\\n\\n5 สารสกัดสําคัญ ที่คิดค้นมาเพื่อแก้ปัญหาผิว\\n1. 5D Matrixcolla ช่วยกระตุ้นการสร้าง Collagen ให้ผิวคืน “ความอ่อนเยาว์”\\n2. HyWhite® ช่วยลดเลือนฝ้า กระ จุดด่างดำ และ a-Linoleic Acid มีคุณสมบัติช่วยให้ผิวขาวกระจ่างใส เพิ่มความสม่ำเสมอให้กับสีผิว\\n3. สารสกัดยีสต์จาก ดอกโฮยาคาร์โนซ่า เติมความแข็งแรงให้กับผิว มอบความแข็งแรงให้กับผิวจากภายใน ช่วยให้ผิวสุขภาพดี เปล่งปลั่ง\\n4. PatcH2O® ช่วยให้ผิวชุ่มชื่นทันที พร้อมล็อกผิวให้ฉ่ำน้ำยาวนาน ถึง 5 วัน หรือ 120 ชั่วโมง *เมื่อใช้เป็นประจำต่อเนื่อง และ สามารถปกป้องผิวจาก มลภาวะฝุ่น PM2.5\\n5. สาหร่ายซุยเซ็นจิ จากประเทศญี่ปุ่น ซึ่งเติบโตในแหล่งน้ำที่อุดมไปด้วยแร่ธาตุจากภูเขาไฟอะโซะ (Aso) เกาะคิวชู (Kyoshu) ช่วยปลอบประโลมผิว ให้ผิวรู้สึกผ่อนคลาย\\n\\nขอแนะนําวิธีการใช้ผลิตภัณฑ์ เพื่อการฟื้นฟูผิวอย่างเต็มประสิทธิภาพ\\nSTEP 1 หลังทำความสะอาดผิวหน้า เตรียมผิวพร้อมรับการบำรุง ใช้ไม้พายตักเซรั่มลงบนหลังมือ\\nSTEP 2 แต้มเซรั่มลงบนผิวหน้า 5 จุด ได้แก่บริเวณ หน้าผาก ปลายจมูก พวงแก้มซ้าย พวงแก้มขวา และปลายคาง\\nSTEP 3 ลูบไล้เซรั่มด้วยปลายนิ้วทั้งสองข้าง เพื่อปลดปล่อยคุณค่าของสารสกัด จากนั้นจึงกดเบาๆ ลงบนผิวหน้าและลำคอ ใช้ทั้งเช้าและเย็น\\nเลขที่ได้รับจดแจ้ง : 11-1-6600001843"}'}, {'role': 'assistant', 'metadata': None, 'content': 'ณภัทรแนะนำโปรโมชั่นสุดฮิตให้ 3 รายการนี้นะคะ| โปรโมชั่นแรกเป็นไตรลาจิน่า 12X เซรั่มคืนความอ่อนเยาว์ 2 กระปุกฟรี 2 กระปุกเล็ก| ราคาพิเศษ 2,900 บาทค่ะ|\n\nโปรโมชั่นที่สองคือไมนัสทเวนตี้ครีมเกล็ดทองคำ 1 กระปุกค่ะ| ช่วยให้ผิวตึงฉ่ำไบร์ทราคาพิเศษ 1,890 บาทค่ะ|\n\nส่วนโปรโมชั่นที่สามเป็นไมนัสทเวนตี้ครีมเกล็ดทองคำเซตขายดี 2 กระปุกฟรี 2 กระปุกเล็กค่ะ| ราคาพิเศษ 2,900 บาทค่ะ| สนใจโปรโมชั่นไหนเป็นพิเศษไหมคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'เอาอันแรกครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รับเป็นไตรลาจิน่า 12X เซรั่มคืนความอ่อนเยาว์ 2 กระปุกฟรี 2 กระปุกเล็กนะคะ| รับกี่เซตดีคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'ครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รับกี่เซตดีคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'หนึ่งครับ', 'options': None}, {'role': 'assistant', 'content': None, 'tool_calls': [{'id': 'function-call-2224878481094213305', 'type': 'function', 'function': {'name': 'summary_order', 'arguments': '{"quantity":1,"promotion_id":"00001"}'}}]}, {'role': 'tool', 'tool_call_id': 'function-call-2224878481094213305', 'content': '{"status": "ok", "promotion_id": "00001", "unit_price": 2900, "quantity": 1, "total_price": 2900, "currency": "THB"}'}, {'role': 'assistant', 'metadata': None, 'content': 'สรุปรายการนะคะ| ไตรลาจิน่า 12X เซรั่มคืนความอ่อนเยาว์ 1 เซตราคา 2,900 บาทถูกต้องไหมคะ| การชำระเงินจะเป็นแบบเก็บเงินปลายทางอย่างเดียวนะคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'ถูกต้องครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รบกวนขอชื่อจริงลูกค้าด้วยค่ะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'จุลจักรครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'นามสกุลอะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'กาญจนาเอกครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รบกวนขอที่อยู่สำหรับจัดส่งด้วยนะคะ| ขอเป็นจังหวัดก่อนค่ะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'กรุงเทพครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'เขตหรืออำเภออะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'น้องจอกครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'แขวงหรือตำบลอะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'บรรทัดชีครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ถนนอะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'สวินทวงศ์ครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'หมู่บ้านหรือชื่ออาคารอะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'บ้านฟอร่าวิวครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ซอยอะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'สุเวนทวงศ์สามแปดครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'บ้านเลขที่อะไรคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'เก้าสิบสองทับสิบห้าครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รบกวนขอรหัสไปรษณีย์ด้วยนะคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'หนึ่งศูนย์ห้าสามศูนย์', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'รบกวนขอเบอร์โทรศัพท์ด้วยนะคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'ศูนย์หกสาม เจ็ดห้าสาม หนึ่งสามเจ็ดเก้าครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ขอทวนข้อมูลการจัดส่งอีกครั้งนะคะ| คุณจุลจักรกาญจนาเอก| ที่อยู่กรุงเทพมหานครเขตหนองจอกแขวงลำผักชีถนนสุวินทวงศ์หมู่บ้านฟอร่าวิวซอยสุวินทวงศ์ 38 บ้านเลขที่ 92/15 รหัสไปรษณีย์ 10530| เบอร์โทรศัพท์ 0637531379| ข้อมูลถูกต้องไหมคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'ไม่ถูกครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ไม่ถูกต้องตรงส่วนไหนคะ| รบกวนแจ้งอีกครั้งได้เลยค่ะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'นามสกุลกาญจนะเอกิงครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ขอโทษด้วยนะคะ| ขอทวนนามสกุลเป็นกาญจนะเอกินนะคะ| ส่วนที่อยู่เป็นกรุงเทพมหานครเขตหนองจอกแขวงลำผักชีถนนสุวินทวงศ์หมู่บ้านฟอร่าวิวซอยสุวินทวงศ์ 38 บ้านเลขที่ 92/15 รหัสไปรษณีย์ 10530| เบอร์โทรศัพท์ 0637531379| ข้อมูลถูกต้องไหมคะ|', 'options': None}, {'role': 'user', 'metadata': None, 'content': 'ไม่ใช่ครับ', 'options': None}, {'role': 'assistant', 'metadata': None, 'content': 'ไม่ถูกต้องตรงส่วนไหนคะ| รบกวนแจ้งอีกครั้งได้เลยค่ะ|', 'options': None}, {'role': 'user', 'content': 'นามสกุลกาญจนาเอกินครับ'}]
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
)
|
| 437 |
-
print(result)
|
| 438 |
-
result = purchase_product(
|
| 439 |
-
promotion_id="00001",
|
| 440 |
-
quantity=1,
|
| 441 |
-
name="สมชาย",
|
| 442 |
-
surname="ใจดี",
|
| 443 |
-
address="123 ถนนสุขุมวิท กรุงเทพฯ 10110",
|
| 444 |
-
tel="0812345678"
|
| 445 |
-
)
|
| 446 |
-
print(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/tts.py
CHANGED
|
@@ -41,9 +41,7 @@ def synthesize_text(text: str, lang = 'th' , speed = 2.0):
|
|
| 41 |
' 4.': ' สี่.', '/n4.': ' สี่.', ' 10.': ' สิบ.', '/n10.': ' สิบ.',
|
| 42 |
'พ.ศ.': 'พอศอ', '. ': ' ', '-19': ' 19', 'เพื่อยก': 'เพื่อ ยก',
|
| 43 |
'√': 'เครื่องหมายติ๊กถูก', '=>': 'จากนั้นเลือก', 'รอกด': 'รอ กด', ' ณ ': ' นะ ',
|
| 44 |
-
'[2ฟรี1]': 'สองฟรีหนึ่ง', "+": 'บวก', "12X": "สิบสองเอ็กซ์", "12x": "สิบสองเอ็กซ์"
|
| 45 |
-
"/": "ทับ"
|
| 46 |
-
|
| 47 |
}
|
| 48 |
for old, new in replacements.items():
|
| 49 |
text = text.replace(old, new)
|
|
|
|
| 41 |
' 4.': ' สี่.', '/n4.': ' สี่.', ' 10.': ' สิบ.', '/n10.': ' สิบ.',
|
| 42 |
'พ.ศ.': 'พอศอ', '. ': ' ', '-19': ' 19', 'เพื่อยก': 'เพื่อ ยก',
|
| 43 |
'√': 'เครื่องหมายติ๊กถูก', '=>': 'จากนั้นเลือก', 'รอกด': 'รอ กด', ' ณ ': ' นะ ',
|
| 44 |
+
'[2ฟรี1]': 'สองฟรีหนึ่ง', "+": 'บวก', "12X": "สิบสองเอ็กซ์", "12x": "สิบสองเอ็กซ์"
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
for old, new in replacements.items():
|
| 47 |
text = text.replace(old, new)
|
backend/utils.py
CHANGED
|
@@ -14,8 +14,7 @@ except ModuleNotFoundError:
|
|
| 14 |
torch = None # type: ignore
|
| 15 |
|
| 16 |
warnings.filterwarnings("ignore")
|
| 17 |
-
|
| 18 |
-
|
| 19 |
|
| 20 |
# --- Device Configuration ---
|
| 21 |
def get_device():
|
|
@@ -28,8 +27,6 @@ def get_device():
|
|
| 28 |
return "mps"
|
| 29 |
else:
|
| 30 |
return "cpu"
|
| 31 |
-
if get_device() == "mps":
|
| 32 |
-
load_dotenv(override=True)
|
| 33 |
|
| 34 |
device = get_device()
|
| 35 |
print(f"Using device: {device}")
|
|
@@ -46,7 +43,6 @@ def get_sync_credentials(ttl=360_000):
|
|
| 46 |
def setup_gcp_credentials():
|
| 47 |
"""Sets up Google Cloud credentials from an environment variable."""
|
| 48 |
gcp_service_account_json_str = os.getenv("GCP_SERVICE_ACCOUNT_JSON")
|
| 49 |
-
# print(gcp_service_account_json_str)
|
| 50 |
if gcp_service_account_json_str:
|
| 51 |
try:
|
| 52 |
# Create a temporary file to store the credentials
|
|
@@ -66,150 +62,47 @@ def setup_gcp_credentials():
|
|
| 66 |
# print("Warning: GCP_SERVICE_ACCOUNT_JSON is not set; Google Cloud clients may fail.")
|
| 67 |
# return gcp_service_account_json_str
|
| 68 |
# --- Audio Processing ---
|
| 69 |
-
# def audiosegment_to_numpy(audio, target_sample_rate=16000):
|
| 70 |
-
# samples = np.array(audio.get_array_of_samples(), dtype=np.float32)
|
| 71 |
-
# if audio.channels > 1:
|
| 72 |
-
# samples = samples.reshape((-1, audio.channels)).mean(axis=1)
|
| 73 |
-
# if audio.frame_rate != target_sample_rate:
|
| 74 |
-
# samples = librosa.resample(samples, orig_sr=audio.frame_rate, target_sr=target_sample_rate)
|
| 75 |
-
# samples /= np.iinfo(audio.array_type).max
|
| 76 |
-
# return samples
|
| 77 |
-
|
| 78 |
def audiosegment_to_numpy(audio, target_sample_rate=16000):
|
| 79 |
-
"""
|
| 80 |
-
Convert pydub.AudioSegment to normalized numpy array in range [-1, 1].
|
| 81 |
-
"""
|
| 82 |
samples = np.array(audio.get_array_of_samples(), dtype=np.float32)
|
| 83 |
if audio.channels > 1:
|
| 84 |
samples = samples.reshape((-1, audio.channels)).mean(axis=1)
|
| 85 |
-
|
| 86 |
-
# Normalize to [-1, 1]
|
| 87 |
samples /= np.iinfo(audio.array_type).max
|
| 88 |
|
| 89 |
-
# Resample if needed
|
| 90 |
if audio.frame_rate != target_sample_rate:
|
| 91 |
samples = librosa.resample(samples, orig_sr=audio.frame_rate, target_sr=target_sample_rate)
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
max_val = np.max(np.abs(samples))
|
| 95 |
-
if max_val > 0:
|
| 96 |
-
samples = samples / max_val
|
| 97 |
-
|
| 98 |
-
return samples.astype(np.float32)
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def preprocess_audio(audio, target_channels=1, target_sr=16000):
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
|
| 105 |
Args:
|
| 106 |
-
audio: tuple (sample_rate, audio_array)
|
|
|
|
|
|
|
|
|
|
| 107 |
Returns:
|
| 108 |
-
|
| 109 |
"""
|
| 110 |
-
target_frame_rate = target_sr
|
| 111 |
-
|
| 112 |
sample_rate, audio_array = audio
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
np.save(f, audio_array)
|
| 116 |
-
print(audio_array)
|
| 117 |
-
print(audio_array[0])
|
| 118 |
-
print(len(audio_array[0]))
|
| 119 |
-
print(audio_array.dtype)
|
| 120 |
-
|
| 121 |
-
# Convert to int16 PCM if needed
|
| 122 |
-
# If input is already float, scale it correctly
|
| 123 |
-
if audio_array.dtype != np.int16:
|
| 124 |
-
audio_array = np.clip(audio_array, -1.0, 1.0)
|
| 125 |
-
audio_array_int16 = (audio_array * 32767).astype(np.int16)
|
| 126 |
-
else:
|
| 127 |
-
audio_array_int16 = audio_array
|
| 128 |
-
|
| 129 |
-
# Wrap as BytesIO for AudioSegment
|
| 130 |
audio_bytes = audio_array_int16.tobytes()
|
| 131 |
audio_io = io.BytesIO(audio_bytes)
|
| 132 |
-
|
| 133 |
-
# Convert to AudioSegment
|
| 134 |
-
segment = AudioSegment.from_raw(
|
| 135 |
-
audio_io,
|
| 136 |
-
sample_width=2,
|
| 137 |
-
frame_rate=sample_rate,
|
| 138 |
-
channels=1
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
# Adjust channels & frame rate
|
| 142 |
segment = segment.set_channels(target_channels)
|
| 143 |
segment = segment.set_frame_rate(target_frame_rate)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
tuple: (target_sr, normalized_audio)
|
| 159 |
-
"""
|
| 160 |
-
original_sr, audio_array = audio
|
| 161 |
-
|
| 162 |
-
# Ensure audio_array is float
|
| 163 |
-
if audio_array.dtype not in [np.float32, np.float64]:
|
| 164 |
-
# Normalize int16 or other int types to [-1, 1]
|
| 165 |
-
audio_array = audio_array.astype(np.float32) / np.iinfo(audio_array.dtype).max
|
| 166 |
-
|
| 167 |
-
# Ensure audio is mono
|
| 168 |
-
# Assumes channels are in the first dimension if it's 2D
|
| 169 |
-
if audio_array.ndim > 1 and audio_array.shape[0] > 1:
|
| 170 |
-
audio_array = np.mean(audio_array, axis=0)
|
| 171 |
-
# If shape is (1, N), flatten it to (N,)
|
| 172 |
-
audio_array = audio_array.flatten()
|
| 173 |
-
|
| 174 |
-
# Resample if needed
|
| 175 |
-
if original_sr != target_sr:
|
| 176 |
-
audio_array = librosa.resample(y=audio_array, orig_sr=original_sr, target_sr=target_sr)
|
| 177 |
-
|
| 178 |
-
# Peak normalization
|
| 179 |
-
max_val = np.max(np.abs(audio_array))
|
| 180 |
-
if max_val > 0:
|
| 181 |
-
audio_array = audio_array / max_val
|
| 182 |
-
|
| 183 |
-
return (target_sr, audio_array.astype(np.float32))
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def is_valid_turn(turn: dict) -> bool:
|
| 188 |
-
"""
|
| 189 |
-
Checks if a conversation turn is valid for inclusion in the LLM history.
|
| 190 |
-
A turn is valid if it has a role and meets role-specific criteria:
|
| 191 |
-
- user: must have non-empty content.
|
| 192 |
-
- assistant: must have EITHER non-empty content OR tool_calls.
|
| 193 |
-
- tool: must have content and a tool_call_id.
|
| 194 |
-
"""
|
| 195 |
-
if not isinstance(turn, dict) or "role" not in turn:
|
| 196 |
-
return False
|
| 197 |
-
|
| 198 |
-
role = turn.get("role")
|
| 199 |
-
|
| 200 |
-
if role == "user":
|
| 201 |
-
# User turn is valid only if it has non-empty text content.
|
| 202 |
-
return bool(turn.get("content") and isinstance(turn.get("content"), str) and turn.get("content").strip())
|
| 203 |
-
|
| 204 |
-
elif role == "assistant":
|
| 205 |
-
# Assistant turn is valid if it has text content OR if it has tool_calls.
|
| 206 |
-
has_content = bool(turn.get("content") and isinstance(turn.get("content"), str) and turn.get("content").strip())
|
| 207 |
-
has_tool_calls = "tool_calls" in turn and turn["tool_calls"] is not None
|
| 208 |
-
return has_content or has_tool_calls
|
| 209 |
-
|
| 210 |
-
elif role == "tool":
|
| 211 |
-
# Tool turn is valid if it has a tool_call_id and content.
|
| 212 |
-
return "tool_call_id" in turn and "content" in turn
|
| 213 |
-
|
| 214 |
-
# Reject any other roles or malformed turns.
|
| 215 |
-
return False
|
|
|
|
| 14 |
torch = None # type: ignore
|
| 15 |
|
| 16 |
warnings.filterwarnings("ignore")
|
| 17 |
+
# load_dotenv(override = True)
|
|
|
|
| 18 |
|
| 19 |
# --- Device Configuration ---
|
| 20 |
def get_device():
|
|
|
|
| 27 |
return "mps"
|
| 28 |
else:
|
| 29 |
return "cpu"
|
|
|
|
|
|
|
| 30 |
|
| 31 |
device = get_device()
|
| 32 |
print(f"Using device: {device}")
|
|
|
|
| 43 |
def setup_gcp_credentials():
|
| 44 |
"""Sets up Google Cloud credentials from an environment variable."""
|
| 45 |
gcp_service_account_json_str = os.getenv("GCP_SERVICE_ACCOUNT_JSON")
|
|
|
|
| 46 |
if gcp_service_account_json_str:
|
| 47 |
try:
|
| 48 |
# Create a temporary file to store the credentials
|
|
|
|
| 62 |
# print("Warning: GCP_SERVICE_ACCOUNT_JSON is not set; Google Cloud clients may fail.")
|
| 63 |
# return gcp_service_account_json_str
|
| 64 |
# --- Audio Processing ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
def audiosegment_to_numpy(audio, target_sample_rate=16000):
|
|
|
|
|
|
|
|
|
|
| 66 |
samples = np.array(audio.get_array_of_samples(), dtype=np.float32)
|
| 67 |
if audio.channels > 1:
|
| 68 |
samples = samples.reshape((-1, audio.channels)).mean(axis=1)
|
|
|
|
|
|
|
| 69 |
samples /= np.iinfo(audio.array_type).max
|
| 70 |
|
|
|
|
| 71 |
if audio.frame_rate != target_sample_rate:
|
| 72 |
samples = librosa.resample(samples, orig_sr=audio.frame_rate, target_sr=target_sample_rate)
|
| 73 |
+
return samples
|
| 74 |
|
| 75 |
+
def preprocess_audio(audio, target_channels=1, target_frame_rate=16000):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
+
Preprocess the audio using pydub AudioSegment by setting the number of channels and frame rate.
|
| 78 |
|
| 79 |
Args:
|
| 80 |
+
audio (tuple): A tuple (sample_rate, audio_array) where audio_array is a NumPy array.
|
| 81 |
+
target_channels (int): Desired number of channels (default is 1 for mono).
|
| 82 |
+
target_frame_rate (int): Desired frame rate (default is 16000 Hz).
|
| 83 |
+
|
| 84 |
Returns:
|
| 85 |
+
np.ndarray: The processed audio as a NumPy array.
|
| 86 |
"""
|
|
|
|
|
|
|
| 87 |
sample_rate, audio_array = audio
|
| 88 |
+
target_frame_rate = sample_rate
|
| 89 |
+
audio_array_int16 = audio_array.astype(np.int16)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
audio_bytes = audio_array_int16.tobytes()
|
| 91 |
audio_io = io.BytesIO(audio_bytes)
|
| 92 |
+
segment = AudioSegment.from_raw(audio_io, sample_width=2, frame_rate=sample_rate, channels=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
segment = segment.set_channels(target_channels)
|
| 94 |
segment = segment.set_frame_rate(target_frame_rate)
|
| 95 |
+
return audiosegment_to_numpy(segment)
|
| 96 |
+
|
| 97 |
+
# --- Conversation Utilities ---
|
| 98 |
+
def is_valid_turn(turn):
|
| 99 |
+
"""Return True if turn is a valid dict with non-empty 'role' and 'content' strings."""
|
| 100 |
+
return (
|
| 101 |
+
isinstance(turn, dict)
|
| 102 |
+
and "role" in turn
|
| 103 |
+
and "content" in turn
|
| 104 |
+
and isinstance(turn["role"], str)
|
| 105 |
+
and isinstance(turn["content"], str)
|
| 106 |
+
and turn["role"].strip() != ""
|
| 107 |
+
and turn["content"].strip() != ""
|
| 108 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|