Files changed (9) hide show
  1. app.py +79 -271
  2. backend/asr.py +34 -122
  3. backend/functions.py +1 -3
  4. backend/main.py +153 -21
  5. backend/models.py +16 -131
  6. backend/systemprompt.py +22 -51
  7. backend/tools.py +0 -446
  8. backend/tts.py +1 -3
  9. 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, transcribe_typhoon
12
- from backend.utils import preprocess_audio, is_valid_turn, preprocess_audio_simplified
13
  from backend.main import stream_chat_response
14
- import json
15
  from pydub import AudioSegment
16
- import ast
17
- from backend.utils import get_device
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: #f9cb9c; /* Lighter pink for telephone interface */
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: #f9cb9c; /* Lighter pink for conversation history */
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
- sr , processed_audio = preprocess_audio_simplified((sample_rate, audio_array), target_sr=16000)
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 = transcribe_typhoon(file_name)
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(format_history_for_chatbot(conversation_history))
275
 
276
- # print("Conversation history:", conversation_history)
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 = 1
289
- max_n_threshold = 1
290
  lang = "th"
291
  chunk_count = 0
292
  first_chunk_sent = False
293
  start_llm_stream = time.time()
294
 
295
  try:
296
- for chunk in stream_chat_response(history_for_stream, transcription):
297
- # print(f"LLM chunk: {text_chunk}")
298
- if isinstance(chunk, str):
299
- text_chunk = chunk
300
-
301
- i = 0
302
- while i < len(text_chunk):
303
- char = text_chunk[i]
304
- text_buffer += char
305
- full_response += char
306
-
307
- assistant_turn["content"] = full_response.strip()
308
-
309
- is_delimiter = False
310
- # if char in {' ', '\n'}:
311
- if char == "|":
312
-
313
- #check the next character is number, not count as delimiter
314
- # if i + 1 < len(text_chunk) and (text_chunk[i + 1].isdigit() or text_chunk[i - 1].isdigit()):
315
- # is_delimiter = False
316
- # else:
317
- # print(f"text_buffer before removing delimiter: '{text_buffer}'")
318
- # text_buffer = text_buffer[:-1] # Remove the delimiter from the buffer
319
- # print(f"text_buffer after removing delimiter: '{text_buffer}'")
320
- is_delimiter = True
321
-
322
- delimiter_count += 1
323
- if i + 1 < len(text_chunk) and text_chunk[i + 1] == 'ๆ':
324
- text_buffer += text_chunk[i + 1]
325
- full_response += text_chunk[i + 1]
326
- i += 1
327
-
328
- send_now = False
329
- if not first_chunk_sent:
330
- if is_delimiter and text_buffer.strip():
331
- send_now = True
332
- else:
333
- if delimiter_count >= n_threshold and text_buffer.strip():
334
- send_now = True
335
- if n_threshold < max_n_threshold:
336
- n_threshold += 1
337
-
338
- if send_now:
339
- buffer_to_send = text_buffer.strip()
340
- try:
341
- if buffer_to_send and buffer_to_send.endswith('วันที่'):
342
- buffer_to_send = buffer_to_send[:-len('วันที่')]
343
- if buffer_to_send and first_chunk_sent and buffer_to_send.endswith('ค่ะ'):
344
- buffer_to_send = buffer_to_send[:-len('ค่ะ')]
345
- except Exception:
346
- buffer_to_send = buffer_to_send.replace('ค่ะ', '')
347
-
348
- if buffer_to_send:
349
- chunk_count += 1
350
- if chunk_count == 1:
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(format_history_for_chatbot(conversation_history))
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="orange", secondary_hue="orange")) as demo:
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.35,
454
- started_talking_threshold=0.35,
455
- speech_threshold=0.2
456
  ),
457
  model_options=SileroVadOptions(
458
- threshold=0.65,
459
- min_speech_duration_ms=200,
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 and dynamic stage switching."""
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
- model_name="scb10x/typhoon-asr-realtime",
54
- map_location=device,
55
- )
56
  print("Typhoon ASR pipeline initialized.")
57
  return asr_model
58
 
59
-
60
- def _initialize_gpt_client() -> Optional[Any]:
61
- return None
62
-
63
-
64
-
65
-
66
- _GPT_ASR_MODEL = os.getenv("GPT_ASR_MODEL", "gpt-4o-mini-transcribe")
67
- _GPT_CLIENT = _initialize_gpt_client()
68
-
69
-
70
- def set_asr_stage(stage: str) -> None:
71
- """Update the active ASR stage."""
72
- normalized_stage = stage.lower()
73
- if normalized_stage not in {"typhoon", "gpt"}:
74
- raise ValueError(f"Unsupported ASR stage '{stage}'")
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
- def transcribe_audio(path: str) -> str:
172
- """Transcribe user audio with the best available backend based on the current stage."""
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
- transcription = _transcribe_with_gpt(path)
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(path).replace("ทางลัด", "ทางรัฐ")
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
- if get_device() == "mps":
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
- from utils import get_device
20
- if get_device() == "mps":
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,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
- # 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
- 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
- # if len(full_conversation) > 7:
111
- # if full_conversation[-7].get("role") == "tool":
112
- # limited_conversation = full_conversation
113
- # else:
114
- # limited_conversation = full_conversation[-7:]
115
- # else:
116
- # limited_conversation = full_conversation
117
- limited_conversation = full_conversation
118
- response_generator = llm_analyzer.generate_normal_response(limited_conversation)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- from utils import get_device
23
- if get_device() == "mps":
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
- import requests
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", tools= tools, stream_options={"include_usage": True}
125
  )
126
  else:
127
  response_stream = await client.chat.completions.create(
128
- model=model, messages=messages, stream=True, tools= tools
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 chunk in response_stream:
 
 
138
  if chunk:
139
- if chunk.usage.prompt_tokens:
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, tools = TOOL_DEFINITIONS
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"""## (Core Role)
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
- ## Topic to answer:
84
  1. 1577 Home shopping product in Thailand
85
 
86
  ### Instructions:
87
- User Journal:
88
- Stage 1: Consultation
89
- - In this stage, the user may ask about promotions, prices, or product descriptions they heard on television.
90
- - Use the tool "get_promotion_description" to retrieve details of the promotion or product.
91
- Stage 2: Purchase & Information Collection
92
- When the user confirms they want to buy a promotion or product:
93
- 1. Ask how many items they would like to purchase.
94
- 2. After receiving the quantity, call summary_order, which will return:
95
- - promotion_id
96
- - quantity
97
- - total_price
98
- 3. Confirm the order details with the user.
99
- - Do not mention promotion_id directly.
100
- - Instead, use the corresponding promotion name from the description.
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
- - Very concise and short. if user ask the promotion, You can select 3 promotion and response it shortly, cut the unecessary parts.
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
- - ตอบเป็นประโยคสั้นๆ **ALWAYS** end the sentence with "|" token to indicate the end of the sentence for TTS system. each sentence should not be longer than 20 words.
133
- - Do not give the promotion_id to the user, use as a argument of summary_order only.
134
- - if you unable to answer or recognize what user ask more than 3 times, call "call_admin" to escalate the issue to human agent.
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": "สิบสองเอ็กซ์",'[2ฟร2]': 'สองฟรีสอง',
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
- # Final safety normalization
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
- Ensures the audio is mono, target sample rate, and normalized to [-1, 1].
104
 
105
  Args:
106
- audio: tuple (sample_rate, audio_array)
 
 
 
107
  Returns:
108
- tuple: (target_frame_rate, normalized_audio)
109
  """
110
- target_frame_rate = target_sr
111
-
112
  sample_rate, audio_array = audio
113
- #save audio array for debug
114
- with open("debug_audio_array.npy", "wb") as f:
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
- # Convert back to normalized numpy
146
- samples = audiosegment_to_numpy(segment, target_sample_rate=target_frame_rate)
147
-
148
- return (target_frame_rate, samples)
149
-
150
- def preprocess_audio_simplified(audio, target_sr=16000):
151
- """
152
- Ensures the audio is mono, at the target sample rate, and normalized to [-1, 1].
153
-
154
- Args:
155
- audio: tuple (original_sr, audio_array)
156
- audio_array is a numpy array.
157
- Returns:
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
+ )