nikeshn commited on
Commit
f45f29c
Β·
verified Β·
1 Parent(s): 21f160f

Update src/api/agent.py

Browse files
Files changed (1) hide show
  1. src/api/agent.py +111 -8
src/api/agent.py CHANGED
@@ -705,16 +705,117 @@ async def _extract_topic(question: str, model: str) -> str:
705
  # They are now self-contained here.
706
 
707
  def _shared_build_primo_boolean_query(topic: str) -> str:
708
- """Build a clean boolean query from topic keywords β€” no resource-type words."""
 
 
 
 
709
  clean = _strip_resource_noise(topic)
710
  if not clean:
711
  clean = topic
712
- keywords = [w for w in clean.split() if len(w) > 1]
713
- if not keywords:
714
- return clean
715
- if len(keywords) == 1:
716
- return keywords[0]
717
- return " AND ".join(keywords[:5])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
 
719
 
720
  def _shared_build_primo_discovery_url(
@@ -870,7 +971,9 @@ async def _prepare_queries(question: str, context: SearchContextPayload, model:
870
  natural = _compose_ai_tool_query(question, context, is_follow_up)
871
  natural = await _grammar_refine_query(natural, model)
872
  context.ai_tool_query = natural
873
- context.primo_boolean_query = _make_primo_boolean_query(context)
 
 
874
  return context
875
 
876
 
 
705
  # They are now self-contained here.
706
 
707
  def _shared_build_primo_boolean_query(topic: str) -> str:
708
+ """
709
+ Regex fallback for boolean query construction β€” used only when LLM is unavailable.
710
+ Keeps multi-word phrases quoted, strips stop words, ANDs remaining concepts.
711
+ Does NOT split known compound terms.
712
+ """
713
  clean = _strip_resource_noise(topic)
714
  if not clean:
715
  clean = topic
716
+ clean = _normalize_whitespace(clean)
717
+
718
+ # Treat the whole cleaned topic as a single phrase if ≀ 4 words
719
+ words = clean.split()
720
+ if len(words) <= 4:
721
+ return f'"{clean}"'
722
+
723
+ # For longer topics: split on stop words, quote resulting multi-word chunks
724
+ _BOOL_STOP = re.compile(
725
+ r"\b(of|on|in|the|a|an|and|or|for|to|with|by|from|at|is|are|was|were|"
726
+ r"be|been|have|has|had|do|does|did|will|would|could|should|may|its|"
727
+ r"this|that|these|those|about|impact|role|effect|use|analysis|review|"
728
+ r"what|how|why|when|where|which|between|within|across|among|using|"
729
+ r"based|related|towards|toward|during|after|before|over|under)\b",
730
+ re.IGNORECASE,
731
+ )
732
+ # Split on stop words, keep non-stop chunks
733
+ parts = _BOOL_STOP.split(clean)
734
+ concepts = [_normalize_whitespace(p) for p in parts if _normalize_whitespace(p) and len(_normalize_whitespace(p)) > 2]
735
+ if not concepts:
736
+ return f'"{clean}"'
737
+ if len(concepts) == 1:
738
+ return f'"{concepts[0]}"'
739
+ # Quote multi-word concepts, leave single words bare
740
+ quoted = [f'"{c}"' if ' ' in c else c for c in concepts[:4]]
741
+ return " AND ".join(quoted)
742
+
743
+
744
+ _BOOLEAN_SYSTEM = """You are an expert academic database search specialist building PRIMO/ExLibris boolean queries.
745
+
746
+ Given a research topic or question, output ONLY a valid boolean search string.
747
+
748
+ RULES:
749
+ 1. Identify 2-4 key CONCEPTS in the query.
750
+ 2. Multi-word concepts MUST be quoted: "machine learning" not machine AND learning.
751
+ 3. Use OR between synonyms of the same concept: ("machine learning" OR "deep learning" OR "neural networks")
752
+ 4. Use AND to connect different concepts: ("climate change" OR "global warming") AND ("renewable energy" OR "clean energy")
753
+ 5. NEVER split a natural phrase with AND: "global warming" stays "global warming", NOT global AND warming.
754
+ 6. NEVER include stop words (of, the, a, in, on, for, to, with, by, impact, role, effect, use, study, analysis, review) as standalone terms.
755
+ 7. For medical/clinical topics, include MeSH-style synonyms.
756
+ 8. Keep to a maximum of 3 concept groups joined by AND.
757
+ 9. Return ONLY the boolean string. No explanation, no preamble, no markdown.
758
+
759
+ EXAMPLES:
760
+ Input: global warming
761
+ Output: ("global warming" OR "climate change" OR "greenhouse effect")
762
+
763
+ Input: machine learning in healthcare
764
+ Output: ("machine learning" OR "deep learning" OR "artificial intelligence") AND ("healthcare" OR "medicine" OR "clinical")
765
+
766
+ Input: impact of social media on mental health
767
+ Output: ("social media" OR "social networks") AND ("mental health" OR "psychological wellbeing" OR "depression")
768
+
769
+ Input: structural engineering UAE
770
+ Output: ("structural engineering" OR "structural analysis") AND ("UAE" OR "United Arab Emirates" OR "Gulf")
771
+
772
+ Input: CRISPR gene editing applications
773
+ Output: ("CRISPR" OR "CRISPR-Cas9" OR "gene editing") AND ("applications" OR "therapeutic" OR "treatment")
774
+
775
+ Input: renewable energy policy
776
+ Output: ("renewable energy" OR "clean energy" OR "sustainable energy") AND ("policy" OR "regulation" OR "governance")
777
+ """
778
+
779
+
780
+ async def _llm_build_boolean_query(topic: str, model: str) -> str:
781
+ """
782
+ Use LLM to generate a high-quality PRIMO boolean query with:
783
+ - Quoted multi-word phrases
784
+ - OR synonyms within concept groups
785
+ - AND between distinct concepts
786
+ Falls back to regex version if LLM fails.
787
+ """
788
+ settings = get_settings()
789
+ if not settings.openai_api_key and not settings.anthropic_api_key:
790
+ return _shared_build_primo_boolean_query(topic)
791
+ try:
792
+ llm = _get_llm(model, temperature=0, max_tokens=120)
793
+ response = await llm.ainvoke([
794
+ {"role": "system", "content": _BOOLEAN_SYSTEM},
795
+ {"role": "user", "content": f"Input: {topic.strip()}"},
796
+ ])
797
+ result = response.content.strip()
798
+ # Sanity checks β€” must contain quotes and AND/OR or be a short phrase
799
+ if not result or len(result) > 500:
800
+ raise ValueError("Bad LLM boolean output")
801
+ # Strip any markdown the LLM snuck in
802
+ result = re.sub(r"^```[a-z]*\n?", "", result).rstrip("`").strip()
803
+ # Must have at least one quote or look like a valid query
804
+ if '"' not in result and ' AND ' not in result and ' OR ' not in result:
805
+ # Wrap bare output as a phrase
806
+ result = f'"{result}"'
807
+ logger.info(f"LLM boolean: {result!r} ← topic: {topic!r}")
808
+ return result
809
+ except Exception as e:
810
+ logger.warning(f"LLM boolean query failed: {e} β€” using regex fallback")
811
+ return _shared_build_primo_boolean_query(topic)
812
+
813
+
814
+ def _make_primo_boolean_query(context: SearchContextPayload) -> str:
815
+ """Sync wrapper β€” returns regex boolean. Async LLM version called from _prepare_queries."""
816
+ topic = _title_case_topic(context.display_topic or context.topic)
817
+ topic = _strip_resource_noise(topic) or (context.topic or "library search")
818
+ return _shared_build_primo_boolean_query(topic)
819
 
820
 
821
  def _shared_build_primo_discovery_url(
 
971
  natural = _compose_ai_tool_query(question, context, is_follow_up)
972
  natural = await _grammar_refine_query(natural, model)
973
  context.ai_tool_query = natural
974
+ # Use LLM to build a high-quality boolean query with quoted phrases and OR synonyms
975
+ topic = _strip_resource_noise(context.display_topic or context.topic) or context.topic
976
+ context.primo_boolean_query = await _llm_build_boolean_query(topic, model)
977
  return context
978
 
979