Paul commited on
Commit
dad418e
·
1 Parent(s): ab5ffe4
Files changed (4) hide show
  1. app.py +89 -10
  2. gemini_service.py +239 -0
  3. perplexity_service.py +167 -0
  4. requirements.txt +2 -0
app.py CHANGED
@@ -4,6 +4,8 @@ from typing import Dict, Any, Tuple
4
 
5
  from reply_service import get_reply_service
6
  from trigger_move_identifier import get_trigger_move_identifier
 
 
7
 
8
 
9
  TRIGGER_MODEL_DIR = "./models/trigger_detector"
@@ -75,8 +77,8 @@ def parse_conversation(text: str) -> Tuple[str, str]:
75
  return male, female
76
 
77
 
78
- def run_full_pipeline(conversation: str, wingman_prompt: str = "") -> Dict[str, Any]:
79
- """Run trigger detector and generate replies from three prompt styles (1 backend)."""
80
  try:
81
  male, female = parse_conversation(conversation)
82
  identifier = get_trigger_move_identifier(
@@ -127,6 +129,21 @@ def run_full_pipeline(conversation: str, wingman_prompt: str = "") -> Dict[str,
127
  wingman_reply = ""
128
  wingman_error = str(exc)
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  models_output["llama"] = {
131
  "label": "Model 1 – Prompt style: an toàn / nhẹ nhàng",
132
  "reply": llama_reply,
@@ -144,6 +161,33 @@ def run_full_pipeline(conversation: str, wingman_prompt: str = "") -> Dict[str,
144
  "reply": wingman_reply,
145
  "error": wingman_error,
146
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  return {
149
  "trigger": trigger,
@@ -201,8 +245,8 @@ with gr.Blocks(title=title) as demo:
201
  gr.Markdown("---")
202
 
203
  # Main Reply Suggestion Tab
204
- gr.Markdown("### 🎯 Generate AI Reply Suggestions (3 Styles, 1 Backend)")
205
- gr.Markdown("Nhập hội thoại và hệ thống sẽ chạy pipeline Trigger → Move → 3 style prompt (an toàn, flirt tinh tế, wingman).")
206
 
207
  with gr.Row():
208
  with gr.Column(scale=2):
@@ -234,6 +278,25 @@ with gr.Blocks(title=title) as demo:
234
  info="You can reference {conversation}, {trigger}, {move} inside your prompt.",
235
  )
236
  gr.Markdown("Leave as-is for default behavior. Edits apply to Model 3 when its LoRA is used.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  reply_btn = gr.Button("Generate Reply Suggestion", variant="primary", size="lg")
238
 
239
  reply_out = gr.JSON(
@@ -262,12 +325,26 @@ with gr.Blocks(title=title) as demo:
262
  placeholder="Reply từ mô hình Wingman LoRA (hoặc fallback prompt) sẽ xuất hiện tại đây.",
263
  )
264
 
265
- def generate_reply_with_extraction(conversation: str, wingman_prompt: str) -> Tuple[Dict[str, Any], str, str, str]:
266
- """Generate replies from three models."""
267
- result = run_full_pipeline(conversation, wingman_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  if "error" in result:
269
  error_msg = f"❌ {result['error']}"
270
- return result, error_msg, error_msg, error_msg
271
 
272
  models = result.get("models", {})
273
 
@@ -284,12 +361,14 @@ with gr.Blocks(title=title) as demo:
284
  extract_text("llama"),
285
  extract_text("pho"),
286
  extract_text("wingman"),
 
 
287
  )
288
 
289
  reply_btn.click(
290
  generate_reply_with_extraction,
291
- inputs=[reply_in, wingman_prompt_in],
292
- outputs=[reply_out, llama_box, pho_box, wingman_box],
293
  api_name="reply"
294
  )
295
 
 
4
 
5
  from reply_service import get_reply_service
6
  from trigger_move_identifier import get_trigger_move_identifier
7
+ from perplexity_service import get_perplexity_service
8
+ from gemini_service import get_gemini_service, get_available_gemini_models
9
 
10
 
11
  TRIGGER_MODEL_DIR = "./models/trigger_detector"
 
77
  return male, female
78
 
79
 
80
+ def run_full_pipeline(conversation: str, wingman_prompt: str = "", gemini_model_name: str = "gemini-2.5-flash") -> Dict[str, Any]:
81
+ """Run trigger detector and generate replies from 5 models (3 prompt styles + Perplexity + Gemini)."""
82
  try:
83
  male, female = parse_conversation(conversation)
84
  identifier = get_trigger_move_identifier(
 
129
  wingman_reply = ""
130
  wingman_error = str(exc)
131
 
132
+ # Model 4 – Perplexity API
133
+ try:
134
+ perplexity_service = get_perplexity_service()
135
+ # Format conversation for Perplexity: "Male: ... ||| Female: ..."
136
+ formatted_conversation = f"Male: {male} ||| Female: {female}"
137
+ perplexity_reply = perplexity_service.generate_reply(
138
+ conversation=formatted_conversation,
139
+ trigger=trigger,
140
+ move=move,
141
+ )
142
+ perplexity_error = ""
143
+ except Exception as exc:
144
+ perplexity_reply = ""
145
+ perplexity_error = str(exc)
146
+
147
  models_output["llama"] = {
148
  "label": "Model 1 – Prompt style: an toàn / nhẹ nhàng",
149
  "reply": llama_reply,
 
161
  "reply": wingman_reply,
162
  "error": wingman_error,
163
  }
164
+
165
+ models_output["perplexity"] = {
166
+ "label": "Model 4 – Perplexity API",
167
+ "reply": perplexity_reply,
168
+ "error": perplexity_error,
169
+ }
170
+
171
+ # Model 5 – Google Gemini API
172
+ try:
173
+ gemini_service = get_gemini_service(model_name=gemini_model_name)
174
+ # Format conversation for Gemini: "Male: ... ||| Female: ..."
175
+ formatted_conversation = f"Male: {male} ||| Female: {female}"
176
+ gemini_reply = gemini_service.generate_reply(
177
+ conversation=formatted_conversation,
178
+ trigger=trigger,
179
+ move=move,
180
+ )
181
+ gemini_error = ""
182
+ except Exception as exc:
183
+ gemini_reply = ""
184
+ gemini_error = str(exc)
185
+
186
+ models_output["gemini"] = {
187
+ "label": f"Model 5 – Gemini API ({gemini_model_name})",
188
+ "reply": gemini_reply,
189
+ "error": gemini_error,
190
+ }
191
 
192
  return {
193
  "trigger": trigger,
 
245
  gr.Markdown("---")
246
 
247
  # Main Reply Suggestion Tab
248
+ gr.Markdown("### 🎯 Generate AI Reply Suggestions (5 Models)")
249
+ gr.Markdown("Nhập hội thoại và hệ thống sẽ chạy pipeline Trigger → Move → 5 models (3 prompt styles + Perplexity API + Gemini API).")
250
 
251
  with gr.Row():
252
  with gr.Column(scale=2):
 
278
  info="You can reference {conversation}, {trigger}, {move} inside your prompt.",
279
  )
280
  gr.Markdown("Leave as-is for default behavior. Edits apply to Model 3 when its LoRA is used.")
281
+
282
+ # Model 5 – Gemini Model Selection
283
+ try:
284
+ gemini_models = get_available_gemini_models()
285
+ gemini_model_choices = [model["name"] for model in gemini_models]
286
+ gemini_model_display = [f"{model['displayName']} ({model['name']})" for model in gemini_models]
287
+ default_gemini_model = gemini_model_choices[0] if gemini_model_choices else "gemini-2.5-flash"
288
+ except Exception as e:
289
+ gemini_model_choices = ["gemini-2.5-flash"]
290
+ gemini_model_display = ["Gemini 2.5 Flash (default - API key may be missing)"]
291
+ default_gemini_model = "gemini-2.5-flash"
292
+
293
+ gemini_model_dropdown = gr.Dropdown(
294
+ choices=gemini_model_choices,
295
+ value=default_gemini_model,
296
+ label="Model 5 – Select Gemini Model",
297
+ info="Choose which Gemini model to use for reply generation",
298
+ )
299
+
300
  reply_btn = gr.Button("Generate Reply Suggestion", variant="primary", size="lg")
301
 
302
  reply_out = gr.JSON(
 
325
  placeholder="Reply từ mô hình Wingman LoRA (hoặc fallback prompt) sẽ xuất hiện tại đây.",
326
  )
327
 
328
+ perplexity_box = gr.Textbox(
329
+ lines=3,
330
+ label="Model 4 – Perplexity API",
331
+ interactive=False,
332
+ placeholder="Reply từ Perplexity API sẽ xuất hiện tại đây.",
333
+ )
334
+
335
+ gemini_box = gr.Textbox(
336
+ lines=3,
337
+ label="Model 5 – Gemini API",
338
+ interactive=False,
339
+ placeholder="Reply từ Gemini API sẽ xuất hiện tại đây.",
340
+ )
341
+
342
+ def generate_reply_with_extraction(conversation: str, wingman_prompt: str, gemini_model_name: str) -> Tuple[Dict[str, Any], str, str, str, str, str]:
343
+ """Generate replies from five models."""
344
+ result = run_full_pipeline(conversation, wingman_prompt, gemini_model_name)
345
  if "error" in result:
346
  error_msg = f"❌ {result['error']}"
347
+ return result, error_msg, error_msg, error_msg, error_msg, error_msg
348
 
349
  models = result.get("models", {})
350
 
 
361
  extract_text("llama"),
362
  extract_text("pho"),
363
  extract_text("wingman"),
364
+ extract_text("perplexity"),
365
+ extract_text("gemini"),
366
  )
367
 
368
  reply_btn.click(
369
  generate_reply_with_extraction,
370
+ inputs=[reply_in, wingman_prompt_in, gemini_model_dropdown],
371
+ outputs=[reply_out, llama_box, pho_box, wingman_box, perplexity_box, gemini_box],
372
  api_name="reply"
373
  )
374
 
gemini_service.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Service for generating replies using Google Gemini API.
3
+ """
4
+ import os
5
+ import requests
6
+ from typing import Optional, List, Dict, Any
7
+ import google.generativeai as genai
8
+
9
+ # Reuse the same system prompt from Perplexity service
10
+ SYSTEM_PROMPT = """
11
+ Bạn là một wingman AI tinh tế, chuyên giúp Nam soạn 1 tin nhắn trả lời duy nhất trong hội thoại hẹn hò tiếng Việt. Bạn luôn nhìn từ góc nhìn của Nam, xưng "anh" và gọi đối phương là "em".
12
+
13
+ Bạn được cung cấp:
14
+
15
+ - HỘI THOẠI: đoạn hội thoại gần nhất giữa Nam (Male) và Nữ (Female), phân tách các tin bằng ký hiệu "|||".
16
+
17
+ - TRIGGER: intent hiện tại (ví dụ: neutral, positive, negative, confused...).
18
+
19
+ - MOVE: chiến lược hiện tại (ví dụ: escalate, hold, de-escalate, tease, comfort...).
20
+
21
+ Nhiệm vụ của bạn:
22
+
23
+ - Dựa trên HỘI THOẠI + TRIGGER + MOVE, hãy chọn một hướng phản hồi tự nhiên, duyên dáng, đúng chiến lược (không quá đẩy hay quá lùi so với MOVE).
24
+
25
+ - Ưu tiên giữ mạch cảm xúc nhất quán với hội thoại, tránh tạo thông tin fact mới về thế giới bên ngoài hoặc về hai người.
26
+
27
+ QUY TẮC CỨNG:
28
+
29
+ - Chỉ trả về đúng 1 câu duy nhất.
30
+
31
+ - Tối đa 25 từ tiếng Việt.
32
+
33
+ - Lịch sự, ấm áp, thân thiện; không phán xét, không thô lỗ.
34
+
35
+ - Không giải thích meta (không nói về "prompt", "AI", "chiến lược", "MOVE", "TRIGGER"...).
36
+
37
+ - Không lặp lại nguyên văn câu của đối phương.
38
+
39
+ - Không thêm fact mới (chỉ dựa trên những gì có trong hội thoại, hoặc các câu nói chung chung, không cụ thể hóa thông tin chưa có).
40
+
41
+ Khi TRIGGER hoặc MOVE có vẻ mâu thuẫn với HỘI THOẠI:
42
+
43
+ - Hãy ưu tiên sự an toàn và mềm mại.
44
+
45
+ - Có thể hỏi lại nhẹ nhàng để làm rõ, nhưng vẫn giữ frame chủ động, tự tin của Nam.
46
+
47
+ PHONG CÁCH:
48
+
49
+ - Ấm áp, tự tin nhưng không tự cao.
50
+
51
+ - Có thể dùng từ đệm tự nhiên (nha, nhé, ạ, dạ) khi phù hợp với ngữ cảnh.
52
+
53
+ - Phản chiếu cảm xúc của đối phương.
54
+
55
+ - Giữ mạch trò chuyện mở để còn đất tăng tương tác về sau.
56
+
57
+ Nếu vì bất kỳ lý do gì bạn không thể tuân thủ tất cả quy tắc trên:
58
+
59
+ - Hãy ưu tiên vẫn trả về đúng 1 câu, ≤25 từ, không chứa meta, không chứa thông tin fact mới.
60
+ """.strip()
61
+
62
+
63
+ def fetch_gemini_models(api_key: Optional[str] = None) -> List[Dict[str, Any]]:
64
+ """
65
+ Fetch available Gemini models from Google API.
66
+
67
+ Args:
68
+ api_key: Google API key. If None, will try to get from GOOGLE_API_KEY env var.
69
+
70
+ Returns:
71
+ List of model dictionaries with name, displayName, and description
72
+ """
73
+ api_key = api_key or os.getenv("GOOGLE_API_KEY")
74
+ if not api_key:
75
+ raise ValueError(
76
+ "Google API key is required.\n\n"
77
+ "Set environment variable:\n"
78
+ " export GOOGLE_API_KEY=AIzaSy...\n\n"
79
+ "Or pass api_key parameter."
80
+ )
81
+
82
+ url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
83
+
84
+ try:
85
+ response = requests.get(url, timeout=10)
86
+ response.raise_for_status()
87
+ data = response.json()
88
+
89
+ models = data.get("models", [])
90
+
91
+ # Filter models that support generateContent
92
+ generative_models = [
93
+ {
94
+ "name": model.get("name", ""),
95
+ "displayName": model.get("displayName", ""),
96
+ "description": model.get("description", ""),
97
+ "version": model.get("version", ""),
98
+ }
99
+ for model in models
100
+ if "generateContent" in model.get("supportedGenerationMethods", [])
101
+ ]
102
+
103
+ return generative_models
104
+
105
+ except requests.exceptions.RequestException as e:
106
+ raise Exception(f"Failed to fetch Gemini models: {str(e)}")
107
+ except Exception as e:
108
+ raise Exception(f"Error parsing Gemini models: {str(e)}")
109
+
110
+
111
+ class GeminiReplyService:
112
+ """Service for generating replies using Google Gemini API."""
113
+
114
+ def __init__(self, api_key: Optional[str] = None, model_name: str = "gemini-2.5-flash"):
115
+ """
116
+ Initialize Gemini service.
117
+
118
+ Args:
119
+ api_key: Google API key. If None, will try to get from GOOGLE_API_KEY env var.
120
+ model_name: Model name to use (e.g., "gemini-2.5-flash", "models/gemini-2.5-pro")
121
+ Can be with or without "models/" prefix
122
+ """
123
+ self.api_key = api_key or os.getenv("GOOGLE_API_KEY")
124
+ if not self.api_key:
125
+ raise ValueError(
126
+ "Google API key is required.\n\n"
127
+ "Set environment variable:\n"
128
+ " export GOOGLE_API_KEY=AIzaSy...\n\n"
129
+ "Or pass api_key parameter when initializing GeminiReplyService."
130
+ )
131
+
132
+ # Configure the API
133
+ genai.configure(api_key=self.api_key)
134
+
135
+ # Normalize model name (remove "models/" prefix if present)
136
+ if model_name.startswith("models/"):
137
+ model_name = model_name.replace("models/", "")
138
+
139
+ self.model_name = model_name
140
+ self.model = genai.GenerativeModel(model_name)
141
+
142
+ def generate_reply(
143
+ self,
144
+ conversation: str,
145
+ trigger: str,
146
+ move: str,
147
+ temperature: float = 0.2,
148
+ max_output_tokens: int = 80,
149
+ ) -> str:
150
+ """
151
+ Generate reply using Google Gemini API.
152
+
153
+ Args:
154
+ conversation: Conversation text in format "Male: ... ||| Female: ..."
155
+ trigger: Trigger label (e.g., "rapport_bid", "flirt_charm")
156
+ move: Move label (e.g., "charm", "invite", "validate")
157
+ temperature: Sampling temperature
158
+ max_output_tokens: Maximum tokens to generate
159
+
160
+ Returns:
161
+ Generated reply text (1 sentence, ≤25 words)
162
+ """
163
+ user_content = f"""
164
+ HỘI THOẠI: "{conversation}"
165
+ TRIGGER: "{trigger}"
166
+ MOVE: "{move}"
167
+ """.strip()
168
+
169
+ # Combine system prompt and user content
170
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{user_content}"
171
+
172
+ try:
173
+ # Generate content
174
+ response = self.model.generate_content(
175
+ full_prompt,
176
+ generation_config=genai.types.GenerationConfig(
177
+ temperature=temperature,
178
+ max_output_tokens=max_output_tokens,
179
+ ),
180
+ )
181
+
182
+ raw = response.text.strip() if response.text else ""
183
+
184
+ # Hậu xử lý: lấy câu đầu, giới hạn 25 từ
185
+ import re
186
+ # Tách theo dấu câu, lấy câu đầu
187
+ sentences = re.split(r'[.!?]', raw)
188
+ one_sentence = sentences[0].strip() if sentences else raw.strip()
189
+
190
+ # Giới hạn 25 từ
191
+ words = one_sentence.split()
192
+ limited = " ".join(words[:25])
193
+
194
+ # Đảm bảo kết thúc bằng dấu câu nếu cần
195
+ if limited and not limited[-1] in ".!?":
196
+ limited = limited.rstrip(",;:") + "."
197
+
198
+ return limited
199
+
200
+ except Exception as e:
201
+ raise Exception(f"Gemini API error: {str(e)}")
202
+
203
+
204
+ # Global singleton instance
205
+ _gemini_service = None
206
+ _cached_models = None
207
+
208
+
209
+ def get_gemini_service(
210
+ api_key: Optional[str] = None,
211
+ model_name: str = "gemini-2.5-flash",
212
+ ) -> GeminiReplyService:
213
+ """Get or create the global Gemini service instance."""
214
+ global _gemini_service
215
+ # Normalize model name (remove "models/" prefix if present)
216
+ normalized_name = model_name.replace("models/", "") if model_name.startswith("models/") else model_name
217
+ if _gemini_service is None or _gemini_service.model_name != normalized_name:
218
+ _gemini_service = GeminiReplyService(api_key=api_key, model_name=normalized_name)
219
+ return _gemini_service
220
+
221
+
222
+ def get_available_gemini_models(api_key: Optional[str] = None, use_cache: bool = True) -> List[Dict[str, Any]]:
223
+ """
224
+ Get list of available Gemini models that support generateContent.
225
+
226
+ Args:
227
+ api_key: Google API key. If None, will try to get from GOOGLE_API_KEY env var.
228
+ use_cache: Whether to use cached models list (default: True)
229
+
230
+ Returns:
231
+ List of model dictionaries
232
+ """
233
+ global _cached_models
234
+ if use_cache and _cached_models is not None:
235
+ return _cached_models
236
+
237
+ _cached_models = fetch_gemini_models(api_key=api_key)
238
+ return _cached_models
239
+
perplexity_service.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Service for generating replies using Perplexity API.
3
+ """
4
+ import os
5
+ from typing import Optional
6
+ from openai import OpenAI
7
+
8
+
9
+ SYSTEM_PROMPT = """
10
+ Bạn là một wingman AI tinh tế, chuyên giúp Nam soạn 1 tin nhắn trả lời duy nhất trong hội thoại hẹn hò tiếng Việt. Bạn luôn nhìn từ góc nhìn của Nam, xưng "anh" và gọi đối phương là "em".
11
+
12
+ Bạn được cung cấp:
13
+
14
+ - HỘI THOẠI: đoạn hội thoại gần nhất giữa Nam (Male) và Nữ (Female), phân tách các tin bằng ký hiệu "|||".
15
+
16
+ - TRIGGER: intent hiện tại (ví dụ: neutral, positive, negative, confused...).
17
+
18
+ - MOVE: chiến lược hiện tại (ví dụ: escalate, hold, de-escalate, tease, comfort...).
19
+
20
+ Nhiệm vụ của bạn:
21
+
22
+ - Dựa trên HỘI THOẠI + TRIGGER + MOVE, hãy chọn một hướng phản hồi tự nhiên, duyên dáng, đúng chiến lược (không quá đẩy hay quá lùi so với MOVE).
23
+
24
+ - Ưu tiên giữ mạch cảm xúc nhất quán với hội thoại, tránh tạo thông tin fact mới về thế giới bên ngoài hoặc về hai người.
25
+
26
+ QUY TẮC CỨNG:
27
+
28
+ - Chỉ trả về đúng 1 câu duy nhất.
29
+
30
+ - Tối đa 25 từ tiếng Việt.
31
+
32
+ - Lịch sự, ấm áp, thân thiện; không phán xét, không thô lỗ.
33
+
34
+ - Không giải thích meta (không nói về "prompt", "AI", "chiến lược", "MOVE", "TRIGGER"...).
35
+
36
+ - Không lặp lại nguyên văn câu của đối phương.
37
+
38
+ - Không thêm fact mới (chỉ dựa trên những gì có trong hội thoại, hoặc các câu nói chung chung, không cụ thể hóa thông tin chưa có).
39
+
40
+ Khi TRIGGER hoặc MOVE có vẻ mâu thuẫn với HỘI THOẠI:
41
+
42
+ - Hãy ưu tiên sự an toàn và mềm mại.
43
+
44
+ - Có thể hỏi lại nhẹ nhàng để làm rõ, nhưng vẫn giữ frame chủ động, tự tin của Nam.
45
+
46
+ PHONG CÁCH:
47
+
48
+ - Ấm áp, tự tin nhưng không tự cao.
49
+
50
+ - Có thể dùng từ đệm tự nhiên (nha, nhé, ạ, dạ) khi phù hợp với ngữ cảnh.
51
+
52
+ - Phản chiếu cảm xúc của đối phương.
53
+
54
+ - Giữ mạch trò chuyện mở để còn đất tăng tương tác về sau.
55
+
56
+ Nếu vì bất kỳ lý do gì bạn không thể tuân thủ tất cả quy tắc trên:
57
+
58
+ - Hãy ưu tiên vẫn trả về đúng 1 câu, ≤25 từ, không chứa meta, không chứa thông tin fact mới.
59
+ """.strip()
60
+
61
+
62
+ class PerplexityReplyService:
63
+ """Service for generating replies using Perplexity API."""
64
+
65
+ def __init__(self, api_key: Optional[str] = None, model: str = "mistral-7b-instruct"):
66
+ """
67
+ Initialize Perplexity service.
68
+
69
+ Args:
70
+ api_key: Perplexity API key. If None, will try to get from PERPLEXITY_API_KEY env var.
71
+ model: Model name to use (default: "mistral-7b-instruct")
72
+ """
73
+ self.api_key = api_key or os.getenv("PERPLEXITY_API_KEY")
74
+ if not self.api_key:
75
+ raise ValueError(
76
+ "Perplexity API key is required.\n\n"
77
+ "Set environment variable:\n"
78
+ " export PERPLEXITY_API_KEY=pplx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n"
79
+ "Or pass api_key parameter when initializing PerplexityReplyService."
80
+ )
81
+
82
+ self.client = OpenAI(
83
+ api_key=self.api_key,
84
+ base_url="https://api.perplexity.ai",
85
+ )
86
+ self.model = model
87
+
88
+ def generate_reply(
89
+ self,
90
+ conversation: str,
91
+ trigger: str,
92
+ move: str,
93
+ temperature: float = 0.2,
94
+ max_tokens: int = 80,
95
+ ) -> str:
96
+ """
97
+ Generate reply using Perplexity API.
98
+
99
+ Args:
100
+ conversation: Conversation text in format "Male: ... ||| Female: ..."
101
+ trigger: Trigger label (e.g., "rapport_bid", "flirt_charm")
102
+ move: Move label (e.g., "charm", "invite", "validate")
103
+ temperature: Sampling temperature
104
+ max_tokens: Maximum tokens to generate
105
+
106
+ Returns:
107
+ Generated reply text (1 sentence, ≤25 words)
108
+ """
109
+ user_content = f"""
110
+ HỘI THOẠI: "{conversation}"
111
+ TRIGGER: "{trigger}"
112
+ MOVE: "{move}"
113
+ """.strip()
114
+
115
+ try:
116
+ completion = self.client.chat.completions.create(
117
+ model=self.model,
118
+ temperature=temperature,
119
+ max_tokens=max_tokens,
120
+ messages=[
121
+ {
122
+ "role": "system",
123
+ "content": SYSTEM_PROMPT,
124
+ },
125
+ {
126
+ "role": "user",
127
+ "content": user_content,
128
+ },
129
+ ],
130
+ )
131
+
132
+ raw = completion.choices[0].message.content.strip() if completion.choices[0].message.content else ""
133
+
134
+ # Hậu xử lý: lấy câu đầu, giới hạn 25 từ
135
+ import re
136
+ # Tách theo dấu câu, lấy câu đầu
137
+ sentences = re.split(r'[.!?]', raw)
138
+ one_sentence = sentences[0].strip() if sentences else raw.strip()
139
+
140
+ # Giới hạn 25 từ
141
+ words = one_sentence.split()
142
+ limited = " ".join(words[:25])
143
+
144
+ # Đảm bảo kết thúc bằng dấu câu nếu cần
145
+ if limited and not limited[-1] in ".!?":
146
+ limited = limited.rstrip(",;:") + "."
147
+
148
+ return limited
149
+
150
+ except Exception as e:
151
+ raise Exception(f"Perplexity API error: {str(e)}")
152
+
153
+
154
+ # Global singleton instance
155
+ _perplexity_service = None
156
+
157
+
158
+ def get_perplexity_service(
159
+ api_key: Optional[str] = None,
160
+ model: str = "mistral-7b-instruct",
161
+ ) -> PerplexityReplyService:
162
+ """Get or create the global Perplexity service instance."""
163
+ global _perplexity_service
164
+ if _perplexity_service is None:
165
+ _perplexity_service = PerplexityReplyService(api_key=api_key, model=model)
166
+ return _perplexity_service
167
+
requirements.txt CHANGED
@@ -16,4 +16,6 @@ bitsandbytes
16
  datasets
17
  pandas
18
  einops
 
 
19
 
 
16
  datasets
17
  pandas
18
  einops
19
+ openai>=1.0.0
20
+ google-generativeai>=0.3.0
21