Spaces:
Sleeping
Sleeping
Pygmales commited on
Commit ·
593a090
1
Parent(s): 91d726a
updated app state
Browse files- app.py +6 -134
- config.py +12 -4
- src/apps/chat/app.py +41 -33
- src/apps/chat/js.py +39 -0
- src/const/agent_response_constants.py +140 -0
- src/database/weavservice.py +5 -1
- src/rag/agent_chain.py +169 -118
- src/rag/language_detection.py +28 -0
- src/rag/models.py +43 -0
- src/rag/prompts.py +25 -0
- src/rag/quality_score_handler.py +57 -0
- src/rag/utilclasses.py +14 -1
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.
|
| 6 |
-
from src.utils.logging import
|
| 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 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 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 =
|
| 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 =
|
| 136 |
-
QUERY_TIMEOUT =
|
| 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.
|
| 4 |
-
from src.
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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(
|
| 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.
|
| 99 |
-
|
| 100 |
answers = []
|
| 101 |
try:
|
| 102 |
-
# Log user input
|
| 103 |
logger.info(f"Processing user query: {message[:100]}...")
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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=
|
| 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.
|
| 27 |
-
from src.utils.
|
| 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
|
| 40 |
-
self.
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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.
|
| 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.
|
| 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 |
-
|
| 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 |
-
|
| 424 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 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:
|
| 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
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
| 500 |
# Reset violation count on valid topic
|
| 501 |
self._scope_violation_count = 0
|
| 502 |
-
|
| 503 |
-
#
|
| 504 |
self._conversation_history.append(HumanMessage(processed_query))
|
| 505 |
-
|
| 506 |
-
#
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
# Step 5: Query agent
|
| 512 |
-
|
| 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 |
-
|
| 521 |
agent_type='lead',
|
| 522 |
enable_chunking=True
|
| 523 |
)
|
| 524 |
else:
|
| 525 |
-
formatted_response = ResponseFormatter.remove_tables(
|
| 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
|
| 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) ->
|
| 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
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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
|