-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtransition_generator.py
More file actions
376 lines (327 loc) · 12.1 KB
/
Copy pathtransition_generator.py
File metadata and controls
376 lines (327 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# ======================================================================
# transition_generator.py
# ----------------------------------------------------------------------
# This module generates:
# 1) An optional *comfort_text* (only when the user was nervous and
# has not yet received comfort for the previous question).
# 2) A *connector_text* that smoothly transitions from the user's
# previous answer to the NEXT main question.
#
# Nervousness logic:
# - Case 1: was_nervous = True, comfort_given = False
# → Provide comfort_text AND a gentle guided connector.
#
# - Case 2: was_nervous = True, comfort_given = True
# → Provide ONLY guided connector (comfort_text must be empty).
#
# - Case 3: was_nervous = False
# → Provide ONLY neutral connector to original_main.
#
# Output:
# {
# "comfort_text": "...",
# "connector_text": "...",
# "target_mode": "guided" or "original"
# }
#
# The model MUST output valid JSON; markdown code fences are removed safely.
# ======================================================================
from __future__ import annotations
import os
import json
from typing import Dict, Any
from dotenv import load_dotenv
from openai import OpenAI
# Load environment variables once at import time
load_dotenv()
class TransitionGenerator:
"""
Generates comfort + connector utterances for interview transitions.
Uses a DeepSeek model (OpenAI-compatible API) to produce short
natural language outputs.
Public API:
result = generate_transition(
was_nervous: bool,
comfort_given: bool,
answer_text: str,
next_original_main: str,
next_guided_main: str
)
"""
def __init__(self, model_name: str | None = None):
api_key = os.getenv("DEEPSEEK_API_KEY")
if not api_key:
raise ValueError("DEEPSEEK_API_KEY not found. Please set it in .env")
# DeepSeek via OpenAI-compatible client
self.client = OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com",
)
env_model = os.getenv("DEEPSEEK_MODEL_NAME", "deepseek-chat")
self.model_name = model_name or env_model
# ==================================================================
# Utility: remove ```json ... ``` wrappers if present
# ==================================================================
@staticmethod
def _strip_code_fence(text: str) -> str:
"""
If the LLM wraps JSON in ```json ... ``` this function
removes those markdown fences.
"""
s = text.strip()
if s.startswith("```"):
s = s.strip().strip("`")
# Remove a leading 'json'
if s.lower().startswith("json"):
s = s[4:].strip()
return s
# ==================================================================
# Prompt builder: Case 1 — nervous & no comfort given
# ==================================================================
def _build_prompt_comfort_and_link(
self,
answer_text: str,
next_guided_main: str,
) -> str:
"""
Case 1:
- User was nervous
- No comfort was given yet
The model must output:
* a short comfort_text (reassurance)
* a connector_text leading softly to the NEXT guided question
"""
return f"""
You are a social robot interviewer interacting with a student who appears nervous.
Below is the student's previous answer:
USER_ANSWER:
{answer_text}
TASK
----
1. Provide a short *comfort_text*:
- Supportive, kind, validating their effort.
- No clinical or diagnostic language.
- ONE short sentence.
2. Provide a short *connector_text* that gently leads to the next guided question:
- Do NOT repeat the full question.
- Use a soft transition phrase.
- ONE sentence.
NEXT_GUIDED_MAIN:
{next_guided_main}
OUTPUT FORMAT
-------------
Return ONLY a JSON object:
{{
"comfort_text": "your warm reassurance sentence",
"connector_text": "your gentle transition sentence"
}}
Your answer MUST be valid JSON ONLY (no markdown, no commentary).
""".strip()
# ==================================================================
# Prompt builder: Case 2 — nervous & already comforted
# ==================================================================
def _build_prompt_link_guided(
self,
answer_text: str,
next_guided_main: str,
) -> str:
"""
Case 2:
- User was nervous
- Comfort was already given earlier
The model must output:
* NO comfort_text (empty string)
* ONLY a gentle connector leading to next guided question
"""
return f"""
You are a social robot interviewer. The student already received comfort earlier.
Below is the student's answer:
USER_ANSWER:
{answer_text}
TASK
----
Generate only a *connector_text*:
- Warm but not overly repetitive.
- ONE sentence.
- Leads gently toward the next guided question.
- comfort_text MUST be empty ("").
NEXT_GUIDED_MAIN:
{next_guided_main}
OUTPUT FORMAT
-------------
Return ONLY a JSON object:
{{
"comfort_text": "",
"connector_text": "your gentle transition sentence"
}}
Your answer MUST be valid JSON ONLY.
""".strip()
# ==================================================================
# Prompt builder: Case 3 — not nervous
# ==================================================================
def _build_prompt_link_neutral(
self,
answer_text: str,
next_original_main: str,
) -> str:
"""
Case 3:
- User was NOT nervous
The model must output:
* NO comfort_text
* A neutral, professional connector_text
"""
return f"""
You are a social robot interviewer in a professional but friendly setting.
Below is the user's previous answer:
USER_ANSWER:
{answer_text}
TASK
----
Generate ONLY a neutral *connector_text* (ONE sentence):
- Brief acknowledgment of the answer.
- Smooth transition to the next original_main question.
- comfort_text MUST be empty ("").
NEXT_ORIGINAL_MAIN:
{next_original_main}
OUTPUT FORMAT
-------------
Return ONLY a JSON object:
{{
"comfort_text": "",
"connector_text": "your neutral transition sentence"
}}
Your answer MUST be valid JSON ONLY.
""".strip()
# ==================================================================
# MAIN PUBLIC FUNCTION
# ==================================================================
def generate_transition(
self,
was_nervous: bool,
comfort_given: bool,
answer_text: str,
next_original_main: str,
next_guided_main: str,
) -> Dict[str, Any]:
"""
Generate transition utterances according to nervousness state.
Args:
was_nervous : bool — previous answer classified as nervous?
comfort_given : bool — has comfort been given already?
answer_text : str — previous user answer
next_original_main: str — next question's original version
next_guided_main : str — next question's guided version
Returns:
Dict with:
comfort_text : str
connector_text : str
target_mode : "guided" or "original"
"""
# ---------- Decide which prompt to use ----------
if was_nervous:
target_mode = "guided"
if not comfort_given:
# Case 1: nervous + no comfort → comfort + guided connector
prompt = self._build_prompt_comfort_and_link(
answer_text=answer_text,
next_guided_main=next_guided_main,
)
else:
# Case 2: nervous + comfort already given → guided connector only
prompt = self._build_prompt_link_guided(
answer_text=answer_text,
next_guided_main=next_guided_main,
)
else:
# Case 3: not nervous → neutral connector
target_mode = "original"
prompt = self._build_prompt_link_neutral(
answer_text=answer_text,
next_original_main=next_original_main,
)
# ---------- Query DeepSeek ----------
resp = self.client.chat.completions.create(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0.5,
)
raw = resp.choices[0].message.content or ""
raw = self._strip_code_fence(raw)
# ---------- Parse JSON safely ----------
try:
data = json.loads(raw)
except json.JSONDecodeError:
# Fallback behavior if LLM output is malformed
if was_nervous and not comfort_given:
return {
"comfort_text": "Thank you for sharing that. You are doing really well.",
"connector_text": "When you feel ready, I would like to continue with the next question.",
"target_mode": "guided",
}
else:
return {
"comfort_text": "",
"connector_text": "Thank you. Let's continue with the next question.",
"target_mode": target_mode,
}
# ---------- Extract fields ----------
comfort_text = str(data.get("comfort_text", "")).strip()
connector_text = str(data.get("connector_text", "")).strip()
# ---------- Enforce rule: no comfort_text except Case 1 ----------
if not was_nervous or (was_nervous and comfort_given):
comfort_text = ""
# ---------- Ensure connector_text always exists ----------
if not connector_text:
if target_mode == "guided":
connector_text = (
"When you feel ready, I would like to move on to the next question."
)
else:
connector_text = (
"Thank you for your answer. Let's continue with the next question."
)
# ---------- Final output ----------
return {
"comfort_text": comfort_text,
"connector_text": connector_text,
"target_mode": target_mode,
}
# ============================================================
# Local test (for debugging) – optional
# ============================================================
if __name__ == "__main__":
gen = TransitionGenerator()
answer = "I am a bit nervous but I tried my best in that project."
next_orig = "Can you tell me about a project you are proud of?"
next_guided = (
"You mentioned several projects. Could you choose one and briefly explain what the goal was, "
"what you did, and what you learned?"
)
print("\n--- Case 1: nervous + no comfort_given ---")
out1 = gen.generate_transition(
was_nervous=True,
comfort_given=False,
answer_text=answer,
next_original_main=next_orig,
next_guided_main=next_guided,
)
print(out1)
print("\n--- Case 2: nervous + comfort_given ---")
out2 = gen.generate_transition(
was_nervous=True,
comfort_given=True,
answer_text=answer,
next_original_main=next_orig,
next_guided_main=next_guided,
)
print(out2)
print("\n--- Case 3: not nervous ---")
out3 = gen.generate_transition(
was_nervous=False,
comfort_given=False,
answer_text=answer,
next_original_main=next_orig,
next_guided_main=next_guided,
)
print(out3)