Update src/api/agent.py
Browse files- src/api/agent.py +65 -23
src/api/agent.py
CHANGED
|
@@ -1224,16 +1224,49 @@ def _contact_footer_for_query(question: str, context: Optional[SearchContextPayl
|
|
| 1224 |
return footer
|
| 1225 |
|
| 1226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1227 |
def _search_trace_block(question: str, context: SearchContextPayload) -> str:
|
| 1228 |
"""
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
"""
|
| 1232 |
topic = _escape(context.display_topic or context.topic)
|
| 1233 |
primo_boolean = _escape(context.primo_boolean_query or "")
|
| 1234 |
source_label = "PubMed" if context.source == "pubmed" else "KU PRIMO Library Discovery"
|
| 1235 |
|
| 1236 |
-
# Build reasoning sentence
|
| 1237 |
intent_label = {
|
| 1238 |
"search_academic": "academic literature search",
|
| 1239 |
"search_medical": "medical / clinical literature search",
|
|
@@ -1255,9 +1288,9 @@ def _search_trace_block(question: str, context: SearchContextPayload) -> str:
|
|
| 1255 |
filters_str = ", ".join(filters_parts)
|
| 1256 |
|
| 1257 |
reasoning = (
|
| 1258 |
-
f"I
|
| 1259 |
-
f"
|
| 1260 |
-
f"built an LLM-optimised boolean query with quoted phrases and synonyms, "
|
| 1261 |
f"then searched <strong>{source_label}</strong> for <strong>{filters_str}</strong>."
|
| 1262 |
)
|
| 1263 |
if context.intent == "search_medical" and context.source != "pubmed":
|
|
@@ -1278,7 +1311,8 @@ def _search_trace_block(question: str, context: SearchContextPayload) -> str:
|
|
| 1278 |
f'<code style="display:block;margin:4px 0;padding:6px 10px;background:#1a1a2e;color:#C8A951;'
|
| 1279 |
f'border-radius:7px;font-size:.78rem;word-break:break-all">{primo_boolean}</code>'
|
| 1280 |
)
|
| 1281 |
-
block += f'<strong>Reasoning:</strong> {reasoning}'
|
|
|
|
| 1282 |
block += '</div></details>'
|
| 1283 |
return block
|
| 1284 |
|
|
@@ -2113,7 +2147,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2113 |
_safe_metrics_bucket("intents", "staff_lookup")
|
| 2114 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2115 |
return AgentResponse(
|
| 2116 |
-
answer=staff_name_answer(staff_match) + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2117 |
intent="library_info",
|
| 2118 |
tools_used=["staff_name_match"],
|
| 2119 |
model_used=model,
|
|
@@ -2127,7 +2161,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2127 |
_safe_metrics_bucket("intents", "staff_lookup")
|
| 2128 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2129 |
return AgentResponse(
|
| 2130 |
-
answer=staff_role_answer(staff_role_match, question) + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2131 |
intent="library_info",
|
| 2132 |
tools_used=["staff_role_match"],
|
| 2133 |
model_used=model,
|
|
@@ -2139,7 +2173,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2139 |
if _is_greeting_menu_followup(question, history):
|
| 2140 |
follow_up_question, follow_up_suggestions = _social_follow_up()
|
| 2141 |
return AgentResponse(
|
| 2142 |
-
answer=_greeting_menu_clarify_answer() + _source_badge("libbee"),
|
| 2143 |
intent="social_greeting",
|
| 2144 |
is_follow_up=True,
|
| 2145 |
model_used=model,
|
|
@@ -2151,7 +2185,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2151 |
if _looks_library_hours_question(question):
|
| 2152 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2153 |
return AgentResponse(
|
| 2154 |
-
answer=_hours_answer() + _source_badge("ku_library", LIBRARY_HOURS_URL),
|
| 2155 |
intent="library_info",
|
| 2156 |
tools_used=["hours_hardcoded"],
|
| 2157 |
ask_librarian=False,
|
|
@@ -2164,7 +2198,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2164 |
if _looks_campus_question(question):
|
| 2165 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2166 |
return AgentResponse(
|
| 2167 |
-
answer=_campus_answer() + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2168 |
intent="library_info",
|
| 2169 |
tools_used=["campus_hardcoded"],
|
| 2170 |
ask_librarian=False,
|
|
@@ -2180,7 +2214,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2180 |
answer += _source_badge("ku_library", "https://library.ku.ac.ae/eresources")
|
| 2181 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2182 |
return AgentResponse(
|
| 2183 |
-
answer=answer,
|
| 2184 |
intent="library_info",
|
| 2185 |
tools_used=["database_recommendation"],
|
| 2186 |
model_used=model,
|
|
@@ -2200,10 +2234,13 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2200 |
if follow_up_action == "summarize_topic":
|
| 2201 |
answer, search_results, sources = await _research_snapshot(context, model)
|
| 2202 |
else:
|
|
|
|
| 2203 |
answer = (
|
|
|
|
| 2204 |
f"<strong>Research guide for {_escape(context.display_topic or context.topic)}</strong><br><br>"
|
| 2205 |
-
f"
|
| 2206 |
+ _source_badge("libbee")
|
|
|
|
| 2207 |
)
|
| 2208 |
search_results = []
|
| 2209 |
sources = []
|
|
@@ -2269,7 +2306,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2269 |
)
|
| 2270 |
follow_up_question, follow_up_suggestions = _social_follow_up()
|
| 2271 |
return AgentResponse(
|
| 2272 |
-
answer=answer + _source_badge("libbee"),
|
| 2273 |
intent="social_greeting",
|
| 2274 |
tools_used=["libbee_casual"],
|
| 2275 |
model_used=model,
|
|
@@ -2279,7 +2316,6 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2279 |
)
|
| 2280 |
|
| 2281 |
if intent == "library_info":
|
| 2282 |
-
# FIX: use get_rag_service() accessor β safe against circular import failures
|
| 2283 |
rag_results = []
|
| 2284 |
try:
|
| 2285 |
from app import get_rag_service
|
|
@@ -2297,19 +2333,22 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2297 |
answer += _source_badge("ku_library", ASK_LIBRARIAN_URL)
|
| 2298 |
sources = []
|
| 2299 |
tools_used = ["rag_no_results"]
|
|
|
|
|
|
|
| 2300 |
else:
|
| 2301 |
answer = await _rag_answer(question, rag_results, history, model)
|
| 2302 |
answer += _source_badge("rag", "https://library.ku.ac.ae/")
|
| 2303 |
sources = [
|
| 2304 |
{"title": r.get("title", "Library KB"), "url": r.get("source", "")}
|
| 2305 |
-
for r in rag_results
|
| 2306 |
-
if r.get("source")
|
| 2307 |
]
|
| 2308 |
tools_used = ["rag_hybrid_search"]
|
|
|
|
|
|
|
| 2309 |
|
| 2310 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2311 |
return AgentResponse(
|
| 2312 |
-
answer=answer,
|
| 2313 |
intent="library_info",
|
| 2314 |
tools_used=tools_used,
|
| 2315 |
sources=sources,
|
|
@@ -2346,8 +2385,10 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2346 |
|
| 2347 |
if summary_mode or deep_research_mode:
|
| 2348 |
answer, search_results, sources = await _research_snapshot(context, model)
|
| 2349 |
-
|
| 2350 |
-
|
|
|
|
|
|
|
| 2351 |
follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
|
| 2352 |
_safe_metrics_increment("search_handoffs")
|
| 2353 |
return AgentResponse(
|
|
@@ -2398,7 +2439,7 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2398 |
answer = await _web_search_answer(question, history, model) + _source_badge("web_live")
|
| 2399 |
follow_up_question, follow_up_suggestions = _general_follow_up(question)
|
| 2400 |
return AgentResponse(
|
| 2401 |
-
answer=answer,
|
| 2402 |
intent="general_recent",
|
| 2403 |
tools_used=["web_search"],
|
| 2404 |
model_used=model,
|
|
@@ -2407,10 +2448,11 @@ async def agent_query(request: Request, req: AgentRequest):
|
|
| 2407 |
follow_up_suggestions=follow_up_suggestions,
|
| 2408 |
)
|
| 2409 |
|
|
|
|
| 2410 |
answer = await _web_search_answer(question, history, model) + _source_badge("web")
|
| 2411 |
follow_up_question, follow_up_suggestions = _general_follow_up(question)
|
| 2412 |
return AgentResponse(
|
| 2413 |
-
answer=answer,
|
| 2414 |
intent="general",
|
| 2415 |
tools_used=["web_search"],
|
| 2416 |
model_used=model,
|
|
|
|
| 1224 |
return footer
|
| 1225 |
|
| 1226 |
|
| 1227 |
+
def _thought_block(intent: str, tool: str, question: str, extra: str = "") -> str:
|
| 1228 |
+
"""
|
| 1229 |
+
Lightweight collapsible reasoning block shown at the top of every non-search response.
|
| 1230 |
+
"""
|
| 1231 |
+
labels = {
|
| 1232 |
+
"library_info": ("π", "Library information lookup", "I searched the KU Library knowledge base for relevant content."),
|
| 1233 |
+
"general": ("π", "General knowledge / web search", "I answered using a web search for current information."),
|
| 1234 |
+
"general_recent": ("π΄", "Live web search", "I ran a live web search for the most up-to-date information."),
|
| 1235 |
+
"social_greeting": ("π", "Conversational response", "I responded directly as LibBee, the KU Library AI Assistant."),
|
| 1236 |
+
"social": ("π", "Conversational response", "I responded directly as LibBee, the KU Library AI Assistant."),
|
| 1237 |
+
"staff_lookup": ("π€", "Staff directory lookup", "I matched your request to the KU Library staff directory."),
|
| 1238 |
+
"hours": ("β°", "Library hours lookup", "I retrieved the current library hours from the KU Library knowledge base."),
|
| 1239 |
+
"campus": ("π", "Campus information lookup", "I retrieved KU Library campus information from the knowledge base."),
|
| 1240 |
+
"database_rec": ("ποΈ", "Database recommendation", "I matched your subject to the KU Library e-resources database."),
|
| 1241 |
+
"rag_search": ("π", "Knowledge base search", "I ran a hybrid semantic + keyword search across the KU Library knowledge base."),
|
| 1242 |
+
}
|
| 1243 |
+
icon, label, reasoning = labels.get(intent, ("π€", "AI response", "I processed your question and generated a response."))
|
| 1244 |
+
if extra:
|
| 1245 |
+
reasoning += " " + extra
|
| 1246 |
+
|
| 1247 |
+
return (
|
| 1248 |
+
f'<details style="margin:0 0 12px 0;border:1px solid #e5e7eb;border-radius:10px;'
|
| 1249 |
+
f'background:#fafafa;overflow:hidden">'
|
| 1250 |
+
f'<summary style="padding:8px 12px;cursor:pointer;font-size:.75rem;font-weight:700;'
|
| 1251 |
+
f'color:#6b7280;list-style:none;display:flex;align-items:center;gap:5px">'
|
| 1252 |
+
f'<span>{icon}</span> My approach β click to see</summary>'
|
| 1253 |
+
f'<div style="padding:4px 12px 10px 12px;font-size:.79rem;line-height:1.7;border-top:1px solid #e5e7eb">'
|
| 1254 |
+
f'<strong>Question:</strong> {_escape(question)}<br>'
|
| 1255 |
+
f'<strong>Classified as:</strong> {label}<br>'
|
| 1256 |
+
f'<strong>How I answered:</strong> {reasoning}'
|
| 1257 |
+
f'</div></details>'
|
| 1258 |
+
)
|
| 1259 |
+
|
| 1260 |
+
|
| 1261 |
def _search_trace_block(question: str, context: SearchContextPayload) -> str:
|
| 1262 |
"""
|
| 1263 |
+
Richer collapsible reasoning block for search responses.
|
| 1264 |
+
Shows topic, boolean query, and reasoning. Also hints that AI tools are at the end.
|
| 1265 |
"""
|
| 1266 |
topic = _escape(context.display_topic or context.topic)
|
| 1267 |
primo_boolean = _escape(context.primo_boolean_query or "")
|
| 1268 |
source_label = "PubMed" if context.source == "pubmed" else "KU PRIMO Library Discovery"
|
| 1269 |
|
|
|
|
| 1270 |
intent_label = {
|
| 1271 |
"search_academic": "academic literature search",
|
| 1272 |
"search_medical": "medical / clinical literature search",
|
|
|
|
| 1288 |
filters_str = ", ".join(filters_parts)
|
| 1289 |
|
| 1290 |
reasoning = (
|
| 1291 |
+
f"I classified this as a <strong>{intent_label}</strong>, "
|
| 1292 |
+
f"extracted the topic <strong>{topic}</strong>, "
|
| 1293 |
+
f"built an LLM-optimised boolean query with quoted phrases and OR synonyms, "
|
| 1294 |
f"then searched <strong>{source_label}</strong> for <strong>{filters_str}</strong>."
|
| 1295 |
)
|
| 1296 |
if context.intent == "search_medical" and context.source != "pubmed":
|
|
|
|
| 1311 |
f'<code style="display:block;margin:4px 0;padding:6px 10px;background:#1a1a2e;color:#C8A951;'
|
| 1312 |
f'border-radius:7px;font-size:.78rem;word-break:break-all">{primo_boolean}</code>'
|
| 1313 |
)
|
| 1314 |
+
block += f'<strong>Reasoning:</strong> {reasoning}<br>'
|
| 1315 |
+
block += '<span style="color:#5b21b6;font-size:.78rem">β¨ AI research tools for deeper exploration are listed at the end of this answer.</span>'
|
| 1316 |
block += '</div></details>'
|
| 1317 |
return block
|
| 1318 |
|
|
|
|
| 2147 |
_safe_metrics_bucket("intents", "staff_lookup")
|
| 2148 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2149 |
return AgentResponse(
|
| 2150 |
+
answer=_thought_block("staff_lookup", "staff_name_match", question) + staff_name_answer(staff_match) + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2151 |
intent="library_info",
|
| 2152 |
tools_used=["staff_name_match"],
|
| 2153 |
model_used=model,
|
|
|
|
| 2161 |
_safe_metrics_bucket("intents", "staff_lookup")
|
| 2162 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2163 |
return AgentResponse(
|
| 2164 |
+
answer=_thought_block("staff_lookup", "staff_role_match", question) + staff_role_answer(staff_role_match, question) + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2165 |
intent="library_info",
|
| 2166 |
tools_used=["staff_role_match"],
|
| 2167 |
model_used=model,
|
|
|
|
| 2173 |
if _is_greeting_menu_followup(question, history):
|
| 2174 |
follow_up_question, follow_up_suggestions = _social_follow_up()
|
| 2175 |
return AgentResponse(
|
| 2176 |
+
answer=_thought_block("social_greeting", "greeting_menu", question) + _greeting_menu_clarify_answer() + _source_badge("libbee"),
|
| 2177 |
intent="social_greeting",
|
| 2178 |
is_follow_up=True,
|
| 2179 |
model_used=model,
|
|
|
|
| 2185 |
if _looks_library_hours_question(question):
|
| 2186 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2187 |
return AgentResponse(
|
| 2188 |
+
answer=_thought_block("hours", "hours_hardcoded", question) + _hours_answer() + _source_badge("ku_library", LIBRARY_HOURS_URL),
|
| 2189 |
intent="library_info",
|
| 2190 |
tools_used=["hours_hardcoded"],
|
| 2191 |
ask_librarian=False,
|
|
|
|
| 2198 |
if _looks_campus_question(question):
|
| 2199 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2200 |
return AgentResponse(
|
| 2201 |
+
answer=_thought_block("campus", "campus_hardcoded", question) + _campus_answer() + _source_badge("ku_library", "https://library.ku.ac.ae/"),
|
| 2202 |
intent="library_info",
|
| 2203 |
tools_used=["campus_hardcoded"],
|
| 2204 |
ask_librarian=False,
|
|
|
|
| 2214 |
answer += _source_badge("ku_library", "https://library.ku.ac.ae/eresources")
|
| 2215 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2216 |
return AgentResponse(
|
| 2217 |
+
answer=_thought_block("database_rec", "database_recommendation", question) + answer,
|
| 2218 |
intent="library_info",
|
| 2219 |
tools_used=["database_recommendation"],
|
| 2220 |
model_used=model,
|
|
|
|
| 2234 |
if follow_up_action == "summarize_topic":
|
| 2235 |
answer, search_results, sources = await _research_snapshot(context, model)
|
| 2236 |
else:
|
| 2237 |
+
# deep_research_tools / best_databases follow-up
|
| 2238 |
answer = (
|
| 2239 |
+
f"{_search_trace_block(question, context)}"
|
| 2240 |
f"<strong>Research guide for {_escape(context.display_topic or context.topic)}</strong><br><br>"
|
| 2241 |
+
f"Here are the best AI tools and resources for a deep dive on this topic."
|
| 2242 |
+ _source_badge("libbee")
|
| 2243 |
+
+ _ai_tools_footer(context)
|
| 2244 |
)
|
| 2245 |
search_results = []
|
| 2246 |
sources = []
|
|
|
|
| 2306 |
)
|
| 2307 |
follow_up_question, follow_up_suggestions = _social_follow_up()
|
| 2308 |
return AgentResponse(
|
| 2309 |
+
answer=_thought_block("social", "libbee_casual", question) + answer + _source_badge("libbee"),
|
| 2310 |
intent="social_greeting",
|
| 2311 |
tools_used=["libbee_casual"],
|
| 2312 |
model_used=model,
|
|
|
|
| 2316 |
)
|
| 2317 |
|
| 2318 |
if intent == "library_info":
|
|
|
|
| 2319 |
rag_results = []
|
| 2320 |
try:
|
| 2321 |
from app import get_rag_service
|
|
|
|
| 2333 |
answer += _source_badge("ku_library", ASK_LIBRARIAN_URL)
|
| 2334 |
sources = []
|
| 2335 |
tools_used = ["rag_no_results"]
|
| 2336 |
+
thought = _thought_block("library_info", "rag_no_results", question,
|
| 2337 |
+
"No matching content was found β I am directing you to Ask a Librarian.")
|
| 2338 |
else:
|
| 2339 |
answer = await _rag_answer(question, rag_results, history, model)
|
| 2340 |
answer += _source_badge("rag", "https://library.ku.ac.ae/")
|
| 2341 |
sources = [
|
| 2342 |
{"title": r.get("title", "Library KB"), "url": r.get("source", "")}
|
| 2343 |
+
for r in rag_results if r.get("source")
|
|
|
|
| 2344 |
]
|
| 2345 |
tools_used = ["rag_hybrid_search"]
|
| 2346 |
+
thought = _thought_block("rag_search", "rag_hybrid_search", question,
|
| 2347 |
+
f"Hybrid FAISS + BM25 search returned {len(rag_results)} relevant chunk(s) from the KB.")
|
| 2348 |
|
| 2349 |
follow_up_question, follow_up_suggestions = _library_follow_up(question)
|
| 2350 |
return AgentResponse(
|
| 2351 |
+
answer=thought + answer,
|
| 2352 |
intent="library_info",
|
| 2353 |
tools_used=tools_used,
|
| 2354 |
sources=sources,
|
|
|
|
| 2385 |
|
| 2386 |
if summary_mode or deep_research_mode:
|
| 2387 |
answer, search_results, sources = await _research_snapshot(context, model)
|
| 2388 |
+
# _research_snapshot already includes _search_trace_block and _ai_tools_footer
|
| 2389 |
+
# Only append extra deep-research guidance if not already present
|
| 2390 |
+
if deep_research_mode and "Research guide" not in answer and "AI tools" not in answer:
|
| 2391 |
+
answer += f"<br><br>{_ai_tools_footer(context)}"
|
| 2392 |
follow_up_question, follow_up_suggestions = await _search_follow_up(context, model, summary_mode=True)
|
| 2393 |
_safe_metrics_increment("search_handoffs")
|
| 2394 |
return AgentResponse(
|
|
|
|
| 2439 |
answer = await _web_search_answer(question, history, model) + _source_badge("web_live")
|
| 2440 |
follow_up_question, follow_up_suggestions = _general_follow_up(question)
|
| 2441 |
return AgentResponse(
|
| 2442 |
+
answer=_thought_block("general_recent", "web_search", question) + answer,
|
| 2443 |
intent="general_recent",
|
| 2444 |
tools_used=["web_search"],
|
| 2445 |
model_used=model,
|
|
|
|
| 2448 |
follow_up_suggestions=follow_up_suggestions,
|
| 2449 |
)
|
| 2450 |
|
| 2451 |
+
# general fallback
|
| 2452 |
answer = await _web_search_answer(question, history, model) + _source_badge("web")
|
| 2453 |
follow_up_question, follow_up_suggestions = _general_follow_up(question)
|
| 2454 |
return AgentResponse(
|
| 2455 |
+
answer=_thought_block("general", "web_search", question) + answer,
|
| 2456 |
intent="general",
|
| 2457 |
tools_used=["web_search"],
|
| 2458 |
model_used=model,
|