smart-advisor / src /rag /services /fallback_service.py
sajaahmed5
Smart Advisor deployment
74e8a7b
Raw
History Blame Contribute Delete
4.97 kB
"""
Fallback mechanism: notify the human academic advisor via Telegram when the
system can't answer a question, and the LLM tool schema used to trigger this.
"""
import json
import time
from datetime import datetime
import threading
import requests
from requests.exceptions import ReadTimeout, ConnectionError
from src.utils.config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
def send_fallback_telegram(student: dict, question: str) -> bool:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
lines = [ # CHANGED β€” escaped brackets for Markdown
"*\\[Smart Advisor\\] Unanswered Question*",
"",
"*Student Details*",
f"Name : {student.get('name', 'Not provided')}",
f"Email : {student.get('email', 'Not provided')}",
f"Phone : {student.get('phone', 'Not provided')}",
f"Time : {timestamp}",
"",
"*Unanswered Question*",
f"{question}",
"",
"_Sent automatically by the UCAS Smart Advisor system._",
]
message = "\n".join(lines)
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
"parse_mode": "Markdown",
}
max_retries = 3 # NEW β€” retry with backoff
for attempt in range(max_retries):
try:
resp = requests.post(url, json=payload, timeout=60)
result = resp.json()
if result.get("ok"):
return True
print(f"[Telegram] API returned error: {result}")
return False # API error β€” no point retrying
except (ReadTimeout, ConnectionError) as e:
wait = 2 ** attempt # 1s, 2s, 4s
print(f"[Telegram] Attempt {attempt + 1} failed: {e}. Retrying in {wait}s...")
if attempt < max_retries - 1:
time.sleep(wait)
else:
print("[Telegram] All retries exhausted β€” question will be logged locally only.")
return False
except Exception as e:
print(f"[Telegram] Unexpected error: {e}")
return False
return False
def _send_fallback_background(student: dict, question: str) -> None:
"""
Runs in a background thread: does the actual Telegram send (with its
internal retries/backoff, now up to 60s per attempt) and, on failure,
writes to the local backup log. Never touches the request/response path,
so a slow or unresponsive Telegram API never delays the reply to the
student or blocks the processing of their next question.
"""
success = send_fallback_telegram(student, question)
if not success: # local backup log
with open("failed_questions.log", "a", encoding="utf-8") as f:
f.write(
f"\n---\nTime: {datetime.now()}\n"
f"Name: {student.get('name')}\n"
f"Email: {student.get('email')}\n"
f"Phone: {student.get('phone')}\n"
f"Question: {question}\n"
)
def record_unknown_question(question: str, name: str, email: str = None, phone: str = None) -> dict:
student = {"name": name, "email": email, "phone": phone}
threading.Thread(
target=_send_fallback_background,
args=(student, question),
daemon=True,
).start()
return {"recorded": "pending"}
# ── LLM tool schema ──────────────────────────────────────────────────────
record_unknown_question_json = {
"name": "record_unknown_question",
"description": (
"Always use this tool to record any question that couldn't be answered. "
"Also records the student's details so the advisor can follow up."
),
"parameters": {
"type": "object",
"properties": {
"question": {"type": "string", "description": "The question that couldn't be answered"},
"name": {"type": "string", "description": "The student's full name"},
"email": {"type": "string", "description": "The student's email address"},
"phone": {"type": "string", "description": "The student's phone number"},
},
"required": ["question", "name"],
"additionalProperties": False,
},
}
tools = [{"type": "function", "function": record_unknown_question_json}]
def handle_tool_calls(tool_calls) -> list[dict]:
results = []
for tc in tool_calls:
args = json.loads(tc.function.arguments)
print(f"Tool called: {tc.function.name}", flush=True)
result = record_unknown_question(**args) if tc.function.name == "record_unknown_question" else {}
results.append({"role": "tool", "content": json.dumps(result), "tool_call_id": tc.id})
return results