Spaces:
Sleeping
Sleeping
Pygmales commited on
Commit ·
1e5fe91
1
Parent(s): 13ee704
updated project status
Browse files- src/rag/agent_chain.py +195 -20
- src/rag/models.py +1 -1
- src/rag/prompts.py +1 -1
src/rag/agent_chain.py
CHANGED
|
@@ -28,7 +28,7 @@ from src.rag.models import ModelConfigurator as modelconf
|
|
| 28 |
from src.rag.input_handler import InputHandler
|
| 29 |
from src.rag.response_formatter import ResponseFormatter
|
| 30 |
from src.rag.scope_guardian import ScopeGuardian
|
| 31 |
-
from src.rag.quality_score_handler import QualityEvaluationResult, QualityScoreHandler
|
| 32 |
from src.rag.language_detection import LanguageDetector
|
| 33 |
|
| 34 |
from src.utils.logging import get_logger
|
|
@@ -49,9 +49,10 @@ class ExecutiveAgentChain:
|
|
| 49 |
self._conversation_history = []
|
| 50 |
self._cache = Cache.get_cache()
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
|
|
|
| 55 |
self._language_detector = LanguageDetector()
|
| 56 |
|
| 57 |
# Generate unique user ID for this session
|
|
@@ -340,6 +341,160 @@ class ExecutiveAgentChain:
|
|
| 340 |
|
| 341 |
return False
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
def _is_explicit_booking_intent(self, query: str) -> bool:
|
| 344 |
"""Detect whether the user is actively asking to book or accepting a booking offer."""
|
| 345 |
query_lower = query.lower()
|
|
@@ -348,6 +503,8 @@ class ExecutiveAgentChain:
|
|
| 348 |
"schedule",
|
| 349 |
"appointment",
|
| 350 |
"consultation",
|
|
|
|
|
|
|
| 351 |
"speak with",
|
| 352 |
"talk to an advisor",
|
| 353 |
"talk to admissions",
|
|
@@ -360,6 +517,12 @@ class ExecutiveAgentChain:
|
|
| 360 |
"termin vereinbaren",
|
| 361 |
"beratungstermin",
|
| 362 |
"beratungsgespräch",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
"mit jemandem sprechen",
|
| 364 |
"mit admissions sprechen",
|
| 365 |
"mit der zulassung sprechen",
|
|
@@ -724,6 +887,11 @@ class ExecutiveAgentChain:
|
|
| 724 |
|
| 725 |
response_language = self._stored_language
|
| 726 |
explicit_booking_intent = self._is_explicit_booking_intent(preprocessed_query)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
# 1. History Update
|
| 729 |
self._conversation_history.append(HumanMessage(preprocessed_query))
|
|
@@ -752,18 +920,17 @@ class ExecutiveAgentChain:
|
|
| 752 |
|
| 753 |
formatted_response = ResponseFormatter.clean_response(formatted_response)
|
| 754 |
|
| 755 |
-
# Step 7: Language fallback mechanisms and response quality evaluation
|
| 756 |
confidence_fallback = False
|
| 757 |
-
if config.chain.EVALUATE_RESPONSE_QUALITY:
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
|
| 768 |
# Add to history
|
| 769 |
self._conversation_history.append(AIMessage(formatted_response))
|
|
@@ -777,17 +944,25 @@ class ExecutiveAgentChain:
|
|
| 777 |
self._log_user_profile()
|
| 778 |
|
| 779 |
formatted_response = ResponseFormatter.format_name_of_university(formatted_response, language=response_language)
|
| 780 |
-
|
| 781 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
|
| 783 |
-
if structured_response.appointment_requested and not
|
| 784 |
-
chain_logger.info("Suppressed booking
|
|
|
|
|
|
|
| 785 |
|
| 786 |
return LeadAgentQueryResponse(
|
| 787 |
response = formatted_response,
|
| 788 |
language = response_language,
|
| 789 |
confidence_fallback = confidence_fallback,
|
| 790 |
-
should_cache =
|
| 791 |
processed_query = preprocessed_query,
|
| 792 |
appointment_requested = appointment_requested,
|
| 793 |
show_booking_widget = show_booking_widget,
|
|
|
|
| 28 |
from src.rag.input_handler import InputHandler
|
| 29 |
from src.rag.response_formatter import ResponseFormatter
|
| 30 |
from src.rag.scope_guardian import ScopeGuardian
|
| 31 |
+
# from src.rag.quality_score_handler import QualityEvaluationResult, QualityScoreHandler
|
| 32 |
from src.rag.language_detection import LanguageDetector
|
| 33 |
|
| 34 |
from src.utils.logging import get_logger
|
|
|
|
| 49 |
self._conversation_history = []
|
| 50 |
self._cache = Cache.get_cache()
|
| 51 |
|
| 52 |
+
# Confidence scoring is intentionally disabled here because the extra
|
| 53 |
+
# model call adds latency and has not been reliable enough to justify it.
|
| 54 |
+
# if config.chain.EVALUATE_RESPONSE_QUALITY:
|
| 55 |
+
# self._quality_handler = QualityScoreHandler()
|
| 56 |
self._language_detector = LanguageDetector()
|
| 57 |
|
| 58 |
# Generate unique user ID for this session
|
|
|
|
| 341 |
|
| 342 |
return False
|
| 343 |
|
| 344 |
+
def _get_latest_ai_message_content(self, skip_latest: bool = False) -> str:
|
| 345 |
+
"""Return the latest assistant message content from conversation history."""
|
| 346 |
+
ai_messages_seen = 0
|
| 347 |
+
|
| 348 |
+
for message in reversed(self._conversation_history):
|
| 349 |
+
if not isinstance(message, AIMessage):
|
| 350 |
+
continue
|
| 351 |
+
|
| 352 |
+
ai_messages_seen += 1
|
| 353 |
+
if skip_latest and ai_messages_seen == 1:
|
| 354 |
+
continue
|
| 355 |
+
|
| 356 |
+
content = getattr(message, "content", "") or getattr(message, "text", "")
|
| 357 |
+
if isinstance(content, list):
|
| 358 |
+
return " ".join(str(part) for part in content)
|
| 359 |
+
return str(content)
|
| 360 |
+
|
| 361 |
+
return ""
|
| 362 |
+
|
| 363 |
+
def _is_booking_preference_follow_up(self, query: str) -> bool:
|
| 364 |
+
"""Detect short follow-up answers that continue an active booking flow."""
|
| 365 |
+
query_lower = query.lower().strip()
|
| 366 |
+
if not query_lower:
|
| 367 |
+
return False
|
| 368 |
+
|
| 369 |
+
preference_terms = [
|
| 370 |
+
"online",
|
| 371 |
+
"on-site",
|
| 372 |
+
"onsite",
|
| 373 |
+
"in person",
|
| 374 |
+
"in-person",
|
| 375 |
+
"st.gallen",
|
| 376 |
+
"st. gallen",
|
| 377 |
+
"morning",
|
| 378 |
+
"mornings",
|
| 379 |
+
"afternoon",
|
| 380 |
+
"afternoons",
|
| 381 |
+
"evening",
|
| 382 |
+
"beginning of the week",
|
| 383 |
+
"start of the week",
|
| 384 |
+
"end of the week",
|
| 385 |
+
"monday",
|
| 386 |
+
"tuesday",
|
| 387 |
+
"wednesday",
|
| 388 |
+
"thursday",
|
| 389 |
+
"friday",
|
| 390 |
+
"morgens",
|
| 391 |
+
"vormittag",
|
| 392 |
+
"vormittags",
|
| 393 |
+
"nachmittag",
|
| 394 |
+
"nachmittags",
|
| 395 |
+
"abends",
|
| 396 |
+
"wochenanfang",
|
| 397 |
+
"anfang der woche",
|
| 398 |
+
"ende der woche",
|
| 399 |
+
"montag",
|
| 400 |
+
"dienstag",
|
| 401 |
+
"mittwoch",
|
| 402 |
+
"donnerstag",
|
| 403 |
+
"freitag",
|
| 404 |
+
"vor ort",
|
| 405 |
+
"vor-ort",
|
| 406 |
+
"persönlich",
|
| 407 |
+
"persoenlich",
|
| 408 |
+
"hybrid",
|
| 409 |
+
]
|
| 410 |
+
|
| 411 |
+
if any(term in query_lower for term in preference_terms):
|
| 412 |
+
return True
|
| 413 |
+
|
| 414 |
+
return False
|
| 415 |
+
|
| 416 |
+
def _previous_response_requested_booking_preferences(self) -> bool:
|
| 417 |
+
"""Return True when the previous assistant turn asked clarifying booking questions."""
|
| 418 |
+
content_lower = self._get_latest_ai_message_content().lower()
|
| 419 |
+
if not content_lower:
|
| 420 |
+
return False
|
| 421 |
+
|
| 422 |
+
booking_context_terms = [
|
| 423 |
+
"appointment options",
|
| 424 |
+
"available appointments",
|
| 425 |
+
"available slots",
|
| 426 |
+
"appointment slots",
|
| 427 |
+
"online-terminoptionen",
|
| 428 |
+
"terminoptionen",
|
| 429 |
+
"verfügbare slots",
|
| 430 |
+
"verfügbare termine",
|
| 431 |
+
"beratungsgespräch",
|
| 432 |
+
"beratung",
|
| 433 |
+
]
|
| 434 |
+
clarification_terms = [
|
| 435 |
+
"do you prefer",
|
| 436 |
+
"would you prefer",
|
| 437 |
+
"which programme",
|
| 438 |
+
"which program",
|
| 439 |
+
"one short question",
|
| 440 |
+
"final question",
|
| 441 |
+
"when i know this",
|
| 442 |
+
"bitte noch kurz",
|
| 443 |
+
"eine kurze rückfrage",
|
| 444 |
+
"eine kurze letzte frage",
|
| 445 |
+
"bevorzugen sie",
|
| 446 |
+
"haben sie eine tagespräferenz",
|
| 447 |
+
"sobald ich das weiss",
|
| 448 |
+
"damit die slots besser passen",
|
| 449 |
+
]
|
| 450 |
+
|
| 451 |
+
return (
|
| 452 |
+
any(term in content_lower for term in booking_context_terms)
|
| 453 |
+
and any(term in content_lower for term in clarification_terms)
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
def _response_commits_to_showing_booking_widget(self, response: str) -> bool:
|
| 457 |
+
"""Detect when the assistant says booking options are being shown now."""
|
| 458 |
+
response_lower = response.lower()
|
| 459 |
+
|
| 460 |
+
positive_terms = [
|
| 461 |
+
"i can show you",
|
| 462 |
+
"contact details and available appointment slots are shown below",
|
| 463 |
+
"appointment options are shown below",
|
| 464 |
+
"available slots are shown below",
|
| 465 |
+
"i can now show you",
|
| 466 |
+
"ich kann ihnen nun",
|
| 467 |
+
"ich kann ihnen jetzt",
|
| 468 |
+
"unten werden ihnen",
|
| 469 |
+
"unten finden sie",
|
| 470 |
+
"unten sehen sie",
|
| 471 |
+
"terminoptionen anzeigen",
|
| 472 |
+
"verfügbaren slots",
|
| 473 |
+
"verfügbaren termine",
|
| 474 |
+
]
|
| 475 |
+
defer_terms = [
|
| 476 |
+
"if you would like",
|
| 477 |
+
"if you later wish",
|
| 478 |
+
"you can ask me",
|
| 479 |
+
"if that would be helpful",
|
| 480 |
+
"sobald ich das weiss",
|
| 481 |
+
"wenn ich das weiss",
|
| 482 |
+
"damit die slots besser passen",
|
| 483 |
+
"bitte noch kurz",
|
| 484 |
+
"eine kurze rückfrage",
|
| 485 |
+
"eine kurze letzte frage",
|
| 486 |
+
"bevorzugen sie",
|
| 487 |
+
"have you got a preference",
|
| 488 |
+
"do you prefer",
|
| 489 |
+
"would you prefer",
|
| 490 |
+
"which programme",
|
| 491 |
+
"which program",
|
| 492 |
+
]
|
| 493 |
+
|
| 494 |
+
return (
|
| 495 |
+
any(term in response_lower for term in positive_terms)
|
| 496 |
+
and not any(term in response_lower for term in defer_terms)
|
| 497 |
+
)
|
| 498 |
def _is_explicit_booking_intent(self, query: str) -> bool:
|
| 499 |
"""Detect whether the user is actively asking to book or accepting a booking offer."""
|
| 500 |
query_lower = query.lower()
|
|
|
|
| 503 |
"schedule",
|
| 504 |
"appointment",
|
| 505 |
"consultation",
|
| 506 |
+
"need a consultation",
|
| 507 |
+
"personal consultation",
|
| 508 |
"speak with",
|
| 509 |
"talk to an advisor",
|
| 510 |
"talk to admissions",
|
|
|
|
| 517 |
"termin vereinbaren",
|
| 518 |
"beratungstermin",
|
| 519 |
"beratungsgespräch",
|
| 520 |
+
"ich brauche eine beratung",
|
| 521 |
+
"ich möchte eine beratung",
|
| 522 |
+
"ich will eine beratung",
|
| 523 |
+
"beratung für",
|
| 524 |
+
"persönliche beratung",
|
| 525 |
+
"persoenliche beratung",
|
| 526 |
"mit jemandem sprechen",
|
| 527 |
"mit admissions sprechen",
|
| 528 |
"mit der zulassung sprechen",
|
|
|
|
| 887 |
|
| 888 |
response_language = self._stored_language
|
| 889 |
explicit_booking_intent = self._is_explicit_booking_intent(preprocessed_query)
|
| 890 |
+
booking_preference_follow_up = (
|
| 891 |
+
self._conversation_state.get('handover_requested') is True
|
| 892 |
+
and self._previous_response_requested_booking_preferences()
|
| 893 |
+
and self._is_booking_preference_follow_up(preprocessed_query)
|
| 894 |
+
)
|
| 895 |
|
| 896 |
# 1. History Update
|
| 897 |
self._conversation_history.append(HumanMessage(preprocessed_query))
|
|
|
|
| 920 |
|
| 921 |
formatted_response = ResponseFormatter.clean_response(formatted_response)
|
| 922 |
|
|
|
|
| 923 |
confidence_fallback = False
|
| 924 |
+
# if config.chain.EVALUATE_RESPONSE_QUALITY:
|
| 925 |
+
# quality_evaluation: QualityEvaluationResult = self._quality_handler. \
|
| 926 |
+
# evaluate_response_quality(preprocessed_query, formatted_response)
|
| 927 |
+
#
|
| 928 |
+
# chain_logger.info(f"Quality Score: {quality_evaluation.overall_score:1.2f}")
|
| 929 |
+
#
|
| 930 |
+
# if quality_evaluation.overall_score < config.chain.CONFIDENCE_THRESHOLD:
|
| 931 |
+
# confidence_fallback = True
|
| 932 |
+
# formatted_response = CONFIDENCE_FALLBACK_MESSAGE[response_language]
|
| 933 |
+
# chain_logger.info("Fallback Mechanism activated!")
|
| 934 |
|
| 935 |
# Add to history
|
| 936 |
self._conversation_history.append(AIMessage(formatted_response))
|
|
|
|
| 944 |
self._log_user_profile()
|
| 945 |
|
| 946 |
formatted_response = ResponseFormatter.format_name_of_university(formatted_response, language=response_language)
|
| 947 |
+
booking_flow_requested = explicit_booking_intent or booking_preference_follow_up
|
| 948 |
+
appointment_requested = bool(booking_flow_requested)
|
| 949 |
+
show_booking_widget = bool(
|
| 950 |
+
booking_flow_requested and (
|
| 951 |
+
structured_response.show_booking_widget
|
| 952 |
+
or self._response_commits_to_showing_booking_widget(formatted_response)
|
| 953 |
+
)
|
| 954 |
+
)
|
| 955 |
|
| 956 |
+
if structured_response.appointment_requested and not booking_flow_requested:
|
| 957 |
+
chain_logger.info("Suppressed booking state because no user-led booking intent was detected.")
|
| 958 |
+
elif booking_preference_follow_up and show_booking_widget:
|
| 959 |
+
chain_logger.info("Continuing active booking flow and showing booking widget for a preference follow-up.")
|
| 960 |
|
| 961 |
return LeadAgentQueryResponse(
|
| 962 |
response = formatted_response,
|
| 963 |
language = response_language,
|
| 964 |
confidence_fallback = confidence_fallback,
|
| 965 |
+
should_cache = False if (confidence_fallback or appointment_requested or structured_response.is_context_dependent) else True,
|
| 966 |
processed_query = preprocessed_query,
|
| 967 |
appointment_requested = appointment_requested,
|
| 968 |
show_booking_widget = show_booking_widget,
|
src/rag/models.py
CHANGED
|
@@ -76,7 +76,7 @@ class ModelConfigurator:
|
|
| 76 |
|
| 77 |
from langchain_openai import ChatOpenAI
|
| 78 |
cls._subagent_model_instance = ChatOpenAI(
|
| 79 |
-
model='gpt-5.1',
|
| 80 |
openai_api_key=config.llm.get_api_key(),
|
| 81 |
max_tokens=3072,
|
| 82 |
temperature=0.01,
|
|
|
|
| 76 |
|
| 77 |
from langchain_openai import ChatOpenAI
|
| 78 |
cls._subagent_model_instance = ChatOpenAI(
|
| 79 |
+
model='gpt-5.1-instant',
|
| 80 |
openai_api_key=config.llm.get_api_key(),
|
| 81 |
max_tokens=3072,
|
| 82 |
temperature=0.01,
|
src/rag/prompts.py
CHANGED
|
@@ -300,7 +300,7 @@ RULES:
|
|
| 300 |
- factual, static information (e.g. prices, durations, deadlines, program structure)
|
| 301 |
- general definitions or explanations
|
| 302 |
- publicly available information that does not vary by user
|
| 303 |
-
|
| 304 |
RULES:
|
| 305 |
- Answer in the user's language. NEVER leave English terms untranslated in a German response. Key German translations:
|
| 306 |
"tuition fee reduction" → "Studiengebührenreduktion", "tuition" → "Studiengebühr(en)", "included in tuition" → "in den Studiengebühren enthalten", "not included" → "nicht enthalten", "application deadline" → "Bewerbungsfrist".
|
|
|
|
| 300 |
- factual, static information (e.g. prices, durations, deadlines, program structure)
|
| 301 |
- general definitions or explanations
|
| 302 |
- publicly available information that does not vary by user
|
| 303 |
+
|
| 304 |
RULES:
|
| 305 |
- Answer in the user's language. NEVER leave English terms untranslated in a German response. Key German translations:
|
| 306 |
"tuition fee reduction" → "Studiengebührenreduktion", "tuition" → "Studiengebühr(en)", "included in tuition" → "in den Studiengebühren enthalten", "not included" → "nicht enthalten", "application deadline" → "Bewerbungsfrist".
|