mikaelmp commited on
Commit
0429e35
·
verified ·
1 Parent(s): 1511142

Added logging and caps responses at 800 characters in answer validation to ensure no "thinking" answers

Browse files
researchsimulation/InteractiveInterviewChatbot.py CHANGED
@@ -1,6 +1,7 @@
1
  import gradio as gr
2
  import logging
3
  import re
 
4
 
5
  from RespondentAgent import *
6
  from langchain_groq import ChatGroq
@@ -16,9 +17,11 @@ def parse_question_with_llm(question, respondent_names, processor_llm):
16
  Uses OpenAI's LLM to extract the specific agents being addressed and their respective questions.
17
  Supports compound requests.
18
  """
19
- logging.info("🔍 ENTERING: parse_question_with_llm()")
20
- logging.info(f"Received user input: {question}")
21
- logging.info(f"Available respondent names: {respondent_names}")
 
 
22
 
23
  prompt = f"""
24
  You are an expert in market research interview analysis.
@@ -61,24 +64,29 @@ def parse_question_with_llm(question, respondent_names, processor_llm):
61
 
62
  Only return the formatted output without explanations.
63
  """
64
-
65
- logging.info("Prompt constructed. Invoking LLM now...")
66
 
 
 
 
 
67
  try:
68
  response = processor_llm.invoke(prompt)
 
 
 
69
  if not hasattr(response, "content") or not response.content:
70
- logging.error("LLM response is empty or malformed.")
71
  return {}
72
 
73
  chatgpt_output = response.content.strip()
74
- logging.info(f"Raw LLM Output:\n{chatgpt_output}")
75
 
76
  except Exception as e:
77
- logging.exception(f"Exception occurred during LLM invocation in parse_question_with_llm: {e}")
78
  return {}
79
 
80
  # Begin parsing the structured response
81
- logging.info("Parsing LLM output for respondent-question pairs...")
82
 
83
  parsed_questions = {}
84
  respondent_name = "General"
@@ -87,19 +95,20 @@ def parse_question_with_llm(question, respondent_names, processor_llm):
87
  for line in chatgpt_output.split("\n"):
88
  if "- Respondent:" in line:
89
  respondent_name = re.sub(r"^.*Respondent:\s*", "", line).strip().capitalize()
 
90
  elif "Question:" in line:
91
  question_text = re.sub(r"^.*Question:\s*", "", line).strip()
92
  if respondent_name and question_text:
93
  parsed_questions[respondent_name] = question_text
94
- logging.info(f"Parsed -> Respondent: {respondent_name}, Question: {question_text}")
95
  respondent_name = "General"
96
  question_text = None
97
 
98
  if not parsed_questions:
99
- logging.warning("No respondent-question pairs were successfully parsed.")
100
 
101
- logging.info(f"Final parsed questions: {parsed_questions}")
102
- logging.info("Exiting parse_question_with_llm()")
103
  return parsed_questions
104
 
105
  def validate_question_topics(parsed_questions, processor_llm):
@@ -108,13 +117,15 @@ def validate_question_topics(parsed_questions, processor_llm):
108
  Converts question to British English spelling if valid.
109
  Returns 'INVALID' for any out-of-scope question.
110
  """
111
- logging.info("ENTERING: validate_question_topics()")
112
- logging.info("Starting question validation against permitted scope...")
113
 
114
  validated_questions = {}
115
 
116
  for respondent, question in parsed_questions.items():
117
- logging.info(f"Validating question for {respondent}: {question}")
 
 
118
  prompt = f"""
119
  You are a senior research analyst. Your job is to **validate** whether a market research question is within the allowed topic scope and convert it to **British English** spelling, grammar, and phrasing.
120
  ### Question:
@@ -180,26 +191,36 @@ def validate_question_topics(parsed_questions, processor_llm):
180
  # - Strictly do not add to the question other than rewriting to **British English**.
181
  # ### Output:
182
  # <Validated question in British English, or "INVALID">
183
-
 
 
 
184
  try:
185
- logging.debug("Sending validation prompt to LLM...")
 
186
  response = processor_llm.invoke(prompt)
 
 
187
 
188
  if not hasattr(response, "content") or not response.content:
189
- logging.error(f"Empty or malformed response from LLM for respondent: '{respondent}'")
190
  validated_output = "INVALID"
191
  else:
192
  validated_output = response.content.strip()
193
- logging.info(f"Validation output for '{respondent}': {validated_output}")
194
 
195
  except Exception as e:
196
- logging.exception(f"Exception during validation for respondent '{respondent}': {e}")
197
  validated_output = "INVALID"
198
 
199
  validated_questions[respondent] = validated_output
 
 
 
 
 
 
200
 
201
- logging.info("Completed validation for all questions.")
202
- logging.debug(f"Final validated questions dictionary:\n{validated_questions}")
203
  return validated_questions
204
 
205
 
@@ -208,89 +229,156 @@ def generate_generic_answer(agent_name, agent_question, respondent_agent):
208
  """
209
  Generates a raw, content-only answer with no stylistic or emotional tailoring.
210
  """
211
- task_description = f"""
212
- You are {agent_name}. Respond to the market research interview question below using only factual, content-relevant information based on your personal experience.
213
-
214
- ---
215
- ### Question:
216
- "{agent_question}"
217
-
218
- ---
219
- ### Instructions:
220
- - Answer **only what is asked** in the question.
221
- - Do **not** include any specific communication style, tone, or emotional expression.
222
- - Your answer must be **clear, concise, and factually accurate**.
223
- - Use **first-person ("I", "my", etc.)** and speak as yourself.
224
- - Do **not** include introductions, conclusions, opinions, or embellishments.
225
- - Use strict **British English** spelling and grammar.
226
- - **Do not** reference your own name or include placeholders like [Your Name].
227
-
228
- ---
229
- Your goal is to provide a direct, stylistically neutral answer to: "{agent_question}"
230
- """
231
 
232
- task = Task(description=task_description, expected_output="A neutral, personal response to the question.", agent=respondent_agent)
233
- Crew(agents=[respondent_agent], tasks=[task], process=Process.sequential).kickoff()
234
- output = task.output
235
- return output.raw if hasattr(output, "raw") else str(output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
 
238
  def tailor_answer_to_profile(agent_name, generic_answer, agent_question, user_profile, respondent_agent):
239
  """
240
  Enhances the generic answer to match the respondent's communication profile and personality traits.
241
  """
242
- style = user_profile.get_field("Communication", "Style")
243
- tone = user_profile.get_field("Communication", "Tone")
244
- length = user_profile.get_field("Communication", "Length")
245
- topics = user_profile.get_field("Communication", "Topics")
246
-
247
- task_description = f"""
248
- You are {agent_name}. Rewrite the following answer to match your personal communication style and tone preferences.
249
-
250
- ---
251
- ### Original Generic Answer:
252
- {generic_answer}
253
-
254
- ---
255
- ### Question:
256
- "{agent_question}"
257
-
258
- ---
259
- ### *Communication Profile Reference:*
260
- - **Style:** {style}
261
- - **Tone:** {tone}
262
- - **Length:** {length}
263
- - **Topics:** {topics}
264
-
265
- ---
266
- ### 🔒 Hard Rules – You Must Follow These Without Exception:
267
- - Keep the **meaning** and **personal point of view** of the original answer.
268
- - Do **not** introduce new information or elaborate beyond what’s stated.
269
- - Use **first person** ("I", "my", etc.) — never speak in third person.
270
- - Always use **British English** spelling, punctuation, and grammar.
271
- - Match the specified **style**, **tone**, and **length**.
272
- - Keep the response **natural, personal, and culturally authentic**.
273
- - Do **not** include emojis, hashtags, placeholders, or third-person descriptions.
274
- - Maintain **narrative consistency** across responses to reflect a coherent personality.
275
- - Tailor phrasing, sentence structure, and vocabulary to fit your **persona** and **communication traits**.
276
-
277
- ---
278
- ### Personality Trait Alignment:
279
- Ensure your answer reflects these aspects of your personality profile:
280
- - Big Five Traits (e.g., Openness, Extraversion)
281
- - Values and Priorities (e.g., Risk Tolerance, Achievement Orientation)
282
- - Communication Preferences (e.g., Directness, Emotional Expressiveness)
283
-
284
- ---
285
- Final Output:
286
- A single paragraph answer that matches the respondent’s tone and style, while strictly preserving the original meaning and personal voice from this answer:
287
- **"{generic_answer}"**
288
- """
289
 
290
- task = Task(description=task_description, expected_output="A styled, culturally authentic, first-person response.", agent=respondent_agent)
291
- Crew(agents=[respondent_agent], tasks=[task], process=Process.sequential).kickoff()
292
- output = task.output
293
- return output.raw if hasattr(output, "raw") else str(output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
 
296
  def validate_tailored_answer(agent_name, agent_question, respondent_agent, tailored_answer_generator, user_profile, processor_llm, max_attempts=3):
@@ -298,20 +386,30 @@ def validate_tailored_answer(agent_name, agent_question, respondent_agent, tailo
298
  Re-generates and validates the respondent's tailored answer with retry logic.
299
  Returns the validated answer, or a fallback string if validation fails after max_attempts.
300
  """
 
 
 
301
  attempt = 0
302
  validated = False
303
  validated_answer = None
 
304
 
305
  while attempt < max_attempts and not validated:
 
 
 
306
  try:
307
- logging.info(f"Attempt {attempt+1}: Generating and validating answer for '{agent_name}'")
308
-
309
  # Generate tailored answer from generic → styled
310
  tailored_answer = tailored_answer_generator()
311
-
312
- logging.debug(f"Generated tailored answer (attempt {attempt+1}): {tailored_answer}")
 
313
 
314
  # Validate response
 
 
315
  is_valid = validate_response(
316
  question=agent_question,
317
  answer=tailored_answer,
@@ -322,25 +420,42 @@ def validate_tailored_answer(agent_name, agent_question, respondent_agent, tailo
322
  ai_evaluator_agent=None,
323
  processor_llm=processor_llm
324
  )
325
-
326
- logging.info(f"Validation result for attempt {attempt+1}: {is_valid}")
 
327
 
328
  if is_valid:
329
- validated = True
330
- validated_answer = tailored_answer
331
- break
 
 
 
 
 
332
  else:
333
- logging.warning(f"Validation failed for attempt {attempt+1}")
334
  attempt += 1
335
 
336
  except Exception as e:
337
- logging.error(f"Error on validation attempt {attempt+1} for '{agent_name}': {e}", exc_info=True)
338
  attempt += 1
339
 
 
 
 
 
 
340
  if validated_answer:
341
- return f"**{agent_name}**: {validated_answer}"
 
342
  else:
343
- return f"**PreData Moderator**: Unable to pass validation after {max_attempts} attempts for {agent_name}."
 
 
 
 
 
344
 
345
  def ask_interview_question(respondent_agents_dict, last_active_agent, question, processor_llm):
346
  """
@@ -348,70 +463,115 @@ def ask_interview_question(respondent_agents_dict, last_active_agent, question,
348
  Uses OpenAI's LLM to extract the intended respondent(s) and their specific question(s).
349
  Generates generic answers, styles them, and validates the output.
350
  """
 
 
351
 
352
- logging.info(f"Received question: {question}")
353
-
354
- agent_names = list(respondent_agents_dict.keys())
355
- logging.info(f"Available respondents: {agent_names}")
356
- print(f"Available respondents: {agent_names}")
357
-
358
- # Step 1: Parse question
359
- parsed_questions = parse_question_with_llm(question, str(agent_names), processor_llm)
360
- if not parsed_questions:
361
- logging.warning("No questions were parsed from input.")
362
- return ["**PreData Moderator**: No valid respondents were detected for this question."]
363
-
364
- # Step 2: Validate topics and spelling
365
- validated_questions = validate_question_topics(parsed_questions, processor_llm)
366
- for resp_name, extracted_question in validated_questions.items():
367
- if extracted_question == "INVALID":
368
- logging.warning(f"Invalid question detected for {resp_name}: {extracted_question}")
369
- return ["**PreData Moderator**: The question is invalid. Please ask another question."]
370
-
371
- # Handle "General" or "All"
372
- if len(validated_questions) > 1:
373
- return ["**PreData Moderator**: Please ask each respondent one question at a time."]
374
-
375
- if "General" in validated_questions:
376
- if isinstance(last_active_agent, list) and all(name in agent_names for name in last_active_agent):
377
- validated_questions = {name: validated_questions["General"] for name in last_active_agent}
378
- else:
379
- validated_questions = {name: validated_questions["General"] for name in agent_names}
380
-
381
- elif "All" in validated_questions:
382
- validated_questions = {name: validated_questions["All"] for name in agent_names}
383
 
384
- # Update last_active_agent
385
- last_active_agent = list(validated_questions.keys())
386
-
387
- responses = []
388
- for agent_name, agent_question in validated_questions.items():
389
- if agent_name not in respondent_agents_dict:
390
- responses.append(f"**PreData Moderator**: {agent_name} is not a valid respondent.")
391
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- respondent_agent = respondent_agents_dict[agent_name].get_agent()
394
- user_profile = respondent_agents_dict[agent_name].get_user_profile()
 
395
 
396
- # Step 1: Generate Generic Answer
397
- generic_answer = generate_generic_answer(agent_name, agent_question, respondent_agent)
 
 
 
398
 
399
- # Step 2 & 3: Tailor + Validate with Retry
400
- def generator():
401
- return tailor_answer_to_profile(agent_name, generic_answer, agent_question, user_profile, respondent_agent)
402
 
403
- validated_response = validate_tailored_answer(
404
- agent_name=agent_name,
405
- agent_question=agent_question,
406
- respondent_agent=respondent_agent,
407
- tailored_answer_generator=generator,
408
- user_profile=user_profile,
409
- processor_llm=processor_llm
410
- )
411
 
412
- responses.append(validated_response)
 
 
413
 
414
- if len(set(validated_questions.values())) == 1:
415
- return ["\n\n".join(responses)]
416
- else:
417
- return responses
 
1
  import gradio as gr
2
  import logging
3
  import re
4
+ import time
5
 
6
  from RespondentAgent import *
7
  from langchain_groq import ChatGroq
 
17
  Uses OpenAI's LLM to extract the specific agents being addressed and their respective questions.
18
  Supports compound requests.
19
  """
20
+ logging.info("[parse_question_with_llm] Entry")
21
+ logging.debug(f"[parse_question_with_llm] Input question: {question}")
22
+ logging.debug(f"[parse_question_with_llm] Available respondent names: {respondent_names}")
23
+
24
+ start_time = time.time()
25
 
26
  prompt = f"""
27
  You are an expert in market research interview analysis.
 
64
 
65
  Only return the formatted output without explanations.
66
  """
 
 
67
 
68
+ logging.debug("[parse_question_with_llm] Prompt constructed successfully.")
69
+ logging.debug(f"[parse_question_with_llm] Prompt preview: {prompt[:500]}...")
70
+
71
+ logging.info("Prompt constructed. Invoking LLM now...")
72
  try:
73
  response = processor_llm.invoke(prompt)
74
+ duration = time.time() - start_time
75
+ logging.info(f"[parse_question_with_llm] LLM call completed in {duration:.2f} seconds.")
76
+
77
  if not hasattr(response, "content") or not response.content:
78
+ logging.error("[parse_question_with_llm] ERROR: LLM response is empty or malformed.")
79
  return {}
80
 
81
  chatgpt_output = response.content.strip()
82
+ logging.info(f"[parse_question_with_llm] Raw LLM output: {chatgpt_output}")
83
 
84
  except Exception as e:
85
+ logging.exception("[parse_question_with_llm] Exception during LLM invocation.")
86
  return {}
87
 
88
  # Begin parsing the structured response
89
+ logging.info("[parse_question_with_llm] Parsing LLM output for respondent-question pairs.")
90
 
91
  parsed_questions = {}
92
  respondent_name = "General"
 
95
  for line in chatgpt_output.split("\n"):
96
  if "- Respondent:" in line:
97
  respondent_name = re.sub(r"^.*Respondent:\s*", "", line).strip().capitalize()
98
+ logging.debug(f"[parse_question_with_llm] Detected respondent: {respondent_name}")
99
  elif "Question:" in line:
100
  question_text = re.sub(r"^.*Question:\s*", "", line).strip()
101
  if respondent_name and question_text:
102
  parsed_questions[respondent_name] = question_text
103
+ logging.info(f"[parse_question_with_llm] Parsed pair: Respondent={respondent_name}, Question={question_text}")
104
  respondent_name = "General"
105
  question_text = None
106
 
107
  if not parsed_questions:
108
+ logging.warning("[parse_question_with_llm] WARNING: No respondent-question pairs parsed.")
109
 
110
+ logging.info(f"[parse_question_with_llm] Final parsed questions: {parsed_questions}")
111
+ logging.info("[parse_question_with_llm] Exit")
112
  return parsed_questions
113
 
114
  def validate_question_topics(parsed_questions, processor_llm):
 
117
  Converts question to British English spelling if valid.
118
  Returns 'INVALID' for any out-of-scope question.
119
  """
120
+ logging.info("[validate_question_topics] Entry")
121
+ logging.debug(f"[validate_question_topics] Input parsed_questions: {parsed_questions}")
122
 
123
  validated_questions = {}
124
 
125
  for respondent, question in parsed_questions.items():
126
+ logging.info(f"[validate_question_topics] Processing respondent: {respondent}")
127
+ logging.debug(f"[validate_question_topics] Original question: {question}")
128
+
129
  prompt = f"""
130
  You are a senior research analyst. Your job is to **validate** whether a market research question is within the allowed topic scope and convert it to **British English** spelling, grammar, and phrasing.
131
  ### Question:
 
191
  # - Strictly do not add to the question other than rewriting to **British English**.
192
  # ### Output:
193
  # <Validated question in British English, or "INVALID">
194
+
195
+ logging.debug(f"[validate_question_topics] Prompt constructed for {respondent}.")
196
+ logging.debug(f"[validate_question_topics] Prompt preview: {prompt[:500]}...")
197
+
198
  try:
199
+ logging.info(f"[validate_question_topics] Invoking LLM for {respondent}")
200
+ llm_start = time.time()
201
  response = processor_llm.invoke(prompt)
202
+ llm_duration = time.time() - llm_start
203
+ logging.info(f"[validate_question_topics] LLM call completed for {respondent} in {llm_duration:.2f} seconds")
204
 
205
  if not hasattr(response, "content") or not response.content:
206
+ logging.error(f"[validate_question_topics] ERROR: Empty or malformed response from LLM for '{respondent}'")
207
  validated_output = "INVALID"
208
  else:
209
  validated_output = response.content.strip()
210
+ logging.debug(f"[validate_question_topics] Raw LLM output for {respondent}: {validated_output}")
211
 
212
  except Exception as e:
213
+ logging.exception(f"[validate_question_topics] Exception during LLM invocation for respondent '{respondent}'")
214
  validated_output = "INVALID"
215
 
216
  validated_questions[respondent] = validated_output
217
+ logging.info(f"[validate_question_topics] Validation result for {respondent}: {validated_output}")
218
+
219
+ total_duration = time.time() - start_time
220
+ logging.info(f"[validate_question_topics] Completed validation for all questions in {total_duration:.2f} seconds.")
221
+ logging.debug(f"[validate_question_topics] Final validated questions: {validated_questions}")
222
+ logging.info("[validate_question_topics] Exit")
223
 
 
 
224
  return validated_questions
225
 
226
 
 
229
  """
230
  Generates a raw, content-only answer with no stylistic or emotional tailoring.
231
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
+ logging.info("[generate_generic_answer] Entry")
234
+ logging.debug(f"[generate_generic_answer] Parameters: agent_name={agent_name}, agent_question={agent_question}")
235
+ start_time = time.time()
236
+
237
+ try:
238
+ # --- Build task description ---
239
+ logging.info("[generate_generic_answer] Constructing task description")
240
+ task_description = f"""
241
+ You are {agent_name}. Respond to the market research interview question below using only factual, content-relevant information based on your personal experience.
242
+
243
+ ---
244
+ ### Question:
245
+ "{agent_question}"
246
+
247
+ ---
248
+ ### Instructions:
249
+ - Answer **only what is asked** in the question.
250
+ - Do **not** include any specific communication style, tone, or emotional expression.
251
+ - Your answer must be **clear, concise, and factually accurate**.
252
+ - Use **first-person ("I", "my", etc.)** and speak as yourself.
253
+ - Do **not** include introductions, conclusions, opinions, or embellishments.
254
+ - Use strict **British English** spelling and grammar.
255
+ - **Do not** reference your own name or include placeholders like [Your Name].
256
+
257
+ ---
258
+ Your goal is to provide a direct, stylistically neutral answer to: "{agent_question}"
259
+ """
260
+
261
+ logging.debug(f"[generate_generic_answer] Task description preview: {task_description[:300]}...")
262
+
263
+ task = Task(description=task_description, expected_output="A neutral, personal response to the question.", agent=respondent_agent)
264
+ Crew(agents=[respondent_agent], tasks=[task], process=Process.sequential)
265
+
266
+ logging.info("[generate_generic_answer] Starting Crew kickoff")
267
+ kickoff_start = time.time()
268
+ crew.kickoff()
269
+ kickoff_duration = time.time() - kickoff_start
270
+ logging.info(f"[generate_generic_answer] Crew kickoff completed in {kickoff_duration:.2f} seconds")
271
+
272
+ # --- Retrieve output ---
273
+ output = task.output
274
+ if hasattr(output, "raw"):
275
+ result = output.raw
276
+ else:
277
+ result = str(output)
278
+
279
+ logging.debug(f"[generate_generic_answer] Raw output: {result}")
280
+
281
+ except Exception as e:
282
+ logging.exception("[generate_generic_answer] Exception occurred during Crew execution")
283
+ result = "Sorry, something went wrong while generating the answer."
284
+
285
+ total_duration = time.time() - start_time
286
+ logging.info(f"[generate_generic_answer] Completed in {total_duration:.2f} seconds")
287
+ logging.info("[generate_generic_answer] Exit")
288
+ return result
289
 
290
 
291
  def tailor_answer_to_profile(agent_name, generic_answer, agent_question, user_profile, respondent_agent):
292
  """
293
  Enhances the generic answer to match the respondent's communication profile and personality traits.
294
  """
295
+ logging.info("[tailor_answer_to_profile] Entry")
296
+ logging.debug(f"[tailor_answer_to_profile] Parameters: agent_name={agent_name}, agent_question={agent_question}")
297
+ logging.debug(f"[tailor_answer_to_profile] generic_answer: {generic_answer}")
298
+ logging.debug(f"[tailor_answer_to_profile] user_profile: {user_profile}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
+ start_time = time.time()
301
+
302
+ try:
303
+ # --- Build task description ---
304
+ logging.info("[tailor_answer_to_profile] Constructing task description")
305
+ style = user_profile.get_field("Communication", "Style")
306
+ tone = user_profile.get_field("Communication", "Tone")
307
+ length = user_profile.get_field("Communication", "Length")
308
+ topics = user_profile.get_field("Communication", "Topics")
309
+
310
+ task_description = f"""
311
+ You are {agent_name}. Rewrite the following answer to match your personal communication style and tone preferences.
312
+
313
+ ---
314
+ ### Original Generic Answer:
315
+ {generic_answer}
316
+
317
+ ---
318
+ ### Question:
319
+ "{agent_question}"
320
+
321
+ ---
322
+ ### *Communication Profile Reference:*
323
+ - **Style:** {style}
324
+ - **Tone:** {tone}
325
+ - **Length:** {length}
326
+ - **Topics:** {topics}
327
+
328
+ ---
329
+ ### Hard Rules – You Must Follow These Without Exception:
330
+ - Keep the **meaning** and **personal point of view** of the original generic answer.
331
+ - Do **not** introduce new information or elaborate beyond what’s stated.
332
+ - Use **first person** ("I", "my", etc.) — never speak in third person.
333
+ - Always use **British English** spelling, punctuation, and grammar.
334
+ - Match the specified **style**, **tone**, and **length**.
335
+ - Keep the response **natural, personal, and culturally authentic**.
336
+ - Do **not** include emojis, hashtags, placeholders, or third-person descriptions.
337
+ - Maintain **narrative consistency** across responses to reflect a coherent personality.
338
+ - Tailor phrasing, sentence structure, and vocabulary to fit your **persona** and **communication traits**.
339
+
340
+ ---
341
+ ### Personality Trait Alignment:
342
+ Ensure your answer reflects these aspects of your personality profile:
343
+ - Big Five Traits (e.g., Openness, Extraversion)
344
+ - Values and Priorities (e.g., Risk Tolerance, Achievement Orientation)
345
+ - Communication Preferences (e.g., Directness, Emotional Expressiveness)
346
+
347
+ ---
348
+ Final Output:
349
+ A single paragraph answer that matches the respondent’s tone and style, while strictly preserving the original meaning and personal voice from this answer:
350
+ **"{generic_answer}"**
351
+ """
352
+
353
+ logging.debug(f"[tailor_answer_to_profile] Task description preview: {task_description[:300]}...")
354
+ logging.info("[tailor_answer_to_profile] Initialising Task and Crew objects")
355
+
356
+ task = Task(description=task_description, expected_output="A styled, culturally authentic, first-person response.", agent=respondent_agent)
357
+ Crew(agents=[respondent_agent], tasks=[task], process=Process.sequential)
358
+
359
+ logging.info("[tailor_answer_to_profile] Starting Crew kickoff")
360
+ kickoff_start = time.time()
361
+ crew.kickoff()
362
+ kickoff_duration = time.time() - kickoff_start
363
+ logging.info(f"[tailor_answer_to_profile] Crew kickoff completed in {kickoff_duration:.2f} seconds")
364
+
365
+ # --- Retrieve output ---
366
+ output = task.output
367
+ if hasattr(output, "raw"):
368
+ result = output.raw
369
+ else:
370
+ result = str(output)
371
+
372
+ logging.debug(f"[tailor_answer_to_profile] Raw output: {result}")
373
+
374
+ except Exception as e:
375
+ logging.exception("[tailor_answer_to_profile] Exception occurred during Crew execution")
376
+ result = "Sorry, something went wrong while generating the styled answer."
377
+
378
+ total_duration = time.time() - start_time
379
+ logging.info(f"[tailor_answer_to_profile] Completed in {total_duration:.2f} seconds")
380
+ logging.info("[tailor_answer_to_profile] Exit")
381
+ return result
382
 
383
 
384
  def validate_tailored_answer(agent_name, agent_question, respondent_agent, tailored_answer_generator, user_profile, processor_llm, max_attempts=3):
 
386
  Re-generates and validates the respondent's tailored answer with retry logic.
387
  Returns the validated answer, or a fallback string if validation fails after max_attempts.
388
  """
389
+ logging.info("[validate_tailored_answer] Entry")
390
+ logging.debug(f"[validate_tailored_answer] Parameters: agent_name={agent_name}, agent_question={agent_question}, max_attempts={max_attempts}")
391
+
392
  attempt = 0
393
  validated = False
394
  validated_answer = None
395
+ overall_start = time.time()
396
 
397
  while attempt < max_attempts and not validated:
398
+ logging.info(f"[validate_tailored_answer] Starting attempt {attempt+1} of {max_attempts}")
399
+ attempt_start = time.time()
400
+
401
  try:
402
+ logging.info(f"[validate_tailored_answer] Attempt {attempt+1}: Generating and validating answer for '{agent_name}'")
403
+ gen_start = time.time()
404
  # Generate tailored answer from generic → styled
405
  tailored_answer = tailored_answer_generator()
406
+ gen_duration = time.time() - gen_start
407
+ logging.info(f"[validate_tailored_answer] Tailored answer generation completed in {gen_duration:.2f} seconds")
408
+ logging.debug(f"[validate_tailored_answer] Tailored answer (attempt {attempt+1}): {tailored_answer}")
409
 
410
  # Validate response
411
+ logging.info(f"[validate_tailored_answer] Validating answer (attempt {attempt+1})")
412
+ val_start = time.time()
413
  is_valid = validate_response(
414
  question=agent_question,
415
  answer=tailored_answer,
 
420
  ai_evaluator_agent=None,
421
  processor_llm=processor_llm
422
  )
423
+ val_duration = time.time() - val_start
424
+ logging.info(f"[validate_tailored_answer] Validation completed in {val_duration:.2f} seconds")
425
+ logging.info(f"[validate_tailored_answer] Validation result for attempt {attempt+1}: {is_valid}")
426
 
427
  if is_valid:
428
+ if len(tailored_answer) > 800:
429
+ logging.warning(f"Tailored answer exceeds 800 characters (length={len(tailored_answer)}); retrying...")
430
+ attempt += 1
431
+ else:
432
+ validated = True
433
+ validated_answer = tailored_answer
434
+ logging.info(f"Answer validated successfully on attempt {attempt+1}")
435
+ break
436
  else:
437
+ logging.warning(f"Validation failed on attempt {attempt+1}")
438
  attempt += 1
439
 
440
  except Exception as e:
441
+ logging.exception(f"[validate_tailored_answer] Exception on attempt {attempt+1}")
442
  attempt += 1
443
 
444
+ attempt_duration = time.time() - attempt_start
445
+ logging.info(f"[validate_tailored_answer] Attempt {attempt+1} duration: {attempt_duration:.2f} seconds")
446
+
447
+ overall_duration = time.time() - overall_start
448
+
449
  if validated_answer:
450
+ final_response = f"**{agent_name}**: {validated_answer}"
451
+ logging.info(f"[validate_tailored_answer] Successfully returning validated answer after {overall_duration:.2f} seconds")
452
  else:
453
+ final_response = f"**PreData Moderator**: Unable to pass validation after {max_attempts} attempts for {agent_name}."
454
+ logging.warning(f"[validate_tailored_answer] Returning failure message after {overall_duration:.2f} seconds")
455
+
456
+ logging.debug(f"[validate_tailored_answer] Final response: {final_response}")
457
+ logging.info("[validate_tailored_answer] Exit")
458
+ return final_response
459
 
460
  def ask_interview_question(respondent_agents_dict, last_active_agent, question, processor_llm):
461
  """
 
463
  Uses OpenAI's LLM to extract the intended respondent(s) and their specific question(s).
464
  Generates generic answers, styles them, and validates the output.
465
  """
466
+ logging.info("[ask_interview_question] Entry")
467
+ logging.debug(f"[ask_interview_question] Parameters: question={question}, last_active_agent={last_active_agent}")
468
 
469
+ overall_start = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
 
471
+ try:
472
+ agent_names = list(respondent_agents_dict.keys())
473
+ logging.info(f"[ask_interview_question] Available respondents: {agent_names}")
474
+
475
+ # --- Step 1: Parse question ---
476
+ logging.info("[ask_interview_question] Parsing question with LLM")
477
+ parse_start = time.time()
478
+ parsed_questions = parse_question_with_llm(question, str(agent_names), processor_llm)
479
+ parse_duration = time.time() - parse_start
480
+ logging.info(f"[ask_interview_question] Parsing completed in {parse_duration:.2f} seconds")
481
+ logging.debug(f"[ask_interview_question] Parsed questions: {parsed_questions}")
482
+
483
+ if not parsed_questions:
484
+ logging.warning("[ask_interview_question] No questions were parsed from input.")
485
+ return ["**PreData Moderator**: No valid respondents were detected for this question."]
486
+
487
+ # --- Step 2: Validate topics and spelling ---
488
+ logging.info("[ask_interview_question] Validating parsed questions")
489
+ validation_start = time.time()
490
+ validated_questions = validate_question_topics(parsed_questions, processor_llm)
491
+ validation_duration = time.time() - validation_start
492
+ logging.info(f"[ask_interview_question] Validation completed in {validation_duration:.2f} seconds")
493
+ logging.debug(f"[ask_interview_question] Validated questions: {validated_questions}")
494
+
495
+ for resp_name, extracted_question in validated_questions.items():
496
+ if extracted_question == "INVALID":
497
+ logging.warning(f"[ask_interview_question] Invalid question detected for {resp_name}: {extracted_question}")
498
+ return ["**PreData Moderator**: The question is invalid. Please ask another question."]
499
+
500
+ # --- Handle "General" or "All" ---
501
+ if len(validated_questions) > 1:
502
+ logging.warning("[ask_interview_question] Multiple respondents detected in single question")
503
+ return ["**PreData Moderator**: Please ask each respondent one question at a time."]
504
+
505
+ if "General" in validated_questions:
506
+ logging.info("[ask_interview_question] Handling 'General' question")
507
+ if isinstance(last_active_agent, list) and all(name in agent_names for name in last_active_agent):
508
+ validated_questions = {name: validated_questions["General"] for name in last_active_agent}
509
+ else:
510
+ validated_questions = {name: validated_questions["General"] for name in agent_names}
511
+ logging.debug(f"[ask_interview_question] Expanded to: {validated_questions}")
512
+
513
+ elif "All" in validated_questions:
514
+ logging.info("[ask_interview_question] Handling 'All' question")
515
+ validated_questions = {name: validated_questions["All"] for name in agent_names}
516
+ logging.debug(f"[ask_interview_question] Expanded to: {validated_questions}")
517
+
518
+ # --- Update last_active_agent ---
519
+ last_active_agent = list(validated_questions.keys())
520
+ logging.info(f"[ask_interview_question] Updated last_active_agent: {last_active_agent}")
521
+
522
+ # --- Step 3: Generate + Tailor answers ---
523
+ responses = []
524
+ for agent_name, agent_question in validated_questions.items():
525
+ logging.info(f"[ask_interview_question] Processing respondent: {agent_name}")
526
+ generation_start = time.time()
527
+
528
+ if agent_name not in respondent_agents_dict:
529
+ logging.warning(f"[ask_interview_question] Invalid respondent name detected: {agent_name}")
530
+ responses.append(f"**PreData Moderator**: {agent_name} is not a valid respondent.")
531
+ continue
532
+
533
+ respondent_agent = respondent_agents_dict[agent_name].get_agent()
534
+ user_profile = respondent_agents_dict[agent_name].get_user_profile()
535
+
536
+ # --- Generate Generic Answer ---
537
+ logging.info(f"[ask_interview_question] Generating generic answer for {agent_name}")
538
+ generic_answer = generate_generic_answer(agent_name, agent_question, respondent_agent)
539
+ logging.debug(f"[ask_interview_question] Generic answer: {generic_answer}")
540
+
541
+ # --- Tailor + Validate with Retry ---
542
+ def generator():
543
+ return tailor_answer_to_profile(agent_name, generic_answer, agent_question, user_profile, respondent_agent)
544
+
545
+ logging.info(f"[ask_interview_question] Tailoring and validating answer for {agent_name}")
546
+ validated_response = validate_tailored_answer(
547
+ agent_name=agent_name,
548
+ agent_question=agent_question,
549
+ respondent_agent=respondent_agent,
550
+ tailored_answer_generator=generator,
551
+ user_profile=user_profile,
552
+ processor_llm=processor_llm
553
+ )
554
+ logging.debug(f"[ask_interview_question] Validated response: {validated_response}")
555
 
556
+ responses.append(validated_response)
557
+ generation_duration = time.time() - generation_start
558
+ logging.info(f"[ask_interview_question] Completed generation + validation for {agent_name} in {generation_duration:.2f} seconds")
559
 
560
+ # --- Format final return ---
561
+ if len(set(validated_questions.values())) == 1:
562
+ result = ["\n\n".join(responses)]
563
+ else:
564
+ result = responses
565
 
566
+ logging.info("[ask_interview_question] Successfully generated all responses")
567
+ logging.debug(f"[ask_interview_question] Final responses: {result}")
 
568
 
569
+ except Exception as e:
570
+ logging.exception("[ask_interview_question] Exception occurred during processing")
571
+ result = ["**PreData Moderator**: An unexpected error occurred while processing the question."]
 
 
 
 
 
572
 
573
+ overall_duration = time.time() - overall_start
574
+ logging.info(f"[ask_interview_question] Completed in {overall_duration:.2f} seconds")
575
+ logging.info("[ask_interview_question] Exit")
576
 
577
+ return result