Pygmales commited on
Commit
593a090
·
1 Parent(s): 91d726a

updated app state

Browse files
app.py CHANGED
@@ -1,139 +1,11 @@
1
- import os
2
- import gradio as gr
3
  import langsmith
4
  from langsmith import traceable
5
- from src.rag.agent_chain import ExecutiveAgentChain
6
- from src.utils.logging import get_logger, init_logging
7
-
8
- init_logging(interactive_mode=False)
9
- logger = get_logger("chatbot_app")
10
-
11
  from dotenv import load_dotenv
12
- load_dotenv()
13
-
14
- class ChatbotApplication:
15
- def __init__(self, language: str = 'de') -> None:
16
- self._app = gr.Blocks()
17
-
18
- with self._app:
19
- # Initial state variables
20
- agent_state = gr.State(None)
21
- lang_state = gr.State(language)
22
-
23
- with gr.Row():
24
- lang_selector = gr.Radio(
25
- choices=["Deutsch", "English"],
26
- value="English" if language == 'en' else 'Deutsch',
27
- label="Selected Language",
28
- interactive=True,
29
- )
30
- reset_button = gr.Button("Reset Conversation")
31
-
32
- chat = gr.ChatInterface(
33
- fn=lambda msg, history, agent: self._chat(
34
- message=msg,
35
- history=history,
36
- agent=agent,
37
- ),
38
- additional_inputs=[agent_state],
39
- title="Executive Education Adviser",
40
- type='messages',
41
- )
42
-
43
-
44
- def clear_chat_immediate():
45
- return []
46
-
47
- def on_lang_change(language):
48
- lang_code = 'en' if language == 'English' else 'de'
49
- return switch_language(lang_code)
50
-
51
- def initalize_agent(language):
52
- agent = ExecutiveAgentChain(language=language)
53
- greeting = agent.generate_greeting()
54
- return agent, [{"role": "assistant", "content": greeting}]
55
-
56
- def switch_language(new_language):
57
- new_agent, greeting = initalize_agent(new_language)
58
- return (
59
- new_agent,
60
- new_language,
61
- greeting,
62
- )
63
-
64
- lang_selector.change(
65
- fn=clear_chat_immediate,
66
- outputs=[chat.chatbot_value],
67
- queue=True,
68
- )
69
-
70
- lang_selector.change(
71
- fn=on_lang_change,
72
- inputs=[lang_selector],
73
- outputs=[agent_state, lang_state, chat.chatbot_value],
74
- queue=True,
75
- )
76
-
77
- reset_button.click(
78
- fn=clear_chat_immediate,
79
- outputs=[chat.chatbot_value],
80
- queue=True,
81
- )
82
-
83
- reset_button.click(
84
- fn=switch_language,
85
- inputs=[lang_state],
86
- outputs=[agent_state, lang_state, chat.chatbot_value],
87
- queue=True,
88
- )
89
-
90
- # Initialize the agent chain on the app startup
91
- self._app.load(
92
- fn=lambda: initalize_agent(language),
93
- outputs=[agent_state, chat.chatbot_value],
94
- )
95
-
96
- @property
97
- def app(self) -> gr.Blocks:
98
- """Expose underlying Gradio Blocks for external runners (e.g., HF Spaces)."""
99
- return self._app
100
-
101
- def _chat(self, message: str, history: list[dict], agent: ExecutiveAgentChain):
102
- if agent is None:
103
- logger.error("Agent not initialized")
104
- return ["I apologize, but the chatbot is not properly initialized. Please refresh the page or contact support."]
105
-
106
- answers = []
107
- try:
108
- # Log user input
109
- logger.info(f"Processing user query: {message[:100]}...")
110
-
111
- # Query agent (now includes input handling, scope checking, and formatting)
112
- response = agent.query(query=message)
113
-
114
- logger.info(f"Received and formatted response from agent ({len(response)} chars)")
115
- answers.append(response)
116
-
117
- except Exception as e:
118
- logger.error(f"Error processing query: {e}", exc_info=True)
119
-
120
- # Provide helpful error message instead of empty string
121
- error_message = (
122
- "I apologize, but I encountered an error processing your request. "
123
- "Please try rephrasing your question or contact our admissions team for assistance."
124
- )
125
- answers.append(error_message)
126
-
127
- return answers
128
-
129
 
130
- def run(self):
131
- self._app.launch(
132
- share=os.getenv("GRADIO_SHARE", "false").lower() == "true",
133
- server_name=os.getenv("SERVER_NAME", "0.0.0.0"),
134
- server_port=int(os.getenv("PORT", 7860)),
135
- )
136
 
137
- if __name__ == '__main__':
138
- app = ChatbotApplication('de')
139
- app.run()
 
 
 
1
  import langsmith
2
  from langsmith import traceable
3
+ from src.apps.chat.app import ChatbotApplication
4
+ from src.utils.logging import init_logging
 
 
 
 
5
  from dotenv import load_dotenv
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ if __name__ == "__main__":
8
+ load_dotenv()
9
+ init_logging(interactive_mode=False)
10
+ ChatbotApplication("de").run()
 
 
11
 
 
 
 
config.py CHANGED
@@ -123,17 +123,21 @@ class LLMProviderConfiguration:
123
  # Weaviate database settings
124
  class WeaviateConfiguration:
125
  LOCAL_DATABASE = False
126
- WEAVIATE_BACKUP_BACKEND = ''
127
  WEAVIATE_COLLECTION_BASENAME = 'hsg_rag_content'
128
 
 
 
 
 
 
129
  # Weaviate Cloud settings
130
- CLUSTER_URL = "r2vd9fuvrcjvx7idsvta.c0.europe-west3.gcp.weaviate.cloud"
131
  WEAVIATE_API_KEY = os.getenv('WEAVIATE_API_KEY')
132
  HUGGING_FACE_API_KEY = os.getenv('HUGGING_FACE_API_KEY')
133
 
134
  # Custom timeouts for Cloud connection (in seconds)
135
- INIT_TIMEOUT = 10
136
- QUERY_TIMEOUT = 3
137
  INSERT_TIMEOUT = 120
138
 
139
  @classmethod
@@ -167,9 +171,13 @@ MAX_RESPONSE_WORDS_LEAD = 100 # Maximum words for lead agent responses
167
  MAX_RESPONSE_WORDS_SUBAGENT = 200 # Maximum words for subagent responses
168
  ENABLE_RESPONSE_CHUNKING = True # Break long responses into multiple turns
169
 
 
 
 
170
  # Conversation state settings
171
  TRACK_USER_PROFILE = True # Track user preferences and avoid repetition
172
  LOCK_LANGUAGE_AFTER_FIRST_MESSAGE = True # Don't change language mid-conversation
 
173
 
174
  # Data processing pipeline settings
175
  CHUNK_MAX_TOKENS = 8191
 
123
  # Weaviate database settings
124
  class WeaviateConfiguration:
125
  LOCAL_DATABASE = False
 
126
  WEAVIATE_COLLECTION_BASENAME = 'hsg_rag_content'
127
 
128
+ # Weaviate backup settings
129
+ AVAILABLE_BACKUP_METHODS = ['manual', 'filesystem', 's3']
130
+ BACKUP_METHOD = 'manual'
131
+ BACKUP_PATH = os.getenv('WEAVIATE_BACKUP_PATH')
132
+
133
  # Weaviate Cloud settings
134
+ CLUSTER_URL = os.getenv('WEAVIATE_CLUSTER_URL')
135
  WEAVIATE_API_KEY = os.getenv('WEAVIATE_API_KEY')
136
  HUGGING_FACE_API_KEY = os.getenv('HUGGING_FACE_API_KEY')
137
 
138
  # Custom timeouts for Cloud connection (in seconds)
139
+ INIT_TIMEOUT = 90
140
+ QUERY_TIMEOUT = 10
141
  INSERT_TIMEOUT = 120
142
 
143
  @classmethod
 
171
  MAX_RESPONSE_WORDS_SUBAGENT = 200 # Maximum words for subagent responses
172
  ENABLE_RESPONSE_CHUNKING = True # Break long responses into multiple turns
173
 
174
+ # Evaluation of agent response quality
175
+ ENABLE_EVALUATE_RESPONSE_QUALITY = True
176
+
177
  # Conversation state settings
178
  TRACK_USER_PROFILE = True # Track user preferences and avoid repetition
179
  LOCK_LANGUAGE_AFTER_FIRST_MESSAGE = True # Don't change language mid-conversation
180
+ MAX_CONVERSATION_TURNS = 15 # End conversation after max turns reached
181
 
182
  # Data processing pipeline settings
183
  CHUNK_MAX_TOKENS = 8191
src/apps/chat/app.py CHANGED
@@ -1,19 +1,22 @@
1
  import os
2
  import gradio as gr
3
- from src.rag.agent_chain import ExecutiveAgentChain
4
- from src.utils.logging import get_logger, cached_log_handler
 
 
 
5
 
6
  logger = get_logger("chatbot_app")
7
 
8
  class ChatbotApplication:
9
  def __init__(self, language: str = 'de') -> None:
10
- self._app = gr.Blocks()
11
-
 
12
  with self._app:
13
- # Initial state variables
14
  agent_state = gr.State(None)
15
- lang_state = gr.State(language)
16
-
17
  with gr.Row():
18
  lang_selector = gr.Radio(
19
  choices=["Deutsch", "English"],
@@ -22,9 +25,9 @@ class ChatbotApplication:
22
  interactive=True,
23
  )
24
  reset_button = gr.Button("Reset Conversation")
25
-
26
  chat = gr.ChatInterface(
27
- fn=lambda msg, history, agent: self._chat(
28
  message=msg,
29
  history=history,
30
  agent=agent,
@@ -34,14 +37,19 @@ class ChatbotApplication:
34
  type='messages',
35
  )
36
 
 
 
 
 
 
37
 
38
  def clear_chat_immediate():
39
- return []
40
-
41
  def on_lang_change(language):
42
  lang_code = 'en' if language == 'English' else 'de'
43
  return switch_language(lang_code)
44
-
45
  def initalize_agent(language):
46
  agent = ExecutiveAgentChain(language=language)
47
  greeting = agent.generate_greeting()
@@ -53,37 +61,40 @@ class ChatbotApplication:
53
  new_agent,
54
  new_language,
55
  greeting,
 
56
  )
57
-
58
  lang_selector.change(
59
  fn=clear_chat_immediate,
60
- outputs=[chat.chatbot_value],
61
  queue=True,
 
62
  )
63
 
64
  lang_selector.change(
65
  fn=on_lang_change,
66
  inputs=[lang_selector],
67
- outputs=[agent_state, lang_state, chat.chatbot_value],
68
  queue=True,
69
  )
70
 
71
  reset_button.click(
72
  fn=clear_chat_immediate,
73
- outputs=[chat.chatbot_value],
74
  queue=True,
 
75
  )
76
 
77
  reset_button.click(
78
  fn=switch_language,
79
  inputs=[lang_state],
80
- outputs=[agent_state, lang_state, chat.chatbot_value],
81
  queue=True,
82
  )
83
-
84
  # Initialize the agent chain on the app startup
85
  self._app.load(
86
- fn=lambda: initalize_agent(language),
87
  outputs=[agent_state, chat.chatbot_value],
88
  )
89
 
@@ -95,31 +106,28 @@ class ChatbotApplication:
95
  def _chat(self, message: str, history: list[dict], agent: ExecutiveAgentChain):
96
  if agent is None:
97
  logger.error("Agent not initialized")
98
- return ["I apologize, but the chatbot is not properly initialized. Please refresh the page or contact support."]
99
-
100
  answers = []
101
  try:
102
- # Log user input
103
  logger.info(f"Processing user query: {message[:100]}...")
104
-
105
- # Query agent (now includes input handling, scope checking, and formatting)
106
- response = agent.query(query=message)
107
-
108
- logger.info(f"Received and formatted response from agent ({len(response)} chars)")
109
- answers.append(response)
110
-
 
111
  except Exception as e:
112
  logger.error(f"Error processing query: {e}", exc_info=True)
113
-
114
- # Provide helpful error message instead of empty string
115
  error_message = (
116
  "I apologize, but I encountered an error processing your request. "
117
  "Please try rephrasing your question or contact our admissions team for assistance."
118
  )
119
  answers.append(error_message)
120
-
121
- return answers
122
 
 
123
 
124
  def run(self):
125
  self._app.launch(
 
1
  import os
2
  import gradio as gr
3
+ from src.apps.chat.js import JS_LISTENER, JS_CLEAR
4
+ from src.const.agent_response_constants import *
5
+ from src.rag.agent_chain import ExecutiveAgentChain
6
+ from src.rag.utilclasses import LeadAgentQueryResponse
7
+ from src.utils.logging import get_logger
8
 
9
  logger = get_logger("chatbot_app")
10
 
11
  class ChatbotApplication:
12
  def __init__(self, language: str = 'de') -> None:
13
+ self._app = gr.Blocks(js=JS_LISTENER)
14
+ self._language = language
15
+
16
  with self._app:
 
17
  agent_state = gr.State(None)
18
+ lang_state = gr.State(language)
19
+
20
  with gr.Row():
21
  lang_selector = gr.Radio(
22
  choices=["Deutsch", "English"],
 
25
  interactive=True,
26
  )
27
  reset_button = gr.Button("Reset Conversation")
28
+
29
  chat = gr.ChatInterface(
30
+ fn=lambda msg, history, agent: self._chat(
31
  message=msg,
32
  history=history,
33
  agent=agent,
 
37
  type='messages',
38
  )
39
 
40
+ iframe_container = gr.HTML(
41
+ value="",
42
+ elem_id="consultation-iframe-container",
43
+ visible=True
44
+ )
45
 
46
  def clear_chat_immediate():
47
+ return [], ""
48
+
49
  def on_lang_change(language):
50
  lang_code = 'en' if language == 'English' else 'de'
51
  return switch_language(lang_code)
52
+
53
  def initalize_agent(language):
54
  agent = ExecutiveAgentChain(language=language)
55
  greeting = agent.generate_greeting()
 
61
  new_agent,
62
  new_language,
63
  greeting,
64
+ ""
65
  )
66
+
67
  lang_selector.change(
68
  fn=clear_chat_immediate,
69
+ outputs=[chat.chatbot_value, iframe_container],
70
  queue=True,
71
+ js=JS_CLEAR
72
  )
73
 
74
  lang_selector.change(
75
  fn=on_lang_change,
76
  inputs=[lang_selector],
77
+ outputs=[agent_state, lang_state, chat.chatbot_value, iframe_container],
78
  queue=True,
79
  )
80
 
81
  reset_button.click(
82
  fn=clear_chat_immediate,
83
+ outputs=[chat.chatbot_value, iframe_container],
84
  queue=True,
85
+ js=JS_CLEAR
86
  )
87
 
88
  reset_button.click(
89
  fn=switch_language,
90
  inputs=[lang_state],
91
+ outputs=[agent_state, lang_state, chat.chatbot_value, iframe_container],
92
  queue=True,
93
  )
94
+
95
  # Initialize the agent chain on the app startup
96
  self._app.load(
97
+ fn=lambda: initalize_agent(self._language),
98
  outputs=[agent_state, chat.chatbot_value],
99
  )
100
 
 
106
  def _chat(self, message: str, history: list[dict], agent: ExecutiveAgentChain):
107
  if agent is None:
108
  logger.error("Agent not initialized")
109
+ return ["I apologize, but the chatbot is not properly initialized."]
110
+
111
  answers = []
112
  try:
 
113
  logger.info(f"Processing user query: {message[:100]}...")
114
+
115
+ lead_resp: LeadAgentQueryResponse = agent.query(query=message)
116
+ answers.append(lead_resp.response)
117
+ self._language = lead_resp.language
118
+
119
+ if lead_resp.confidence_fallback or lead_resp.max_turns_reached:
120
+ answers.extend(APPOINTMENT_LINKS[self._language])
121
+
122
  except Exception as e:
123
  logger.error(f"Error processing query: {e}", exc_info=True)
 
 
124
  error_message = (
125
  "I apologize, but I encountered an error processing your request. "
126
  "Please try rephrasing your question or contact our admissions team for assistance."
127
  )
128
  answers.append(error_message)
 
 
129
 
130
+ return answers
131
 
132
  def run(self):
133
  self._app.launch(
src/apps/chat/js.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ JS_LISTENER = """
2
+ function() {
3
+ document.addEventListener('click', function(e) {
4
+ // 1. Use .closest() to find the <a> tag even if user clicks the text/icon inside it
5
+ const target = e.target.closest('a.appointment-btn');
6
+
7
+ if (target) {
8
+ // 2. Prevent the link from opening in a new tab/window
9
+ e.preventDefault();
10
+
11
+ // 3. Get the URL from the standard href attribute
12
+ const url = target.getAttribute('href');
13
+ const container = document.getElementById('consultation-iframe-container');
14
+
15
+ if (container) {
16
+ container.innerHTML = `
17
+ <div style="margin-top: 20px; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
18
+ <div style="background: #f9fafb; padding: 10px; font-weight: bold; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
19
+ <span>Appointment Booking</span>
20
+ <button onclick="document.getElementById('consultation-iframe-container').innerHTML=''" style="cursor: pointer; color: red;">✕ Close</button>
21
+ </div>
22
+ <iframe src="${url}" width="100%" height="600px" frameborder="0"></iframe>
23
+ </div>
24
+ `;
25
+ container.scrollIntoView({ behavior: 'smooth' });
26
+ }
27
+ }
28
+ });
29
+ }
30
+ """
31
+
32
+ JS_CLEAR = """
33
+ function() {
34
+ const el = document.getElementById('consultation-iframe-container');
35
+ if (el) {
36
+ el.innerHTML = '';
37
+ }
38
+ }
39
+ """
src/const/agent_response_constants.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from gradio import ChatMessage
2
+
3
+ """ Constants for Gradio app """
4
+
5
+ GREETING_MESSAGES = {
6
+ "en": [
7
+ "Hello and welcome! I’m your Executive Education Advisor for the HSG Executive MBA programs (**IEMBA**, **emba X**, and **EMBA**). How can I best support your MBA planning today?",
8
+ "Hello and welcome! I’m your Executive Education Advisor for the University of St.Gallen’s Executive MBA programs (**IEMBA**, **emba X**, **EMBA**). How can I support your MBA planning today?",
9
+ "Hello and welcome! I’m your Executive Education Advisor for the HSG Executive MBA programs (**EMBA**, **IEMBA**, **emba X**). How can I help you with your EMBA journey today?",
10
+ "Hello and welcome! I’m your Executive Education Advisor for the University of St.Gallen’s EMBA programs, here to help you navigate our **EMBA**, **IEMBA**, and **emba X** options.",
11
+ "Hello and welcome. I’m your Executive Education Advisor for the University of St.Gallen’s Executive MBA programs, here to help you assess fit and navigate the **EMBA**, **IEMBA**, and **emba X** options.",
12
+ ],
13
+ "de": [
14
+ "Guten Tag! Ich bin Ihr Executive-Education-Berater für die HSG Executive MBA Programme und unterstütze Sie gerne bei Fragen zu **EMBA**, **IEMBA** und **emba X**.",
15
+ "Guten Tag, ich bin Ihr Executive-Education-Berater für die HSG Executive MBA Programme (**EMBA**, **IEMBA**, **emba X**). Ich unterstütze Sie bei Programmwahl, Ablauf und Zulassungsfragen.",
16
+ "Guten Tag und herzlich willkommen! Ich bin Ihr Executive Education Advisor für die HSG Executive MBA Programme und unterstütze Sie gern bei Fragen zu **EMBA**, **IEMBA** und **emba X**.",
17
+ "Guten Tag, ich bin Ihr Executive-Education-Berater für die HSG Executive MBA-Programme (**EMBA**, **IEMBA**, **emba X**) und unterstütze Sie gerne bei Programmwahl und Zulassungsfragen.",
18
+ "Guten Tag! Ich bin Ihr Executive-Education-Berater für die HSG Executive MBA Programme (**EMBA**, **IEMBA**, **emba X**) und unterstütze Sie gerne bei Programmwahl und Zulassungsfragen.",
19
+ ]
20
+ }
21
+
22
+ QUERY_EXCEPTION_MESSAGE = {
23
+ "en": "I'm sorry, I cannot provide a helpful response right now. Please contact tech support or try again later.",
24
+ "de": "Es tut mir leid, ich kann im Moment keine hilfreiche Antwort geben. Bitte wenden Sie sich an den technischen Support oder versuchen Sie es später erneut.",
25
+ }
26
+
27
+ NOT_VALID_QUERY_MESSAGE = {
28
+ "en": "I didn't quite understand that. Could you please rephrase your question?",
29
+ "de": "Das habe ich nicht ganz verstanden. Könnten Sie Ihre Frage bitte anders formulieren?",
30
+ }
31
+
32
+ CONFIDENCE_FALLBACK_MESSAGE = {
33
+ "en": (
34
+ "I'm sorry, but I couldn't find any information in my records that matches your request, "
35
+ "so I can't answer it with confidence. Could you please rephrase your question?\n\n"
36
+ "Alternatively, you can book an appointment with a student services advisor using the links below."
37
+ ),
38
+ "de": (
39
+ "Es tut mir leid, aber ich konnte in meinen Unterlagen keine Informationen finden, "
40
+ "die zu Ihrer Anfrage passen, sodass ich sie nicht mit ausreichender Sicherheit beantworten kann. "
41
+ "Könnten Sie Ihre Frage bitte umformulieren?\n\n"
42
+ "Alternativ können Sie über die untenstehenden Links einen Termin bei der Studienberatung buchen."
43
+ ),
44
+ }
45
+
46
+ LANGUAGE_FALLBACK_MESSAGE = {
47
+ "en": (
48
+ "I am sorry, I can only reply in English or German. "
49
+ "Would you like to continue our conversation in English?"
50
+ ),
51
+ "de": (
52
+ "Es tut mir leid, ich kann nur auf Englisch oder Deutsch antworten. "
53
+ "Möchten Sie unser Gespräch auf Deutsch fortführen?"
54
+ ),
55
+ }
56
+
57
+ CONVERSATION_END_MESSAGE = {
58
+ "en": (
59
+ "This conversation has reached its maximum length. "
60
+ "To make sure you receive the best possible support, "
61
+ "please continue with a personal consultation.\n\n"
62
+ "You can book an appointment with a student services advisor using the links below. "
63
+ "Thank you for your understanding."
64
+ ),
65
+ "de": (
66
+ "Dieses Gespräch hat die maximale Länge erreicht. "
67
+ "Damit Sie bestmöglich unterstützt werden, bitten wir Sie, "
68
+ "das Anliegen in einem persönlichen Beratungsgespräch fortzusetzen.\n\n"
69
+ "Über die untenstehenden Links können Sie einen Termin mit der Studienberatung buchen. "
70
+ "Vielen Dank für Ihr Verständnis."
71
+ ),
72
+ }
73
+
74
+
75
+ def create_appt_button(url, title, lang_text):
76
+ return (
77
+ f'<a href="{url}" class="appointment-btn" '
78
+ f'style="display: block; background-color: #f3f4f6; border: 1px solid #d1d5db; '
79
+ f'padding: 8px 16px; border-radius: 6px; cursor: pointer; '
80
+ f'color: #374151; font-weight: 600; width: 100%; text-align: left; '
81
+ f'margin-top: 5px; text-decoration: none;">'
82
+ f'📅 {lang_text}: {title}'
83
+ f'</a>'
84
+ )
85
+
86
+
87
+ APPOINTMENT_LINKS = {
88
+ "en": [
89
+ ChatMessage(
90
+ role="assistant",
91
+ content=create_appt_button(
92
+ "https://calendly.com/cyra-vonmueller/beratungsgespraech-emba-hsg",
93
+ "Cyra von Müller",
94
+ "Book Appointment"
95
+ ),
96
+ ),
97
+ ChatMessage(
98
+ role="assistant",
99
+ content=create_appt_button(
100
+ "https://calendly.com/kristin-fuchs-unisg/iemba-online-personal-consultation",
101
+ "Kristin Fuchs",
102
+ "Book Appointment"
103
+ ),
104
+ ),
105
+ ChatMessage(
106
+ role="assistant",
107
+ content=create_appt_button(
108
+ "https://calendly.com/teyuna-giger-unisg",
109
+ "Teyuna Giger",
110
+ "Book Appointment"
111
+ ),
112
+ ),
113
+ ],
114
+ "de": [
115
+ ChatMessage(
116
+ role="assistant",
117
+ content=create_appt_button(
118
+ "https://calendly.com/cyra-vonmueller/beratungsgespraech-emba-hsg",
119
+ "Cyra von Müller",
120
+ "Termin buchen"
121
+ ),
122
+ ),
123
+ ChatMessage(
124
+ role="assistant",
125
+ content=create_appt_button(
126
+ "https://calendly.com/kristin-fuchs-unisg/iemba-online-personal-consultation",
127
+ "Kristin Fuchs",
128
+ "Termin buchen"
129
+ ),
130
+ ),
131
+ ChatMessage(
132
+ role="assistant",
133
+ content=create_appt_button(
134
+ "https://calendly.com/teyuna-giger-unisg",
135
+ "Teyuna Giger",
136
+ "Termin buchen"
137
+ ),
138
+ ),
139
+ ],
140
+ }
src/database/weavservice.py CHANGED
@@ -99,9 +99,13 @@ class WeaviateService:
99
  if wvtconf.is_local():
100
  self._client = wvt.connect_to_local()
101
  break
 
 
 
 
102
 
103
  self._client = wvt.connect_to_weaviate_cloud(
104
- cluster_url=wvtconf.CLUSTER_URL,
105
  auth_credentials=wvtconf.WEAVIATE_API_KEY,
106
  additional_config=AdditionalConfig(
107
  timeout=Timeout(
 
99
  if wvtconf.is_local():
100
  self._client = wvt.connect_to_local()
101
  break
102
+
103
+ cluster_url = wvtconf.CLUSTER_URL
104
+ if not cluster_url.startswith('http'):
105
+ cluster_url = f"https://{cluster_url}"
106
 
107
  self._client = wvt.connect_to_weaviate_cloud(
108
+ cluster_url=cluster_url,
109
  auth_credentials=wvtconf.WEAVIATE_API_KEY,
110
  additional_config=AdditionalConfig(
111
  timeout=Timeout(
src/rag/agent_chain.py CHANGED
@@ -1,51 +1,64 @@
 
 
1
  from langchain.tools import tool
2
  from langchain.agents import create_agent
3
  from langchain_core.messages import (
4
- HumanMessage,
5
- AIMessage,
6
- SystemMessage,
7
  )
8
  from langchain.agents.middleware import ModelFallbackMiddleware
 
9
 
10
  import uuid
11
  import json
12
  import os
13
  import re
 
14
  from datetime import datetime
15
 
16
  from src.database.weavservice import WeaviateService
17
 
18
  from src.rag.utilclasses import *
 
19
  from src.rag.middleware import AgentChainMiddleware as chainmdw
20
  from src.rag.prompts import PromptConfigurator as promptconf
21
  from src.rag.models import ModelConfigurator as modelconf
22
  from src.rag.input_handler import InputHandler
23
  from src.rag.response_formatter import ResponseFormatter
24
  from src.rag.scope_guardian import ScopeGuardian
 
 
25
 
26
- from src.utils.lang import detect_language, get_language_name
27
- from src.utils.logging import get_logger
28
  from config import (
29
  TOP_K_RETRIEVAL,
30
- LOCK_LANGUAGE_AFTER_FIRST_MESSAGE,
31
  TRACK_USER_PROFILE,
32
- ENABLE_RESPONSE_CHUNKING
 
 
33
  )
34
 
35
  chain_logger = get_logger('agent_chain')
36
 
 
37
  class ExecutiveAgentChain:
38
  def __init__(self, language: str = 'en') -> None:
39
- self._initial_language = language
40
- self._language = language
41
- self._user_language = None # Will be locked after first user message
42
  self._dbservice = WeaviateService()
43
  self._agents, self._config = self._init_agents()
44
  self._conversation_history = []
45
 
 
 
 
 
 
46
  # Generate unique user ID for this session
47
  self._user_id = str(uuid.uuid4())
48
-
49
  # Initialize conversation state with user profile tracking
50
  self._conversation_state: ConversationState = {
51
  'user_id': self._user_id,
@@ -62,12 +75,11 @@ class ExecutiveAgentChain:
62
  'topics_discussed': [],
63
  'preferences_known': False
64
  }
65
-
66
  # Track scope violations for escalation
67
  self._scope_violation_count = 0
68
-
69
- chain_logger.info(f"Initialized new Agent Chain for language '{language}' with user_id: {self._user_id}")
70
 
 
71
 
72
  def _retrieve_context(self, query: str, language: str = None):
73
  """
@@ -77,18 +89,17 @@ class ExecutiveAgentChain:
77
  query: Keywords depicting information you want to retrieve in the primary language.
78
  language: Optional parameter (either 'en' for English language or 'de' for German language). This parameter selects the language of the database to query from. The input query must be written in the same language as the selected language. Use this parameter only if there's not enough information in your main language.
79
  """
80
- lang = language or self._language
81
  try:
82
  response, _ = self._dbservice.query(
83
- query=query,
84
- lang=lang,
85
  limit=TOP_K_RETRIEVAL,
86
  )
87
  serialized = '\n\n'.join([doc.properties.get('body', '') for doc in response.objects])
88
  return serialized
89
  except Exception as e:
90
  raise e
91
-
92
 
93
  def _call_emba_agent(self, query: str) -> str:
94
  """
@@ -98,17 +109,16 @@ class ExecutiveAgentChain:
98
  query: Query to the EMBA support agent. Provide collected user data in the query if possible.
99
  """
100
  try:
101
- response = self._query(
102
- agent=self._agents['emba'],
103
  messages=[HumanMessage(query)],
104
  thread_id=f"emba_{hash(query)}",
105
  )
106
- return response
107
  except Exception as e:
108
  chain_logger.error(f"EMBA Agent error: {e}")
109
  raise RuntimeError("Unable to retrieve EMBA information at this time.")
110
 
111
-
112
  def _call_iemba_agent(self, query: str) -> str:
113
  """
114
  Invokes the IEMBA support agent to retrieve more detailed information about the IEMBA program.
@@ -117,17 +127,16 @@ class ExecutiveAgentChain:
117
  query: Query to the IEMBA support agent. Provide collected user data in the query if possible.
118
  """
119
  try:
120
- response = self._query(
121
- agent=self._agents['iemba'],
122
  messages=[HumanMessage(query)],
123
  thread_id=f"emba_{hash(query)}",
124
  )
125
- return response
126
  except Exception as e:
127
  chain_logger.error(f"IEMBA Agent error: {e}")
128
  raise RuntimeError("Unable to retrieve IEMBA information at this time.")
129
 
130
-
131
  def _call_embax_agent(self, query: str) -> str:
132
  """
133
  Invokes the emba X support agent to retrieve more detailed information about the emba X program.
@@ -136,17 +145,16 @@ class ExecutiveAgentChain:
136
  query: Query to the emba X support agent. Provide collected user data in the query if possible.
137
  """
138
  try:
139
- response = self._query(
140
- agent=self._agents['embax'],
141
  messages=[HumanMessage(query)],
142
  thread_id=f"emba_{hash(query)}",
143
  )
144
- return response
145
  except Exception as e:
146
  chain_logger.error(f"emba X Agent error: {e}")
147
  raise RuntimeError("Unable to retrieve emba X information at this time.")
148
 
149
-
150
  def _init_agents(self):
151
  config: RunnableConfig = {
152
  'configurable': {'thread_id': 0}
@@ -186,22 +194,25 @@ class ExecutiveAgentChain:
186
  model=modelconf.get_main_agent_model(),
187
  tools=tools_agent_calling,
188
  state_schema=LeadInformationState,
189
- system_prompt=promptconf.get_configured_agent_prompt('lead', language=self._language),
190
  middleware=[
191
  chainmdw.get_tool_wrapper(),
192
  chainmdw.get_model_wrapper(),
193
  fallback_middleware,
194
  ],
195
  context_schema=AgentContext,
196
- ),
 
 
 
197
  }
198
  for agent in ['emba', 'iemba', 'embax']:
199
- agents[agent]=create_agent(
200
  name=f"{agent.upper()} Agent",
201
  model=modelconf.get_subagent_model(),
202
  tools=[tool_retrieve_context],
203
  state_schema=LeadInformationState,
204
- system_prompt=promptconf.get_configured_agent_prompt(agent, language=self._language),
205
  middleware=[
206
  fallback_middleware,
207
  chainmdw.get_tool_wrapper(),
@@ -210,7 +221,7 @@ class ExecutiveAgentChain:
210
  context_schema=AgentContext,
211
  )
212
  return agents, config
213
-
214
  def _extract_experience_years(self, conversation: str) -> int | None:
215
  """Extract years of professional experience from conversation text."""
216
  # Look for patterns like "10 years", "5 years experience", etc.
@@ -243,7 +254,7 @@ class ExecutiveAgentChain:
243
  """Extract professional field/industry from conversation text."""
244
  # Common fields mentioned in executive education
245
  fields = [
246
- 'finance', 'banking', 'technology', 'tech', 'IT', 'healthcare',
247
  'consulting', 'manufacturing', 'retail', 'marketing', 'sales',
248
  'engineering', 'pharma', 'telecommunications', 'energy',
249
  'Finanzwesen', 'Technologie', 'Gesundheitswesen', 'Beratung' # German
@@ -264,8 +275,8 @@ class ExecutiveAgentChain:
264
  'Strategie', 'Innovation', 'Führung', 'Digitalisierung' # German
265
  ]
266
  conversation_lower = conversation.lower()
267
- found_interests = [interest for interest in interests
268
- if interest.lower() in conversation_lower]
269
  return ', '.join(found_interests) if found_interests else None
270
 
271
  def _extract_name(self, conversation: str) -> str | None:
@@ -301,15 +312,15 @@ class ExecutiveAgentChain:
301
  def _determine_suggested_program(self) -> str | None:
302
  """Determine recommended program based on user profile."""
303
  state = self._conversation_state
304
-
305
  # If program interest was explicitly mentioned
306
  if state['program_interest']:
307
  return state['program_interest'][0]
308
-
309
  # Make recommendation based on profile
310
  experience = state.get('experience_years', 0) or 0
311
  leadership = state.get('leadership_years', 0) or 0
312
-
313
  # EMBA: 5+ years experience, 2+ years leadership
314
  if experience >= 5 and leadership >= 2:
315
  return 'EMBA'
@@ -317,64 +328,64 @@ class ExecutiveAgentChain:
317
  elif experience >= 3:
318
  return 'IEMBA'
319
  # EMBA X: Digital/Innovation focus
320
- elif state.get('interest') and any(kw in state.get('interest', '').lower()
321
  for kw in ['digital', 'innovation', 'technology']):
322
  return 'EMBA X'
323
-
324
  return None
325
 
326
  def _update_conversation_state(self, user_query: str, agent_response: str) -> None:
327
  """Update conversation state by extracting information from the conversation."""
328
  if not TRACK_USER_PROFILE:
329
  return
330
-
331
  # Combine query and response for analysis
332
  conversation_text = f"{user_query} {agent_response}"
333
-
334
  # Extract profile information
335
  if not self._conversation_state.get('experience_years'):
336
  exp_years = self._extract_experience_years(conversation_text)
337
  if exp_years:
338
  self._conversation_state['experience_years'] = exp_years
339
  chain_logger.info(f"Extracted experience years: {exp_years}")
340
-
341
  if not self._conversation_state.get('leadership_years'):
342
  lead_years = self._extract_leadership_years(conversation_text)
343
  if lead_years:
344
  self._conversation_state['leadership_years'] = lead_years
345
  chain_logger.info(f"Extracted leadership years: {lead_years}")
346
-
347
  if not self._conversation_state.get('field'):
348
  field = self._extract_field(conversation_text)
349
  if field:
350
  self._conversation_state['field'] = field
351
  chain_logger.info(f"Extracted field: {field}")
352
-
353
  if not self._conversation_state.get('interest'):
354
  interest = self._extract_interest(conversation_text)
355
  if interest:
356
  self._conversation_state['interest'] = interest
357
  chain_logger.info(f"Extracted interest: {interest}")
358
-
359
  # Extract name
360
  if not self._conversation_state.get('user_name'):
361
  name = self._extract_name(conversation_text)
362
  if name:
363
  self._conversation_state['user_name'] = name
364
  chain_logger.info(f"Extracted name: {name}")
365
-
366
  # Detect handover request
367
  if self._detect_handover_request(conversation_text):
368
  self._conversation_state['handover_requested'] = True
369
  chain_logger.info("Handover request detected")
370
-
371
  # Check for program mentions
372
  programs = ['EMBA', 'IEMBA', 'EMBA X']
373
  for program in programs:
374
  if program.lower() in conversation_text.lower():
375
  if program not in self._conversation_state['program_interest']:
376
  self._conversation_state['program_interest'].append(program)
377
-
378
  # Update suggested program
379
  suggested = self._determine_suggested_program()
380
  if suggested and not self._conversation_state.get('suggested_program'):
@@ -385,12 +396,12 @@ class ExecutiveAgentChain:
385
  """Log user profile to JSON file."""
386
  if not TRACK_USER_PROFILE:
387
  return
388
-
389
  try:
390
  # Create logs directory if it doesn't exist
391
  log_dir = os.path.join('logs', 'user_profiles')
392
  os.makedirs(log_dir, exist_ok=True)
393
-
394
  # Create profile data
395
  profile_data = {
396
  'user_id': self._conversation_state['user_id'],
@@ -405,34 +416,26 @@ class ExecutiveAgentChain:
405
  'user_language': self._conversation_state.get('user_language'),
406
  'program_interest': self._conversation_state.get('program_interest', []),
407
  }
408
-
409
  # Log file path with timestamp
410
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
411
  log_file = os.path.join(log_dir, f'profile_{self._user_id}_{timestamp}.json')
412
-
413
  # Write to file
414
  with open(log_file, 'w', encoding='utf-8') as f:
415
  json.dump(profile_data, f, indent=2, ensure_ascii=False)
416
-
417
  chain_logger.info(f"User profile logged to {log_file}")
418
-
419
  except Exception as e:
420
  chain_logger.error(f"Failed to log user profile: {e}")
421
 
422
  def generate_greeting(self) -> str:
423
- self._conversation_history.extend([
424
- SystemMessage("Generate a short greeting message and introduce yourself. 30 words max."),
425
- SystemMessage(f"Respond in {get_language_name(self._language)} language."),
426
- ])
427
- response = self._query(
428
- agent=self._agents['lead'],
429
- messages=self._conversation_history,
430
- )
431
- self._conversation_history.append(AIMessage(response))
432
- return response
433
-
434
 
435
- def query(self, query: str) -> str:
 
436
  """
437
  Process user query with input handling, scope checking, and response formatting.
438
 
@@ -442,44 +445,47 @@ class ExecutiveAgentChain:
442
  Returns:
443
  Formatted response
444
  """
 
 
 
 
 
 
 
 
 
 
445
  # Step 1: Process input (handle numeric inputs, validation)
446
  processed_query, is_valid = InputHandler.process_input(
447
  query,
448
  [msg for msg in self._conversation_history if isinstance(msg, (HumanMessage, AIMessage))]
449
  )
450
-
451
  if not is_valid or not processed_query:
452
  chain_logger.warning(f"Invalid input received: '{query}'")
453
- return "I didn't quite understand that. Could you please rephrase your question?"
454
-
 
 
 
455
  # Log if input was interpreted
456
  if processed_query != query:
457
  chain_logger.info(f"Interpreted input '{query}' as '{processed_query}'")
458
-
459
- # Step 2: Lock language on first user message
460
- if LOCK_LANGUAGE_AFTER_FIRST_MESSAGE and self._user_language is None:
461
- self._user_language = detect_language(processed_query)
462
- self._conversation_state['user_language'] = self._user_language
463
- self._language = self._user_language
464
- chain_logger.info(f"Locked conversation language to '{self._user_language}'")
465
-
466
- # Use locked language or current language
467
- response_language = self._user_language or self._language
468
-
469
- # Step 3: Check scope before querying agent
470
  scope_type = ScopeGuardian.check_scope(processed_query, response_language)
471
-
472
  if scope_type != 'on_topic':
473
  chain_logger.info(f"Out-of-scope query detected: {scope_type}")
474
  self._scope_violation_count += 1
475
-
476
  # Check if should escalate
477
  should_escalate, escalation_type = ScopeGuardian.should_escalate(
478
  processed_query,
479
  scope_type,
480
  self._scope_violation_count
481
  )
482
-
483
  if should_escalate:
484
  redirect_msg = ScopeGuardian.get_escalation_message(
485
  escalation_type,
@@ -490,71 +496,116 @@ class ExecutiveAgentChain:
490
  scope_type,
491
  response_language
492
  )
493
-
494
  # Add to history
495
  self._conversation_history.append(HumanMessage(processed_query))
496
  self._conversation_history.append(AIMessage(redirect_msg))
497
-
498
- return redirect_msg
499
-
 
 
 
500
  # Reset violation count on valid topic
501
  self._scope_violation_count = 0
502
-
503
- # Step 4: Build messages with locked language
504
  self._conversation_history.append(HumanMessage(processed_query))
505
-
506
- # Add language instruction (use locked language)
507
- language_instruction = SystemMessage(
508
- f"Respond in {get_language_name(response_language)} language."
509
- )
510
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  # Step 5: Query agent
512
- response = self._query(
513
  agent=self._agents['lead'],
514
- messages=self._conversation_history + [language_instruction],
515
  )
516
-
 
517
  # Step 6: Format response (remove tables, chunk if needed)
518
  if ENABLE_RESPONSE_CHUNKING:
519
  formatted_response = ResponseFormatter.format_response(
520
- response,
521
  agent_type='lead',
522
  enable_chunking=True
523
  )
524
  else:
525
- formatted_response = ResponseFormatter.remove_tables(response)
526
-
527
  # Clean up response
528
  formatted_response = ResponseFormatter.clean_response(formatted_response)
529
-
 
 
 
 
 
 
 
 
 
 
 
530
  # Add to history
531
  self._conversation_history.append(AIMessage(formatted_response))
532
-
533
- # Step 7: Update conversation state and log profile if tracking is enabled
534
  if TRACK_USER_PROFILE:
535
  self._update_conversation_state(processed_query, formatted_response)
536
  # Log profile every 5 messages or when program is suggested
537
  message_count = len([m for m in self._conversation_history if isinstance(m, HumanMessage)])
538
- if (message_count % 5 == 0 or
539
- self._conversation_state.get('suggested_program')):
540
  self._log_user_profile()
541
-
542
- return formatted_response
543
 
 
 
 
 
 
544
 
545
- def _query(self, agent, messages: list, thread_id: str = None) -> str:
546
  try:
547
  config = self._config.copy()
548
  config['configurable']['thread_id'] = thread_id or 0
549
-
550
  result: AIMessage = agent.invoke(
551
  {"messages": messages},
552
  config=config,
553
  context=AgentContext(agent_name=agent.name),
554
  )
555
- response = result['messages'][-1]
556
- return response.text
 
 
 
 
 
 
 
557
  except Exception as e:
558
  error_msg = e.body['message'] if hasattr(e, 'body') else str(e)
559
  chain_logger.error(f"Failed to invoke the agent: {error_msg}")
560
- return "I'm sorry, I cannot provide a helpful response right now. Please contact tech support or try again later."
 
 
 
 
 
1
+ from langchain_core.runnables import RunnableConfig
2
+ from langsmith import traceable
3
  from langchain.tools import tool
4
  from langchain.agents import create_agent
5
  from langchain_core.messages import (
6
+ HumanMessage,
7
+ AIMessage,
8
+ SystemMessage,
9
  )
10
  from langchain.agents.middleware import ModelFallbackMiddleware
11
+ from langchain.agents.structured_output import ProviderStrategy
12
 
13
  import uuid
14
  import json
15
  import os
16
  import re
17
+ import random
18
  from datetime import datetime
19
 
20
  from src.database.weavservice import WeaviateService
21
 
22
  from src.rag.utilclasses import *
23
+ from src.const.agent_response_constants import *
24
  from src.rag.middleware import AgentChainMiddleware as chainmdw
25
  from src.rag.prompts import PromptConfigurator as promptconf
26
  from src.rag.models import ModelConfigurator as modelconf
27
  from src.rag.input_handler import InputHandler
28
  from src.rag.response_formatter import ResponseFormatter
29
  from src.rag.scope_guardian import ScopeGuardian
30
+ from src.rag.quality_score_handler import QualityEvaluationResult, QualityScoreHandler
31
+ from src.rag.language_detection import LanguageDetector
32
 
33
+ from src.utils.logging import get_logger
34
+ from src.utils.lang import get_language_name
35
  from config import (
36
  TOP_K_RETRIEVAL,
 
37
  TRACK_USER_PROFILE,
38
+ ENABLE_RESPONSE_CHUNKING,
39
+ ENABLE_EVALUATE_RESPONSE_QUALITY,
40
+ MAX_CONVERSATION_TURNS,
41
  )
42
 
43
  chain_logger = get_logger('agent_chain')
44
 
45
+
46
  class ExecutiveAgentChain:
47
  def __init__(self, language: str = 'en') -> None:
48
+ self._initial_language = language
49
+ self._stored_language = language
 
50
  self._dbservice = WeaviateService()
51
  self._agents, self._config = self._init_agents()
52
  self._conversation_history = []
53
 
54
+ # AI-middlewares
55
+ if ENABLE_EVALUATE_RESPONSE_QUALITY:
56
+ self._quality_handler = QualityScoreHandler()
57
+ self._language_detector = LanguageDetector()
58
+
59
  # Generate unique user ID for this session
60
  self._user_id = str(uuid.uuid4())
61
+
62
  # Initialize conversation state with user profile tracking
63
  self._conversation_state: ConversationState = {
64
  'user_id': self._user_id,
 
75
  'topics_discussed': [],
76
  'preferences_known': False
77
  }
78
+
79
  # Track scope violations for escalation
80
  self._scope_violation_count = 0
 
 
81
 
82
+ chain_logger.info(f"Initialized new Agent Chain for language '{language}' with user_id: {self._user_id}")
83
 
84
  def _retrieve_context(self, query: str, language: str = None):
85
  """
 
89
  query: Keywords depicting information you want to retrieve in the primary language.
90
  language: Optional parameter (either 'en' for English language or 'de' for German language). This parameter selects the language of the database to query from. The input query must be written in the same language as the selected language. Use this parameter only if there's not enough information in your main language.
91
  """
92
+ lang = language if language in ['en', 'de'] else self._initial_language
93
  try:
94
  response, _ = self._dbservice.query(
95
+ query=query,
96
+ lang=lang,
97
  limit=TOP_K_RETRIEVAL,
98
  )
99
  serialized = '\n\n'.join([doc.properties.get('body', '') for doc in response.objects])
100
  return serialized
101
  except Exception as e:
102
  raise e
 
103
 
104
  def _call_emba_agent(self, query: str) -> str:
105
  """
 
109
  query: Query to the EMBA support agent. Provide collected user data in the query if possible.
110
  """
111
  try:
112
+ structured_response = self._query(
113
+ agent=self._agents['emba'],
114
  messages=[HumanMessage(query)],
115
  thread_id=f"emba_{hash(query)}",
116
  )
117
+ return structured_response.response
118
  except Exception as e:
119
  chain_logger.error(f"EMBA Agent error: {e}")
120
  raise RuntimeError("Unable to retrieve EMBA information at this time.")
121
 
 
122
  def _call_iemba_agent(self, query: str) -> str:
123
  """
124
  Invokes the IEMBA support agent to retrieve more detailed information about the IEMBA program.
 
127
  query: Query to the IEMBA support agent. Provide collected user data in the query if possible.
128
  """
129
  try:
130
+ structured_response = self._query(
131
+ agent=self._agents['iemba'],
132
  messages=[HumanMessage(query)],
133
  thread_id=f"emba_{hash(query)}",
134
  )
135
+ return structured_response.response
136
  except Exception as e:
137
  chain_logger.error(f"IEMBA Agent error: {e}")
138
  raise RuntimeError("Unable to retrieve IEMBA information at this time.")
139
 
 
140
  def _call_embax_agent(self, query: str) -> str:
141
  """
142
  Invokes the emba X support agent to retrieve more detailed information about the emba X program.
 
145
  query: Query to the emba X support agent. Provide collected user data in the query if possible.
146
  """
147
  try:
148
+ structured_response = self._query(
149
+ agent=self._agents['embax'],
150
  messages=[HumanMessage(query)],
151
  thread_id=f"emba_{hash(query)}",
152
  )
153
+ return structured_response.response
154
  except Exception as e:
155
  chain_logger.error(f"emba X Agent error: {e}")
156
  raise RuntimeError("Unable to retrieve emba X information at this time.")
157
 
 
158
  def _init_agents(self):
159
  config: RunnableConfig = {
160
  'configurable': {'thread_id': 0}
 
194
  model=modelconf.get_main_agent_model(),
195
  tools=tools_agent_calling,
196
  state_schema=LeadInformationState,
197
+ system_prompt=promptconf.get_configured_agent_prompt('lead', language=self._initial_language),
198
  middleware=[
199
  chainmdw.get_tool_wrapper(),
200
  chainmdw.get_model_wrapper(),
201
  fallback_middleware,
202
  ],
203
  context_schema=AgentContext,
204
+ response_format=ProviderStrategy(
205
+ StructuredAgentResponse
206
+ ),
207
+ ),
208
  }
209
  for agent in ['emba', 'iemba', 'embax']:
210
+ agents[agent] = create_agent(
211
  name=f"{agent.upper()} Agent",
212
  model=modelconf.get_subagent_model(),
213
  tools=[tool_retrieve_context],
214
  state_schema=LeadInformationState,
215
+ system_prompt=promptconf.get_configured_agent_prompt(agent, language=self._initial_language),
216
  middleware=[
217
  fallback_middleware,
218
  chainmdw.get_tool_wrapper(),
 
221
  context_schema=AgentContext,
222
  )
223
  return agents, config
224
+
225
  def _extract_experience_years(self, conversation: str) -> int | None:
226
  """Extract years of professional experience from conversation text."""
227
  # Look for patterns like "10 years", "5 years experience", etc.
 
254
  """Extract professional field/industry from conversation text."""
255
  # Common fields mentioned in executive education
256
  fields = [
257
+ 'finance', 'banking', 'technology', 'tech', 'IT', 'healthcare',
258
  'consulting', 'manufacturing', 'retail', 'marketing', 'sales',
259
  'engineering', 'pharma', 'telecommunications', 'energy',
260
  'Finanzwesen', 'Technologie', 'Gesundheitswesen', 'Beratung' # German
 
275
  'Strategie', 'Innovation', 'Führung', 'Digitalisierung' # German
276
  ]
277
  conversation_lower = conversation.lower()
278
+ found_interests = [interest for interest in interests
279
+ if interest.lower() in conversation_lower]
280
  return ', '.join(found_interests) if found_interests else None
281
 
282
  def _extract_name(self, conversation: str) -> str | None:
 
312
  def _determine_suggested_program(self) -> str | None:
313
  """Determine recommended program based on user profile."""
314
  state = self._conversation_state
315
+
316
  # If program interest was explicitly mentioned
317
  if state['program_interest']:
318
  return state['program_interest'][0]
319
+
320
  # Make recommendation based on profile
321
  experience = state.get('experience_years', 0) or 0
322
  leadership = state.get('leadership_years', 0) or 0
323
+
324
  # EMBA: 5+ years experience, 2+ years leadership
325
  if experience >= 5 and leadership >= 2:
326
  return 'EMBA'
 
328
  elif experience >= 3:
329
  return 'IEMBA'
330
  # EMBA X: Digital/Innovation focus
331
+ elif state.get('interest') and any(kw in state.get('interest', '').lower()
332
  for kw in ['digital', 'innovation', 'technology']):
333
  return 'EMBA X'
334
+
335
  return None
336
 
337
  def _update_conversation_state(self, user_query: str, agent_response: str) -> None:
338
  """Update conversation state by extracting information from the conversation."""
339
  if not TRACK_USER_PROFILE:
340
  return
341
+
342
  # Combine query and response for analysis
343
  conversation_text = f"{user_query} {agent_response}"
344
+
345
  # Extract profile information
346
  if not self._conversation_state.get('experience_years'):
347
  exp_years = self._extract_experience_years(conversation_text)
348
  if exp_years:
349
  self._conversation_state['experience_years'] = exp_years
350
  chain_logger.info(f"Extracted experience years: {exp_years}")
351
+
352
  if not self._conversation_state.get('leadership_years'):
353
  lead_years = self._extract_leadership_years(conversation_text)
354
  if lead_years:
355
  self._conversation_state['leadership_years'] = lead_years
356
  chain_logger.info(f"Extracted leadership years: {lead_years}")
357
+
358
  if not self._conversation_state.get('field'):
359
  field = self._extract_field(conversation_text)
360
  if field:
361
  self._conversation_state['field'] = field
362
  chain_logger.info(f"Extracted field: {field}")
363
+
364
  if not self._conversation_state.get('interest'):
365
  interest = self._extract_interest(conversation_text)
366
  if interest:
367
  self._conversation_state['interest'] = interest
368
  chain_logger.info(f"Extracted interest: {interest}")
369
+
370
  # Extract name
371
  if not self._conversation_state.get('user_name'):
372
  name = self._extract_name(conversation_text)
373
  if name:
374
  self._conversation_state['user_name'] = name
375
  chain_logger.info(f"Extracted name: {name}")
376
+
377
  # Detect handover request
378
  if self._detect_handover_request(conversation_text):
379
  self._conversation_state['handover_requested'] = True
380
  chain_logger.info("Handover request detected")
381
+
382
  # Check for program mentions
383
  programs = ['EMBA', 'IEMBA', 'EMBA X']
384
  for program in programs:
385
  if program.lower() in conversation_text.lower():
386
  if program not in self._conversation_state['program_interest']:
387
  self._conversation_state['program_interest'].append(program)
388
+
389
  # Update suggested program
390
  suggested = self._determine_suggested_program()
391
  if suggested and not self._conversation_state.get('suggested_program'):
 
396
  """Log user profile to JSON file."""
397
  if not TRACK_USER_PROFILE:
398
  return
399
+
400
  try:
401
  # Create logs directory if it doesn't exist
402
  log_dir = os.path.join('logs', 'user_profiles')
403
  os.makedirs(log_dir, exist_ok=True)
404
+
405
  # Create profile data
406
  profile_data = {
407
  'user_id': self._conversation_state['user_id'],
 
416
  'user_language': self._conversation_state.get('user_language'),
417
  'program_interest': self._conversation_state.get('program_interest', []),
418
  }
419
+
420
  # Log file path with timestamp
421
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
422
  log_file = os.path.join(log_dir, f'profile_{self._user_id}_{timestamp}.json')
423
+
424
  # Write to file
425
  with open(log_file, 'w', encoding='utf-8') as f:
426
  json.dump(profile_data, f, indent=2, ensure_ascii=False)
427
+
428
  chain_logger.info(f"User profile logged to {log_file}")
429
+
430
  except Exception as e:
431
  chain_logger.error(f"Failed to log user profile: {e}")
432
 
433
  def generate_greeting(self) -> str:
434
+ greeting_message = random.choice(GREETING_MESSAGES[self._stored_language])
435
+ return greeting_message
 
 
 
 
 
 
 
 
 
436
 
437
+ @traceable
438
+ def query(self, query: str) -> LeadAgentQueryResponse:
439
  """
440
  Process user query with input handling, scope checking, and response formatting.
441
 
 
445
  Returns:
446
  Formatted response
447
  """
448
+ # Select fallback language if language was changed by previous query
449
+ response_language = self._stored_language
450
+
451
+ if len(self._conversation_history) >= MAX_CONVERSATION_TURNS * 2:
452
+ return LeadAgentQueryResponse(
453
+ response = CONVERSATION_END_MESSAGE[response_language],
454
+ language = response_language,
455
+ max_turns_reached = True,
456
+ )
457
+
458
  # Step 1: Process input (handle numeric inputs, validation)
459
  processed_query, is_valid = InputHandler.process_input(
460
  query,
461
  [msg for msg in self._conversation_history if isinstance(msg, (HumanMessage, AIMessage))]
462
  )
463
+
464
  if not is_valid or not processed_query:
465
  chain_logger.warning(f"Invalid input received: '{query}'")
466
+ return LeadAgentQueryResponse(
467
+ response=NOT_VALID_QUERY_MESSAGE[self._stored_language],
468
+ language=response_language,
469
+ )
470
+
471
  # Log if input was interpreted
472
  if processed_query != query:
473
  chain_logger.info(f"Interpreted input '{query}' as '{processed_query}'")
474
+
475
+ # Step 2: Check scope before querying agent
 
 
 
 
 
 
 
 
 
 
476
  scope_type = ScopeGuardian.check_scope(processed_query, response_language)
477
+
478
  if scope_type != 'on_topic':
479
  chain_logger.info(f"Out-of-scope query detected: {scope_type}")
480
  self._scope_violation_count += 1
481
+
482
  # Check if should escalate
483
  should_escalate, escalation_type = ScopeGuardian.should_escalate(
484
  processed_query,
485
  scope_type,
486
  self._scope_violation_count
487
  )
488
+
489
  if should_escalate:
490
  redirect_msg = ScopeGuardian.get_escalation_message(
491
  escalation_type,
 
496
  scope_type,
497
  response_language
498
  )
499
+
500
  # Add to history
501
  self._conversation_history.append(HumanMessage(processed_query))
502
  self._conversation_history.append(AIMessage(redirect_msg))
503
+
504
+ return LeadAgentQueryResponse(
505
+ response=redirect_msg,
506
+ language=response_language,
507
+ )
508
+
509
  # Reset violation count on valid topic
510
  self._scope_violation_count = 0
511
+
512
+ # Append user query to conversation history before querying
513
  self._conversation_history.append(HumanMessage(processed_query))
514
+
515
+ # Step 3: Detect query language using the language detector
516
+ detected_language = self._language_detector.detect_language(processed_query)
517
+ chain_logger.info(f"Detected query language: {detected_language}")
518
+ self._conversation_state['user_language'] = detected_language
519
+
520
+ # Store the query language if it's valid; return fallback message otherwise
521
+ if detected_language in ['de', 'en']:
522
+ self._stored_language = detected_language
523
+ response_language = detected_language
524
+ else:
525
+ chain_logger.info("User query is not in a valid language, switching to fallback message...")
526
+ fallback_message = LANGUAGE_FALLBACK_MESSAGE[response_language]
527
+ self._conversation_history.append(AIMessage(fallback_message))
528
+
529
+ return LeadAgentQueryResponse(
530
+ response=fallback_message,
531
+ language=response_language,
532
+ )
533
+
534
+ # Step 4: Build messages with locked language
535
+ language_instruction = SystemMessage(f"Respond in {get_language_name(response_language)} language.")
536
+
537
  # Step 5: Query agent
538
+ structured_response = self._query(
539
  agent=self._agents['lead'],
540
+ messages=self._conversation_history + [language_instruction],
541
  )
542
+ agent_response = structured_response.response
543
+
544
  # Step 6: Format response (remove tables, chunk if needed)
545
  if ENABLE_RESPONSE_CHUNKING:
546
  formatted_response = ResponseFormatter.format_response(
547
+ agent_response,
548
  agent_type='lead',
549
  enable_chunking=True
550
  )
551
  else:
552
+ formatted_response = ResponseFormatter.remove_tables(agent_response)
553
+
554
  # Clean up response
555
  formatted_response = ResponseFormatter.clean_response(formatted_response)
556
+
557
+ # Step 7: Language fallback mechanisms and response quality evaluation
558
+ confidence_fallback = False
559
+ if ENABLE_EVALUATE_RESPONSE_QUALITY:
560
+ quality_evaluation: QualityEvaluationResult = self._quality_handler. \
561
+ evaluate_response_quality(query, formatted_response)
562
+ chain_logger.info(f"Lead agent response recieved quality score of {quality_evaluation.overall_score:1.2f}")
563
+
564
+ if quality_evaluation.overall_score < 0.3:
565
+ confidence_fallback = True
566
+ formatted_response = CONFIDENCE_FALLBACK_MESSAGE[response_language]
567
+
568
  # Add to history
569
  self._conversation_history.append(AIMessage(formatted_response))
570
+
571
+ # Step 8: Update conversation state and log profile if tracking is enabled
572
  if TRACK_USER_PROFILE:
573
  self._update_conversation_state(processed_query, formatted_response)
574
  # Log profile every 5 messages or when program is suggested
575
  message_count = len([m for m in self._conversation_history if isinstance(m, HumanMessage)])
576
+ if (message_count % 5 == 0 or self._conversation_state.get('suggested_program')):
 
577
  self._log_user_profile()
 
 
578
 
579
+ return LeadAgentQueryResponse(
580
+ response = formatted_response,
581
+ language = response_language,
582
+ confidence_fallback = confidence_fallback,
583
+ )
584
 
585
+ def _query(self, agent, messages: list, thread_id: str = None) -> StructuredAgentResponse:
586
  try:
587
  config = self._config.copy()
588
  config['configurable']['thread_id'] = thread_id or 0
589
+
590
  result: AIMessage = agent.invoke(
591
  {"messages": messages},
592
  config=config,
593
  context=AgentContext(agent_name=agent.name),
594
  )
595
+ response = result.get(
596
+ 'structured_response',
597
+ StructuredAgentResponse(
598
+ response=result['messages'][-1].text,
599
+ confidence_score=0.5,
600
+ language=self._initial_language,
601
+ )
602
+ )
603
+ return response
604
  except Exception as e:
605
  error_msg = e.body['message'] if hasattr(e, 'body') else str(e)
606
  chain_logger.error(f"Failed to invoke the agent: {error_msg}")
607
+ return StructuredAgentResponse(
608
+ response=QUERY_EXCEPTION_MESSAGE[self._stored_language],
609
+ confidence_score=0.0,
610
+ language=self._initial_language,
611
+ )
src/rag/language_detection.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from langchain_core.messages import HumanMessage
3
+ from src.rag.models import ModelConfigurator as modconf
4
+ from src.rag.prompts import PromptConfigurator as promptconf
5
+
6
+ from src.utils.logging import get_logger
7
+
8
+ logger = get_logger('lang_detector')
9
+
10
+ class LanguageDetectionResult(BaseModel):
11
+ language_code: str = Field(description="ISO language code (e.g., en, de, fa, ru) of the language in which the message is written")
12
+
13
+ class LanguageDetector:
14
+ def __init__(self) -> None:
15
+ self._model = modconf.get_language_detector_model()
16
+ self._model = self._model.with_structured_output(LanguageDetectionResult)
17
+
18
+ def detect_language(self, query: str) -> str:
19
+ prompt = promptconf.get_language_detector_prompt(query)
20
+ messages = [HumanMessage(prompt)]
21
+
22
+ try:
23
+ result = self._model.invoke(messages)
24
+ return result.language_code
25
+ except Exception as e:
26
+ logger.error(f"Failed to detect language: {e}")
27
+ return ""
28
+
src/rag/models.py CHANGED
@@ -10,7 +10,50 @@ class ModelConfigurator:
10
  _subagent_model_instance: BaseChatModel = None
11
  _fallback_models_instances: list[BaseChatModel] = None
12
  _summarization_model_instance: BaseChatModel = None
 
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  @classmethod
16
  def get_summarization_model(cls) -> BaseChatModel:
 
10
  _subagent_model_instance: BaseChatModel = None
11
  _fallback_models_instances: list[BaseChatModel] = None
12
  _summarization_model_instance: BaseChatModel = None
13
+ _confidence_scoring_model_instance: BaseChatModel = None
14
+ _language_detector_model_instance: BaseChatModel = None
15
 
16
+ @classmethod
17
+ def get_language_detector_model(cls) -> BaseChatModel:
18
+ if cls._confidence_scoring_model_instance:
19
+ return cls._confidence_scoring_model_instance
20
+ try:
21
+ from langchain_openai import ChatOpenAI
22
+ cls._language_detector_model_instance = ChatOpenAI(
23
+ model='gpt-4o-mini',
24
+ openai_api_key=llmconf.get_api_key(),
25
+ max_tokens=3072,
26
+ temperature=0.00,
27
+ timeout=60,
28
+ request_timeout=60,
29
+ )
30
+ logger.info(f"Initialized language detection model")
31
+ return cls._language_detector_model_instance
32
+ except Exception as e:
33
+ logger.error(f"Failed to initialize language detection model: {e}")
34
+ raise e
35
+
36
+ @classmethod
37
+ def get_confidence_scoring_model(cls) -> BaseChatModel:
38
+ if cls._confidence_scoring_model_instance:
39
+ return cls._confidence_scoring_model_instance
40
+
41
+ try:
42
+ from langchain_openai import ChatOpenAI
43
+ cls._confidence_scoring_model_instance = ChatOpenAI(
44
+ model='gpt-4o-mini',
45
+ openai_api_key=llmconf.get_api_key(),
46
+ max_tokens=3072,
47
+ temperature=0.00,
48
+ timeout=60,
49
+ request_timeout=60,
50
+ )
51
+ logger.info(f"Initialized confidence scoring model")
52
+ return cls._confidence_scoring_model_instance
53
+ except Exception as e:
54
+ logger.error(f"Failed to initialize confidence scoring model: {e}")
55
+ raise e
56
+
57
 
58
  @classmethod
59
  def get_summarization_model(cls) -> BaseChatModel:
src/rag/prompts.py CHANGED
@@ -73,6 +73,27 @@ Keep to 100 words max."""
73
 
74
  _SUMMARY_PREFIX_PROMPT = "Conversation Summary:"
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @classmethod
77
  def get_summarization_prompt(cls):
78
  return cls._SUMMARIZATION_PROMPT
@@ -101,3 +122,7 @@ Keep to 100 words max."""
101
  program_name=agent.upper(),
102
  selected_language=selected_language,
103
  )
 
 
 
 
 
73
 
74
  _SUMMARY_PREFIX_PROMPT = "Conversation Summary:"
75
 
76
+ _QUALITY_SCORING_PROMPT = """You are performing a quick evaluation of an AI response from an Executive Education Advisor agent for HSG EMBA, IEMBA and emba X programs. Rate the response on a scale 0.0-1.0 on these categories: format adherence, context awareness, pricing adherence, scope compliance and general rules. Deduct points for violations of the agent's guidelines.
77
+
78
+ Rules for categories:
79
+ - Format adherence: short paragraphs or bullet points, no tables, bold keywords, maximum 100 words.
80
+ - Content awareness: focuses on programs listed in user query, single numbers in user query interpreted as years of experience.
81
+ - Pricing adherence: Prices in range CHF 75'000 - 110'000, mentions included services, mentions Early Bird discount if possible, does not provide detailed financial planning, redirects to admissions team for detailed information.
82
+ - Scope compliance: redirects to MBA if user query is off-topic, discusses only program details and admissions process, suggests contacting admissions team if possible.
83
+ - General rules: no competitive MBA programs mentioned, no admission predictions, no marketing language or undefined claims; if Agent is uncertain, it should recommend contacting the admissions team.
84
+
85
+ User query: {query}
86
+ AI response: {response}"""
87
+
88
+ _LANGUAGE_DETECTOR_PROMPT = """Detect the language the user is writing in or explicitly requests to speak in, and return its ISO language code (e.g., en, de, fa, ru) in the language field.
89
+
90
+ User query: {query}
91
+ """
92
+
93
+ @classmethod
94
+ def get_language_detector_prompt(cls, query):
95
+ return cls._LANGUAGE_DETECTOR_PROMPT.format(query=query)
96
+
97
  @classmethod
98
  def get_summarization_prompt(cls):
99
  return cls._SUMMARIZATION_PROMPT
 
122
  program_name=agent.upper(),
123
  selected_language=selected_language,
124
  )
125
+
126
+ @classmethod
127
+ def get_quality_scoring_prompt(cls, query: str, response: str) -> str:
128
+ return cls._QUALITY_SCORING_PROMPT.format(query=query, response=response)
src/rag/quality_score_handler.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from langchain_core.messages import HumanMessage
3
+ from langsmith import Client
4
+ from src.rag.models import ModelConfigurator as modconf
5
+ from src.rag.prompts import PromptConfigurator as promptconf
6
+
7
+ from src.utils.logging import get_logger
8
+
9
+ from time import perf_counter
10
+
11
+ logger = get_logger('quality_score_handler')
12
+
13
+ class QualityEvaluationResult(BaseModel):
14
+ """Result of response quality evaluation."""
15
+
16
+ overall_score: float = Field(description='Overall response rating')
17
+ format_adherence_score: float = Field(description='Format adherence score')
18
+ context_awareness_score: float = Field(description='Context awareness score')
19
+ pricing_adherence_score: float = Field(description='Pricing guidelines adherence score')
20
+ scope_compliance_score: float = Field(description='Scope compliance score')
21
+ general_rules_score: float = Field(description='General rules score')
22
+ comment: str = Field(description='Brief explanation')
23
+
24
+
25
+ class QualityScoreHandler:
26
+ def __init__(self) -> None:
27
+ self._smith_client = Client()
28
+ self._model = modconf.get_confidence_scoring_model()
29
+ self._model = self._model.with_structured_output(QualityEvaluationResult)
30
+
31
+
32
+ def evaluate_response_quality(self, query: str, response: str) -> QualityEvaluationResult:
33
+ prompt = promptconf.get_quality_scoring_prompt(query, response)
34
+ messages = [HumanMessage(prompt)]
35
+
36
+ try:
37
+ time_start = perf_counter()
38
+ logger.info("Evaluating the response quality...")
39
+ evaluation: QualityEvaluationResult = self._model.invoke(messages)
40
+ time_elapsed = perf_counter() - time_start
41
+ logger.info(f"Finished confidence evaluation in {time_elapsed:1.3} sec")
42
+
43
+ evaluation.overall_score = sum([
44
+ evaluation.format_adherence_score,
45
+ evaluation.context_awareness_score,
46
+ evaluation.pricing_adherence_score,
47
+ evaluation.scope_compliance_score,
48
+ evaluation.general_rules_score,
49
+ ]) / 5.0
50
+
51
+ logger.info(f"- scoring: {evaluation.overall_score:1.2f}")
52
+ logger.info(f"- comment: {evaluation.comment}")
53
+
54
+ return evaluation
55
+ except Exception as e:
56
+ logger.error(f"Failed to evaluate the response's confidence: {e}")
57
+ return QualityEvaluationResult()
src/rag/utilclasses.py CHANGED
@@ -1,4 +1,5 @@
1
- from dataclasses import dataclass, field
 
2
  from typing_extensions import TypedDict
3
  from langchain.agents import AgentState
4
  from langchain_core.messages import AnyMessage
@@ -7,6 +8,18 @@ from langchain_core.messages import AnyMessage
7
  class AgentContext:
8
  agent_name: str
9
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  class State(TypedDict):
11
  messages: list[AnyMessage]
12
  answer: str
 
1
+ from dataclasses import dataclass
2
+ from pydantic import BaseModel, Field
3
  from typing_extensions import TypedDict
4
  from langchain.agents import AgentState
5
  from langchain_core.messages import AnyMessage
 
8
  class AgentContext:
9
  agent_name: str
10
 
11
+ @dataclass
12
+ class LeadAgentQueryResponse:
13
+ response: str
14
+ language: str
15
+ confidence_fallback: bool = False
16
+ max_turns_reached: bool = False
17
+
18
+ class StructuredAgentResponse(BaseModel):
19
+ response: str = Field(description="Main response to the query.")
20
+ confidence_score: float = Field("Value in range 0.0 to 1.0 that determines how confident the agent is in it's response based on the accumulated information.")
21
+
22
+
23
  class State(TypedDict):
24
  messages: list[AnyMessage]
25
  answer: str