Hamdy005's picture
feat: replace manual web scraping with an agentic tool-selection engine
373441d
Raw
History Blame Contribute Delete
4.79 kB
import re
from loguru import logger
from src.rag.rag import get_summary_llm, get_summary_fallback_llm, _agentic_gather_web_content
from .constants import (
SUMMARIZER_PROMPT_TEMPLATE,
WEB_SUMMARIZER_PROMPT_TEMPLATE,
MAX_INPUT_CHARS,
WIKI_TOP_K_RESULTS,
WIKI_DOC_CONTENT_CHARS_MAX,
)
def summarizer_prompt():
return SUMMARIZER_PROMPT_TEMPLATE
def clean_summary(text: str) -> str:
lines = text.split('\n')
cleaned = []
for line in lines:
stripped = line.strip()
# Remove markdown table separator lines (e.g. |---|---|)
if re.match(r'^[\s\|]*[-]{2,}[\s\|]*$', stripped):
continue
# If it's a table row, just keep it but maybe clean it up a bit
if re.match(r'^\|.*\|$', stripped):
parts = [p.strip() for p in stripped.split('|')]
parts = [p for p in parts if p]
line = ' | '.join(parts)
# Note: We NO LONGER strip bold (**) here because the frontend uses it for styling
cleaned.append(line)
return '\n'.join(cleaned)
def _truncate_text(text: str, max_chars: int = MAX_INPUT_CHARS) -> str:
if len(text) <= max_chars:
return text
first_len = int(max_chars * 0.6)
last_len = max_chars - first_len
first_part = text[:first_len]
last_part = text[-last_len:]
logger.info(f"Text truncated from {len(text)} to {max_chars} chars")
return f"{first_part}\n\n...[content truncated]...\n\n{last_part}"
def summarizer(text: str) -> str:
logger.info(f"Summarizer started for text of length {len(text)}")
text = _truncate_text(text)
prompt = summarizer_prompt()
try:
llm = get_summary_llm()
chain = prompt | llm
response = chain.invoke({"input": text})
return clean_summary(response.content)
except Exception as e:
logger.warning(f"Primary summary model (gemini-3.5-flash-lite) failed: {e}. Falling back to gemini-3.1-flash-lite.")
try:
fallback_llm = get_summary_fallback_llm()
chain = prompt | fallback_llm
response = chain.invoke({"input": text})
return clean_summary(response.content)
except Exception as fallback_err:
logger.error(f"Fallback summary generation failed: {fallback_err}", exc_info=True)
raise fallback_err
def fetch_web_content(topic: str) -> str:
"""
Fetch web content for *topic* using the agentic tool-selection engine.
The router LLM decides which combination of Wikipedia, DuckDuckGo, and
ArXiv will produce the richest educational content for this topic, then
executes the selected tools in parallel and returns the combined text.
The returned text can be:
- Chunked and stored in the DB for future reuse.
- Passed directly to an LLM prompt as context.
"""
logger.info(f"fetch_web_content (agentic) started for topic: {topic}")
combined, has_wiki, has_ddg, has_arxiv = _agentic_gather_web_content(
query=topic,
is_topic=True,
existing_doc_context="",
subject_title="",
)
if not combined or not combined.strip():
logger.warning(f"No web content found for topic: {topic}. Returning placeholder.")
return f"No web content found for: {topic}"
logger.info(
f"fetch_web_content done for '{topic}': "
f"wiki={has_wiki} ddg={has_ddg} arxiv={has_arxiv} chars={len(combined)}"
)
return combined
def web_summarizer(topic: str, raw_content: str | None = None) -> str:
"""
Generate an LLM summary for *topic*.
If *raw_content* is provided (pre-fetched web text) it is used directly,
skipping the Wiki/DDG network calls. Otherwise fetch_web_content() is
called internally so this function stays usable standalone.
"""
logger.info(f"Web summarizer started for topic: {topic}")
combined = raw_content if raw_content is not None else fetch_web_content(topic)
combined = _truncate_text(combined)
try:
llm = get_summary_llm()
chain = WEB_SUMMARIZER_PROMPT_TEMPLATE | llm
response = chain.invoke({"topic": topic, "input": combined})
return clean_summary(response.content)
except Exception as e:
logger.warning(f"Primary web summarizer model (gemini-3.5-flash-lite) failed: {e}. Falling back to gemini-3.1-flash-lite.")
try:
fallback_llm = get_summary_fallback_llm()
chain = WEB_SUMMARIZER_PROMPT_TEMPLATE | fallback_llm
response = chain.invoke({"topic": topic, "input": combined})
return clean_summary(response.content)
except Exception as fallback_err:
logger.error(f"Fallback web summarizer failed: {fallback_err}", exc_info=True)
raise fallback_err