| import tiktoken
|
| from typing import Optional
|
| from fastapi import Request
|
|
|
| from qdrant_client import models
|
| from ..vectorstore import VectorStoreFactory
|
| from ..embedding import EmbeddingFactory
|
| from ..llm import LLMFactory
|
| from ...core.logger import SingletonLogger
|
| from .pdf_parser import load_pdf_content
|
| from ...config.prompts import PAPER_SUMMARY_SYSTEM_PROMPT
|
|
|
| logger = SingletonLogger().get_logger()
|
|
|
|
|
| class SummaryEngine:
|
| """SummaryEngine class for generating summaries of papers."""
|
|
|
| DEFAULT_MODEL = "groq/qwen3-32b"
|
|
|
| @staticmethod
|
| async def generate_paper_summary(
|
| arxiv_id: str = None, pdf_url: str = None, request: Optional[Request] = None,
|
| model_name: Optional[str] = None,
|
| ) -> str:
|
| try:
|
| embedding = EmbeddingFactory.build_embedding_model(request=request)
|
| vector_store = VectorStoreFactory.build_vector_store(
|
| embedding_model=embedding
|
| )
|
| full_content = ""
|
|
|
| if arxiv_id:
|
| filter_condition = models.Filter(
|
| must=[
|
| models.FieldCondition(
|
| key="metadata.paper_id",
|
| match=models.MatchValue(value=arxiv_id),
|
| )
|
| ]
|
| )
|
| retriever = vector_store.as_retriever(
|
| search_kwargs={"filter": filter_condition, "k": 200}
|
| )
|
| docs = await retriever._aget_relevant_documents(
|
| query="*", run_manager=None
|
| )
|
| sorted_docs = await SummaryEngine._sort_docs(docs)
|
| full_content = "\n\n".join([doc.page_content for doc in sorted_docs])
|
|
|
| if not full_content.strip() and pdf_url:
|
| logger.info(
|
| "No indexed content found for paper %s. Falling back to PDF URL.",
|
| arxiv_id,
|
| )
|
| full_content = await load_pdf_content(pdf_url)
|
| elif pdf_url:
|
| full_content = await load_pdf_content(pdf_url)
|
|
|
| if not full_content.strip():
|
| raise ValueError("No paper content available for summary generation")
|
|
|
| encoding = tiktoken.get_encoding("cl100k_base")
|
| tokens = encoding.encode(full_content)
|
| token_limit = 32768
|
|
|
| cumulative_summary = ""
|
|
|
| if len(tokens) > token_limit:
|
| logger.warning(
|
| f"Content for paper {arxiv_id} exceeds token limit ({len(tokens)}/{token_limit}). Generating summary in chunks."
|
| )
|
| for i in range(0, len(tokens), token_limit):
|
| chunk_tokens = tokens[i : i + token_limit]
|
| chunk_content = encoding.decode(chunk_tokens)
|
| summary = await SummaryEngine.__generate_summary(
|
| chunk_content, request, model_name
|
| )
|
| cumulative_summary += summary + "\n\n"
|
| final_summary = await SummaryEngine.__generate_summary(
|
| cumulative_summary, request, model_name
|
| )
|
| else:
|
| final_summary = await SummaryEngine.__generate_summary(
|
| full_content, request, model_name
|
| )
|
| return final_summary
|
| except Exception as e:
|
| logger.error(f"Error generating summary for paper {arxiv_id}: {str(e)}")
|
| raise e
|
|
|
| @classmethod
|
| async def __generate_summary(
|
| cls, content: str, request: Optional[Request] = None,
|
| model_name: Optional[str] = None,
|
| ) -> str:
|
| """Generate a summary of the given content."""
|
| try:
|
| llm = LLMFactory.build_llm(
|
| model_name=model_name or cls.DEFAULT_MODEL,
|
| max_tokens=4096,
|
| reasoning="hidden",
|
| request=request,
|
| )
|
| messages = [
|
| {
|
| "role": "system",
|
| "content": PAPER_SUMMARY_SYSTEM_PROMPT,
|
| },
|
| {"role": "user", "content": content},
|
| ]
|
| response = await llm.ainvoke(messages)
|
| if isinstance(response.content, str):
|
| return response.content
|
| else:
|
| if isinstance(response.content[0], str):
|
| return str(response.content[0])
|
| else:
|
| return str(response.content[0]["text"])
|
| except Exception as e:
|
| logger.error(f"Error in __generate_summary: {str(e)}")
|
| raise e
|
|
|
| @classmethod
|
| async def _sort_docs(cls, docs):
|
| """Sort documents by page number."""
|
|
|
| def extract_page_number(doc):
|
| try:
|
| return int(doc.metadata.get("page_number"))
|
| except (ValueError, TypeError):
|
| return 0
|
|
|
| return sorted(docs, key=extract_page_number)
|
|
|