Update src/api/agent.py
Browse files- src/api/agent.py +79 -0
src/api/agent.py
CHANGED
|
@@ -1750,6 +1750,54 @@ Return ONLY valid JSON:
|
|
| 1750 |
{{"corrected":"...","natural":"...","boolean":"(\"A\" OR \"B\") AND (\"C\" OR \"D\")","year_from":"","year_to":"","peer_reviewed":false,"open_access":false}}"""
|
| 1751 |
|
| 1752 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1753 |
async def _build_search_plan(question: str, model: str) -> dict:
|
| 1754 |
"""
|
| 1755 |
LLM-driven search plan β ported from app81.py and adapted for LibBee.
|
|
@@ -1796,6 +1844,37 @@ async def _build_search_plan(question: str, model: str) -> dict:
|
|
| 1796 |
}
|
| 1797 |
# ββ End ambiguity gate ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1798 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1799 |
if settings.openai_api_key or settings.anthropic_api_key:
|
| 1800 |
try:
|
| 1801 |
llm = _get_llm(model, temperature=0, max_tokens=300)
|
|
|
|
| 1750 |
{{"corrected":"...","natural":"...","boolean":"(\"A\" OR \"B\") AND (\"C\" OR \"D\")","year_from":"","year_to":"","peer_reviewed":false,"open_access":false}}"""
|
| 1751 |
|
| 1752 |
|
| 1753 |
+
async def _validate_topic(topic: str, model: str) -> Tuple[bool, str]:
|
| 1754 |
+
"""
|
| 1755 |
+
Stage 2 topic validator β runs after the regex ambiguity gate.
|
| 1756 |
+
Catches gibberish/unrecognisable words that passed the gate.
|
| 1757 |
+
|
| 1758 |
+
Returns:
|
| 1759 |
+
(True, corrected_topic) β topic is recognisable; corrected if typo
|
| 1760 |
+
(False, "") β topic is gibberish/unrecognisable
|
| 1761 |
+
|
| 1762 |
+
Uses a tiny ~30-token LLM call. Fails open (returns True) on any exception
|
| 1763 |
+
so a validator failure never blocks a legitimate search.
|
| 1764 |
+
"""
|
| 1765 |
+
settings = get_settings()
|
| 1766 |
+
if not settings.openai_api_key and not settings.anthropic_api_key:
|
| 1767 |
+
return True, topic # No LLM available β fail open
|
| 1768 |
+
try:
|
| 1769 |
+
llm = _get_llm(model, temperature=0, max_tokens=40)
|
| 1770 |
+
response = await llm.ainvoke([
|
| 1771 |
+
{"role": "system", "content": (
|
| 1772 |
+
"You are a topic validator for an academic library search system. "
|
| 1773 |
+
"Given a word or phrase, decide if it is a recognisable research topic, "
|
| 1774 |
+
"subject area, acronym, proper noun, or concept β even if misspelled.\n\n"
|
| 1775 |
+
"Rules:\n"
|
| 1776 |
+
"- If recognisable or a fixable typo: return JSON {\"valid\": true, \"corrected\": \"<corrected spelling>\"}\n"
|
| 1777 |
+
"- If gibberish, random characters, or completely unrecognisable: return JSON {\"valid\": false, \"corrected\": \"\"}\n"
|
| 1778 |
+
"- Acronyms like NLP, ML, AI, CRISPR, IoT are always valid.\n"
|
| 1779 |
+
"- Proper nouns (country names, people, organisations) are always valid.\n"
|
| 1780 |
+
"- Misspellings like 'machne lernig' β corrected: 'machine learning' β valid: true\n"
|
| 1781 |
+
"- Random strings like 'sdfmdnoc', 'xyzabc123', 'qwerty' β valid: false\n"
|
| 1782 |
+
"Return ONLY valid JSON. No explanation."
|
| 1783 |
+
)},
|
| 1784 |
+
{"role": "user", "content": f"Topic: {topic}"},
|
| 1785 |
+
])
|
| 1786 |
+
raw = response.content.strip()
|
| 1787 |
+
if raw.startswith("```"):
|
| 1788 |
+
raw = raw.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
| 1789 |
+
s, e = raw.find("{"), raw.rfind("}")
|
| 1790 |
+
if s != -1 and e > s:
|
| 1791 |
+
result = json.loads(raw[s:e + 1])
|
| 1792 |
+
is_valid = bool(result.get("valid", True))
|
| 1793 |
+
corrected = str(result.get("corrected") or topic).strip()
|
| 1794 |
+
return is_valid, corrected
|
| 1795 |
+
return True, topic # Malformed response β fail open
|
| 1796 |
+
except Exception as exc:
|
| 1797 |
+
logger.warning(f"_validate_topic failed: {exc} β failing open")
|
| 1798 |
+
return True, topic # Always fail open so validator never blocks search
|
| 1799 |
+
|
| 1800 |
+
|
| 1801 |
async def _build_search_plan(question: str, model: str) -> dict:
|
| 1802 |
"""
|
| 1803 |
LLM-driven search plan β ported from app81.py and adapted for LibBee.
|
|
|
|
| 1844 |
}
|
| 1845 |
# ββ End ambiguity gate ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1846 |
|
| 1847 |
+
# ββ Stage 2: LLM topic validator βββββββββββββββββββββββββββββββββββββββββ
|
| 1848 |
+
# Catches gibberish/unrecognisable words that passed the regex gate.
|
| 1849 |
+
# Typos are silently corrected. Gibberish triggers clarification.
|
| 1850 |
+
# Uses a tiny ~30-token LLM call β much cheaper than running full boolean
|
| 1851 |
+
# builder on garbage input.
|
| 1852 |
+
if settings.openai_api_key or settings.anthropic_api_key:
|
| 1853 |
+
_valid, _corrected_topic = await _validate_topic(_stripped, model)
|
| 1854 |
+
if not _valid:
|
| 1855 |
+
return {
|
| 1856 |
+
"corrected": raw_query,
|
| 1857 |
+
"natural": light_query,
|
| 1858 |
+
"boolean": "",
|
| 1859 |
+
"database_query": "",
|
| 1860 |
+
"year_from": "",
|
| 1861 |
+
"year_to": "",
|
| 1862 |
+
"peer_reviewed": False,
|
| 1863 |
+
"open_access": False,
|
| 1864 |
+
"clarification_needed": True,
|
| 1865 |
+
"clarification_message": (
|
| 1866 |
+
f"I couldn't recognise <strong>{_escape(_stripped)}</strong> as a research topic. "
|
| 1867 |
+
f"Could you check the spelling, or describe what you're looking for in more detail? "
|
| 1868 |
+
f"For example: <em>\"find research on machine learning\"</em> or "
|
| 1869 |
+
f"<em>\"articles on renewable energy in UAE\"</em>."
|
| 1870 |
+
),
|
| 1871 |
+
}
|
| 1872 |
+
# Silently use corrected spelling for the rest of the pipeline
|
| 1873 |
+
if _corrected_topic and _corrected_topic.lower() != _stripped.lower():
|
| 1874 |
+
logger.info(f"_validate_topic corrected: {_stripped!r} β {_corrected_topic!r}")
|
| 1875 |
+
light_query = _corrected_topic
|
| 1876 |
+
# ββ End stage 2 ββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1877 |
+
|
| 1878 |
if settings.openai_api_key or settings.anthropic_api_key:
|
| 1879 |
try:
|
| 1880 |
llm = _get_llm(model, temperature=0, max_tokens=300)
|