nikeshn commited on
Commit
d168607
Β·
verified Β·
1 Parent(s): f6e8df5

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. 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)