Pygmales commited on
Commit
5ff514f
·
1 Parent(s): fd5438b

updated project version

Browse files
app.py CHANGED
@@ -7,7 +7,7 @@ from dotenv import load_dotenv
7
 
8
  if __name__ == "__main__":
9
  load_dotenv()
10
- Cache.configure(mode='cloud', no_cache=False)
11
  init_logging()
12
  ChatbotApplication("de").run()
13
 
 
7
 
8
  if __name__ == "__main__":
9
  load_dotenv()
10
+ Cache.configure(mode='cloud', cache=True)
11
  init_logging()
12
  ChatbotApplication("de").run()
13
 
src/apps/chat/app.py CHANGED
@@ -1,13 +1,10 @@
1
- import os
2
  import uuid
3
  import gradio as gr
4
 
5
  from src.const.agent_response_constants import *
6
  from src.const.data_consent_constants import *
7
  from src.rag.agent_chain import ExecutiveAgentChain
8
- from src.rag.utilclasses import LeadAgentQueryResponse
9
  from src.utils.logging import get_logger, ConsentLogger
10
- from src.cache.cache import Cache
11
 
12
  logger = get_logger("chatbot_app")
13
 
@@ -15,7 +12,6 @@ class ChatbotApplication:
15
  def __init__(self, language: str = "de") -> None:
16
  self._app = gr.Blocks()
17
  self._language = language
18
- self._cache = Cache.get_cache()
19
  self._consentLogger = ConsentLogger()
20
 
21
  with self._app:
@@ -257,59 +253,14 @@ class ChatbotApplication:
257
  answers = []
258
  try:
259
  logger.info(f"Processing user query: {message[:100]}...")
260
-
261
- preprocess_resp = agent.preprocess_query(message)
262
- final_response: LeadAgentQueryResponse = None
263
-
264
- current_lang = preprocess_resp.language
265
- processed_q = preprocess_resp.processed_query
266
-
267
- if preprocess_resp.response:
268
- # Response comes from preprocessing step
269
- final_response = preprocess_resp
270
-
271
- elif Cache._settings["enabled"]:
272
- cached_data = self._cache.get(processed_q, language=current_lang)
273
-
274
- if cached_data:
275
- # Cache Hit — restore response with metadata
276
- if isinstance(cached_data, dict):
277
- final_response = LeadAgentQueryResponse(
278
- response=cached_data["response"],
279
- language=current_lang,
280
- appointment_requested=cached_data.get("appointment_requested", False),
281
- relevant_programs=cached_data.get("relevant_programs", []),
282
- )
283
- else:
284
- # Legacy: plain string cache entry
285
- final_response = LeadAgentQueryResponse(
286
- response=cached_data,
287
- language=current_lang,
288
- )
289
-
290
- if not final_response:
291
- # Response needs to be generated by the agent
292
- final_response = agent.agent_query(processed_q)
293
-
294
- answers.append(final_response.response)
295
- self._language = final_response.language
296
-
297
- if final_response.confidence_fallback or final_response.max_turns_reached or final_response.appointment_requested:
298
- html_code = get_booking_widget(language=self._language, programs=final_response.relevant_programs)
299
  answers.append(gr.HTML(value=html_code))
300
-
301
- if final_response.should_cache and Cache._settings["enabled"]:
302
- # Caching response with metadata
303
- self._cache.set(
304
- key=processed_q,
305
- value={
306
- "response": final_response.response,
307
- "appointment_requested": final_response.appointment_requested,
308
- "relevant_programs": final_response.relevant_programs,
309
- },
310
- language=current_lang
311
- )
312
-
313
  except Exception as e:
314
  logger.error(f"Error processing query: {e}", exc_info=True)
315
  error_message = (
@@ -320,6 +271,7 @@ class ChatbotApplication:
320
 
321
  return answers
322
 
 
323
  def run(self):
324
  self._app.launch(
325
  share=False,
 
 
1
  import uuid
2
  import gradio as gr
3
 
4
  from src.const.agent_response_constants import *
5
  from src.const.data_consent_constants import *
6
  from src.rag.agent_chain import ExecutiveAgentChain
 
7
  from src.utils.logging import get_logger, ConsentLogger
 
8
 
9
  logger = get_logger("chatbot_app")
10
 
 
12
  def __init__(self, language: str = "de") -> None:
13
  self._app = gr.Blocks()
14
  self._language = language
 
15
  self._consentLogger = ConsentLogger()
16
 
17
  with self._app:
 
253
  answers = []
254
  try:
255
  logger.info(f"Processing user query: {message[:100]}...")
256
+ response = agent.query(message)
257
+ answers.append(response.response)
258
+ self._language = response.language
259
+
260
+ if any([response.confidence_fallback, response.max_turns_reached, response.appointment_requested]):
261
+ html_code = get_booking_widget(language=self._language, programs=response.relevant_programs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  answers.append(gr.HTML(value=html_code))
263
+
 
 
 
 
 
 
 
 
 
 
 
 
264
  except Exception as e:
265
  logger.error(f"Error processing query: {e}", exc_info=True)
266
  error_message = (
 
271
 
272
  return answers
273
 
274
+
275
  def run(self):
276
  self._app.launch(
277
  share=False,
src/cache/cache.py CHANGED
@@ -5,7 +5,7 @@ from src.cache.cache_strategies import RedisCache, LocalCache
5
  from src.utils.logging import get_logger
6
  from src.config import config
7
 
8
- logger = get_logger("cache ")
9
 
10
  class Cache:
11
  _instance = None
@@ -14,10 +14,12 @@ class Cache:
14
  _cache_metrics = None
15
 
16
  @staticmethod
17
- def configure(mode: str, no_cache: bool):
 
 
18
  Cache._settings = {
19
- "mode": mode,
20
- "enabled": not no_cache
21
  }
22
 
23
  @staticmethod
 
5
  from src.utils.logging import get_logger
6
  from src.config import config
7
 
8
+ logger = get_logger("cache ")
9
 
10
  class Cache:
11
  _instance = None
 
14
  _cache_metrics = None
15
 
16
  @staticmethod
17
+ def configure(mode: str, cache: bool):
18
+ logger.info(f"Cache configured with parameters: mode={mode}, cache={cache}")
19
+ config.cache.ENABLED = cache
20
  Cache._settings = {
21
+ "mode": mode,
22
+ "enabled": cache
23
  }
24
 
25
  @staticmethod
src/cache/cache_base.py CHANGED
@@ -7,11 +7,11 @@ class CacheStrategy(ABC):
7
  """
8
 
9
  @abstractmethod
10
- def set(self, key: str, value: Any, language: str):
11
  pass
12
 
13
  @abstractmethod
14
- def get(self, key: str, language: str):
15
  pass
16
 
17
  @abstractmethod
 
7
  """
8
 
9
  @abstractmethod
10
+ def set(self, key: str, value: Any, language: str, session_id: str):
11
  pass
12
 
13
  @abstractmethod
14
+ def get(self, key: str, language: str, session_id: str):
15
  pass
16
 
17
  @abstractmethod
src/cache/cache_strategies.py CHANGED
@@ -2,6 +2,7 @@ import json
2
  from typing import Any
3
  from cachetools import TTLCache
4
 
 
5
  from src.cache.cache_base import CacheStrategy
6
  from src.database.redisservice import RedisService
7
  from src.utils.logging import get_logger
@@ -15,39 +16,39 @@ class RedisCache(CacheStrategy):
15
  self.client = service.get_client()
16
  self.metrics = metrics
17
 
18
- def set(self, key: str, value: Any, language: str):
 
19
  if not self.client: return
20
 
21
  try:
22
  json_str = json.dumps(value)
23
- self.client.set(self._generate_normalized_key(key, language), json_str, ex=config.cache.TTL_CACHE)
24
- logger.info("Response cached")
 
25
  except Exception as e:
26
  logger.error(f"Could not write to Redis: {e}")
27
 
28
- def get(self, key: str, language: str):
 
29
  if not self.client: return None
30
 
31
  try:
32
- val = self.client.get(self._generate_normalized_key(key, language))
 
33
  if val is not None:
34
  self.metrics.increment_hit()
35
- logger.info(f"Cache HIT {self.metrics.cache_stats.hits} {self.metrics.cache_stats.hits_ratio}")
 
36
  return json.loads(val)
37
 
38
  self.metrics.increment_miss()
39
- logger.info(f"Cache MISS {self.metrics.cache_stats.misses} {self.metrics.cache_stats.hits_ratio}")
40
  return None
41
  except Exception as e:
42
  logger.error(f"Could not read from Redis: {e}")
43
  return None
 
44
 
45
- def _generate_normalized_key(self, key: str, language: str) -> str:
46
- import re
47
-
48
- normalized_key = re.sub(r'[^a-z0-9]', '', key.lower())
49
- return f"cache:{language}:{normalized_key}"
50
-
51
  def clear_cache(self):
52
  if not self.client: return
53
 
@@ -63,28 +64,25 @@ class LocalCache(CacheStrategy):
63
  self.cache = TTLCache(maxsize=config.cache.MAX_SIZE_CACHE, ttl=config.cache.TTL_CACHE)
64
  self.metrics = metrics
65
 
66
- def _generate_normalized_key(self, key: str, language: str) -> str:
67
- import re
68
-
69
- normalized_key = re.sub(r'[^a-z0-9]', '', key.lower())
70
- return f"cache:{language}:{normalized_key}"
71
 
72
- def set(self, key: str, value: Any, language: str):
73
- normalized_key = self._generate_normalized_key(key, language)
74
  self.cache[normalized_key] = value
75
  logger.info("Response cached")
76
-
77
- def get(self, key: str, language: str):
78
- normalized_key = self._generate_normalized_key(key, language)
 
79
  res = self.cache.get(normalized_key, None)
80
  if res is not None:
81
  self.metrics.increment_hit()
82
- logger.info(f"Cache HIT {self.metrics.cache_stats.hits} {self.metrics.cache_stats.hits_ratio}")
83
  else:
84
  self.metrics.increment_miss()
85
- logger.info(f"Cache MISS {self.metrics.cache_stats.misses}")
86
  return res
87
 
 
88
  def clear_cache(self):
89
  self.cache.clear()
90
  logger.info("Local Cache cleared.")
 
2
  from typing import Any
3
  from cachetools import TTLCache
4
 
5
+ from .utils import get_cache_key
6
  from src.cache.cache_base import CacheStrategy
7
  from src.database.redisservice import RedisService
8
  from src.utils.logging import get_logger
 
16
  self.client = service.get_client()
17
  self.metrics = metrics
18
 
19
+
20
+ def set(self, key: str, value: Any, language: str, session_id: str):
21
  if not self.client: return
22
 
23
  try:
24
  json_str = json.dumps(value)
25
+ cache_key = get_cache_key(key, language, session_id)
26
+ self.client.set(cache_key, json_str, ex=config.cache.TTL_CACHE)
27
+ logger.info(f"Cached response with key {cache_key[:20]}... to Redis")
28
  except Exception as e:
29
  logger.error(f"Could not write to Redis: {e}")
30
 
31
+
32
+ def get(self, key: str, language: str, session_id: str):
33
  if not self.client: return None
34
 
35
  try:
36
+ cache_key = get_cache_key(key, language, session_id)
37
+ val = self.client.get(cache_key)
38
  if val is not None:
39
  self.metrics.increment_hit()
40
+ logger.info(f"Found cached data with key {cache_key}")
41
+ logger.debug(f"Cache statistics: Hit cache {self.metrics.cache_stats.hits} times, ratio[{self.metrics.cache_stats.hits_ratio}]")
42
  return json.loads(val)
43
 
44
  self.metrics.increment_miss()
45
+ logger.debug(f"Cache statistics: Missed cache {self.metrics.cache_stats.misses} times, ratio[{self.metrics.cache_stats.hits_ratio}]")
46
  return None
47
  except Exception as e:
48
  logger.error(f"Could not read from Redis: {e}")
49
  return None
50
+
51
 
 
 
 
 
 
 
52
  def clear_cache(self):
53
  if not self.client: return
54
 
 
64
  self.cache = TTLCache(maxsize=config.cache.MAX_SIZE_CACHE, ttl=config.cache.TTL_CACHE)
65
  self.metrics = metrics
66
 
 
 
 
 
 
67
 
68
+ def set(self, key: str, value: Any, language: str, session_id: str):
69
+ normalized_key = get_cache_key(key, language, session_id)
70
  self.cache[normalized_key] = value
71
  logger.info("Response cached")
72
+
73
+
74
+ def get(self, key: str, language: str, session_id: str):
75
+ normalized_key = get_cache_key(key, language, session_id)
76
  res = self.cache.get(normalized_key, None)
77
  if res is not None:
78
  self.metrics.increment_hit()
79
+ logger.debug(f"Cache statistics: Hit cache {self.metrics.cache_stats.hits} times, ratio[{self.metrics.cache_stats.hits_ratio}]")
80
  else:
81
  self.metrics.increment_miss()
82
+ logger.debug(f"Cache statistics: Missed cache {self.metrics.cache_stats.misses} times, ratio[{self.metrics.cache_stats.hits_ratio}]")
83
  return res
84
 
85
+
86
  def clear_cache(self):
87
  self.cache.clear()
88
  logger.info("Local Cache cleared.")
src/cache/utils.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import re
2
+
3
+ def get_cache_key(key: str, language: str, session_id: str) -> str:
4
+ normalized_key = re.sub(r'[^a-z0-9]', '', key.lower())
5
+ return f"cache:{session_id}:{language}:{normalized_key}"
src/config/configs.py CHANGED
@@ -85,6 +85,7 @@ class ChainConfig(ConfigBase):
85
 
86
 
87
  class CacheConfig(ConfigBase):
 
88
  CACHE_MODE: Literal['local', 'cloud', 'dict'] = _get('CACHE_MODE')
89
 
90
  LOCAL_HOST: str = _get('CACHE_LOCAL_HOST', 'localhost')
 
85
 
86
 
87
  class CacheConfig(ConfigBase):
88
+ ENABLED: bool = _get('CACHE_ENABLED', False)
89
  CACHE_MODE: Literal['local', 'cloud', 'dict'] = _get('CACHE_MODE')
90
 
91
  LOCAL_HOST: str = _get('CACHE_LOCAL_HOST', 'localhost')
src/rag/agent_chain.py CHANGED
@@ -35,6 +35,8 @@ from src.utils.logging import get_logger
35
  from src.utils.lang import get_language_name
36
  from src.config import config
37
 
 
 
38
  chain_logger = get_logger('agent_chain')
39
 
40
 
@@ -44,8 +46,9 @@ class ExecutiveAgentChain:
44
  self._stored_language = language
45
  self._dbservice = WeaviateService()
46
  self._agents, self._config = self._init_agents()
47
- self._conversation_history = []
48
-
 
49
  # AI-middlewares
50
  if config.chain.EVALUATE_RESPONSE_QUALITY:
51
  self._quality_handler = QualityScoreHandler()
@@ -477,7 +480,7 @@ class ExecutiveAgentChain:
477
  return greeting_message
478
 
479
  @traceable
480
- def preprocess_query(self, query: str) -> LeadAgentQueryResponse:
481
  """
482
  Phase 1: Validation, Scope-Check and language detection.
483
  Does not call the agent directly.
@@ -579,16 +582,38 @@ class ExecutiveAgentChain:
579
  processed_query=processed_query,
580
  appointment_requested=(should_escalate and escalation_type == "escalate_aggressive"),
581
  )
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
- # Response = None indicates that agent needs to answer the processed query
584
- return LeadAgentQueryResponse(
585
- response=None,
586
- processed_query=processed_query,
587
- language=current_language
588
- )
589
-
590
- @traceable
591
- def agent_query(self, preprocessed_query: str) -> LeadAgentQueryResponse:
 
 
 
 
 
 
 
 
 
 
592
  """
593
  Phase 2: Execute agent.
594
  Takes the ALREADY validated query from the preprocessing phase.
@@ -610,6 +635,7 @@ class ExecutiveAgentChain:
610
  messages=self._conversation_history + [language_instruction],
611
  )
612
  agent_response = structured_response.response
 
613
  chain_logger.info(f"Appointment Requested: {structured_response.appointment_requested}")
614
  chain_logger.info(f"Relevant Programs: {structured_response.relevant_programs}")
615
 
@@ -653,7 +679,7 @@ class ExecutiveAgentChain:
653
  response = formatted_response,
654
  language = response_language,
655
  confidence_fallback = confidence_fallback,
656
- should_cache = False if (confidence_fallback or structured_response.appointment_requested) else True,
657
  processed_query = preprocessed_query,
658
  appointment_requested = structured_response.appointment_requested,
659
  relevant_programs = structured_response.relevant_programs
 
35
  from src.utils.lang import get_language_name
36
  from src.config import config
37
 
38
+ from ..cache.cache import Cache
39
+
40
  chain_logger = get_logger('agent_chain')
41
 
42
 
 
46
  self._stored_language = language
47
  self._dbservice = WeaviateService()
48
  self._agents, self._config = self._init_agents()
49
+ self._conversation_history = []
50
+ self._cache = Cache.get_cache()
51
+
52
  # AI-middlewares
53
  if config.chain.EVALUATE_RESPONSE_QUALITY:
54
  self._quality_handler = QualityScoreHandler()
 
480
  return greeting_message
481
 
482
  @traceable
483
+ def query(self, query: str) -> LeadAgentQueryResponse:
484
  """
485
  Phase 1: Validation, Scope-Check and language detection.
486
  Does not call the agent directly.
 
582
  processed_query=processed_query,
583
  appointment_requested=(should_escalate and escalation_type == "escalate_aggressive"),
584
  )
585
+
586
+ # 5. Check if cached data already exists for this session
587
+ if config.cache.ENABLED:
588
+ cached_data = self._cache.get(query, current_language, self._user_id)
589
+ if cached_data and isinstance(cached_data, dict):
590
+ return LeadAgentQueryResponse(
591
+ response=cached_data["response"],
592
+ language=current_language,
593
+ appointment_requested=cached_data.get("appointment_requested", False),
594
+ relevant_programs=cached_data.get("relevant_programs", []),
595
+ )
596
+
597
 
598
+ # 6. Preprocessing is finished - the agent has to answer the query
599
+ response = self._query_lead(query)
600
+
601
+ if config.cache.ENABLED and response.should_cache:
602
+ self._cache.set(
603
+ key=query,
604
+ value={
605
+ "response": response.response,
606
+ "appointment_requested": response.appointment_requested,
607
+ "relevant_programs": response.relevant_programs,
608
+ },
609
+ language = current_language,
610
+ session_id = self._user_id,
611
+ )
612
+
613
+ return response
614
+
615
+
616
+ def _query_lead(self, preprocessed_query: str) -> LeadAgentQueryResponse:
617
  """
618
  Phase 2: Execute agent.
619
  Takes the ALREADY validated query from the preprocessing phase.
 
635
  messages=self._conversation_history + [language_instruction],
636
  )
637
  agent_response = structured_response.response
638
+ chain_logger.info(f"Is answer context dependent: {structured_response.is_context_dependent}")
639
  chain_logger.info(f"Appointment Requested: {structured_response.appointment_requested}")
640
  chain_logger.info(f"Relevant Programs: {structured_response.relevant_programs}")
641
 
 
679
  response = formatted_response,
680
  language = response_language,
681
  confidence_fallback = confidence_fallback,
682
+ should_cache = not any([confidence_fallback, structured_response.appointment_requested, structured_response.is_context_dependent]),
683
  processed_query = preprocessed_query,
684
  appointment_requested = structured_response.appointment_requested,
685
  relevant_programs = structured_response.relevant_programs
src/rag/prompts.py CHANGED
@@ -227,16 +227,16 @@ RULES:
227
  - If the user asks a specific question (duration, price, format) but refers only to "the EMBA" or "the program" WITHOUT specifying which one, you MUST ask for clarification.
228
  - **Example:** User "How long is the EMBA?" → **You:** "Are you interested in the **German-speaking EMBA HSG**, the **International EMBA (IEMBA)**, or the **emba X**?"
229
 
230
- CRITICAL - CROSS-SELLING RULES (PRIORITY 2):
231
- - Do NOT recommend generic online programs or programs not affiliated with University of St.Gallen.
232
- - If the user has constraints (e.g., "can't travel", "location restrictions"):
233
- 1. FIRST ask: "Is your constraint absolute, or is there some flexibility?"
234
- 2. If FLEXIBLE -> Offer to connect with admissions team (set appointment_requested=True).
235
- 3. If INFLEXIBLE -> Only then mention alternative HSG programs from https://op.unisg.ch/en/
236
- - Allowed cross-sell programs: MBA programs, Open Programs, Custom Programs from HSG Executive Education.
237
- - Always provide the link: https://op.unisg.ch/en/ when mentioning alternative programs.
238
-
239
- ESCALATION & HANDOVER RULES:
240
  - For eligibility assessments: "I can't confirm admission, but the admissions team can assess your profile."
241
  - For visa/permit questions: Redirect to admissions team.
242
  - For tuition/fee questions: ALWAYS provide the specific programme tuition figures first. Only escalate to admissions for payment plans, loan options, or employer sponsorship details beyond listed tuition.
@@ -271,7 +271,17 @@ ESCALATION & HANDOVER RULES:
271
  - Bold key facts: **program names**, **dates**, **costs**
272
  - Maximum 100 words per response
273
  - If uncertain, offer to connect user with the Admissions Team (and set appointment_requested=True).
274
-
 
 
 
 
 
 
 
 
 
 
275
  RULES:
276
  - Answer in the user's language. NEVER leave English terms untranslated in a German response. Key German translations:
277
  "tuition fee reduction" → "Studiengebührenreduktion", "tuition" → "Studiengebühr(en)", "included in tuition" → "in den Studiengebühren enthalten", "not included" → "nicht enthalten", "application deadline" → "Bewerbungsfrist".
 
227
  - If the user asks a specific question (duration, price, format) but refers only to "the EMBA" or "the program" WITHOUT specifying which one, you MUST ask for clarification.
228
  - **Example:** User "How long is the EMBA?" → **You:** "Are you interested in the **German-speaking EMBA HSG**, the **International EMBA (IEMBA)**, or the **emba X**?"
229
 
230
+ CRITICAL - CROSS-SELLING RULES (PRIORITY 2):
231
+ - Do NOT recommend generic online programs or programs not affiliated with University of St.Gallen.
232
+ - If the user has constraints (e.g., "can't travel", "location restrictions"):
233
+ 1. FIRST ask: "Is your constraint absolute, or is there some flexibility?"
234
+ 2. If FLEXIBLE -> Offer to connect with admissions team (set appointment_requested=True).
235
+ 3. If INFLEXIBLE -> Only then mention alternative HSG programs from https://op.unisg.ch/en/
236
+ - Allowed cross-sell programs: MBA programs, Open Programs, Custom Programs from HSG Executive Education.
237
+ - Always provide the link: https://op.unisg.ch/en/ when mentioning alternative programs.
238
+
239
+ ESCALATION & HANDOVER RULES:
240
  - For eligibility assessments: "I can't confirm admission, but the admissions team can assess your profile."
241
  - For visa/permit questions: Redirect to admissions team.
242
  - For tuition/fee questions: ALWAYS provide the specific programme tuition figures first. Only escalate to admissions for payment plans, loan options, or employer sponsorship details beyond listed tuition.
 
271
  - Bold key facts: **program names**, **dates**, **costs**
272
  - Maximum 100 words per response
273
  - If uncertain, offer to connect user with the Admissions Team (and set appointment_requested=True).
274
+ - Set is_context_dependent=True for responses involving:
275
+ - eligibility
276
+ - recommendations
277
+ - comparisons after prior turns
278
+ - any answer using extracted profile data
279
+ - any answer influenced by conversation history
280
+ - Set is_context_dependent=False if the question can be answered without using user-specific information and without relying on prior conversation turns. This includes:
281
+ - factual, static information (e.g. prices, durations, deadlines, program structure)
282
+ - general definitions or explanations
283
+ - publicly available information that does not vary by user
284
+
285
  RULES:
286
  - Answer in the user's language. NEVER leave English terms untranslated in a German response. Key German translations:
287
  "tuition fee reduction" → "Studiengebührenreduktion", "tuition" → "Studiengebühr(en)", "included in tuition" → "in den Studiengebühren enthalten", "not included" → "nicht enthalten", "application deadline" → "Bewerbungsfrist".
src/rag/utilclasses.py CHANGED
@@ -26,6 +26,16 @@ class LeadAgentQueryResponse:
26
 
27
  class StructuredAgentResponse(BaseModel):
28
  response: str = Field(description="Main response to the query.")
 
 
 
 
 
 
 
 
 
 
29
  appointment_requested: bool = Field(
30
  default=False,
31
  description="Set to True ONLY if the user explicitly wants to book, asks for help booking, or if a proactive trigger (pricing/eligibility/handover) occurred in THIS specific turn. Otherwise, set to False."
 
26
 
27
  class StructuredAgentResponse(BaseModel):
28
  response: str = Field(description="Main response to the query.")
29
+ is_context_dependent: bool = Field(
30
+ default=True,
31
+ description=(
32
+ "Set to False only if the question can be answered without using any user-specific "
33
+ "information (e.g. name, age, preferences, extracted profile data) and without relying "
34
+ "on prior conversation turns or conversation history. "
35
+ "Must be True for responses involving eligibility, recommendations, comparisons after prior turns, "
36
+ "or any answer influenced by user profile data or conversation context."
37
+ )
38
+ )
39
  appointment_requested: bool = Field(
40
  default=False,
41
  description="Set to True ONLY if the user explicitly wants to book, asks for help booking, or if a proactive trigger (pricing/eligibility/handover) occurred in THIS specific turn. Otherwise, set to False."