| import tiktoken
|
| from typing import Optional
|
| from fastapi import Request
|
|
|
| from langchain.agents import create_agent
|
| from pydantic import BaseModel, Field
|
| from typing import Optional, Dict, List
|
| from qdrant_client import models
|
| from langchain_core.documents import Document
|
| from ..vectorstore import VectorStoreFactory
|
| from ..embedding import EmbeddingFactory
|
| from ..llm import LLMFactory
|
| from ...core.logger import SingletonLogger
|
| from ...lib.enum import DomainEnum, EmergingTechEnum, ReproducibilityEnum
|
| from ...config.prompts import USABILITY_GENERATION_SYSTEM_PROMPT
|
| from .pdf_parser import load_pdf_content
|
|
|
| logger = SingletonLogger().get_logger()
|
|
|
|
|
| class UsabilitySchema(BaseModel):
|
| """Metrics for arXiv paper usability, applicability, and reproducibility."""
|
|
|
| domain_applicability: Dict[DomainEnum, float] = Field(
|
| ...,
|
| description="Applicability scores (0.0-1.0) per domain based on LLM analysis. Give detailed scores for each domain with dense values.",
|
| )
|
| reproducibility_score: Dict[ReproducibilityEnum, float] = Field(
|
| ...,
|
| description="Scores (0.0-1.0) for reproducibility and reapplicability based on code/data/methods availability. Provide separate scores for both Reproducible and Reapplicable aspects. Give detailed scores with dense values. E.g {'Reproducible': 0.65, 'Reapplicable': 0.72}",
|
| )
|
| new_tech_applicability: Dict[EmergingTechEnum, float] = Field(
|
| default_factory=dict,
|
| description="Scores for emerging technologies. All technologies should be included with a score of 0.0 if not applicable. Give detailed scores for each domain with dense values. E.g 0.46",
|
| )
|
| impact_score: float = Field(
|
| default=0.0,
|
| ge=0.0,
|
| description="Composite impact based on explained innovation over domains. Give detailed overall score with dense value. E.g 0.83",
|
| )
|
| impact_score_description: str = Field(
|
| default="",
|
| description="Short description on how this score explains the impact score; e.g. 'Strong fit for cost-efficient inference deployment', 'Memory efficient Agentic RL training; Domain agnostic', 'Can be used to optimize finance applications; needs several manual effort'"
|
| )
|
|
|
|
|
| class UsabilityEngine:
|
| """UsabilityEngine 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,
|
| ) -> dict:
|
| """Generate a summary of the paper's usability, applicability, and reproducibility based on its content.
|
|
|
| Args:
|
| arxiv_id (str, optional): arXiv ID of the paper. Defaults to None.
|
| pdf_url (str, optional): URL of the PDF. Defaults to None.
|
| request (Request, optional): FastAPI Request object to fetch API keys. Defaults to None.
|
|
|
| Raises:
|
| NotImplementedError: If PDF URL processing is not implemented.
|
| e: If any other error occurs.
|
|
|
| Returns:
|
| dict: A dictionary containing the usability summary with keys 'domain_applicability', 'reproducibility_score', 'new_tech_applicability', 'impact_score' and 'impact_score_description'.
|
| """
|
| try:
|
| embedding = EmbeddingFactory.build_embedding_model(request=request)
|
| vector_store = VectorStoreFactory.build_vector_store(
|
| embedding_model=embedding
|
| )
|
| llm = LLMFactory.build_llm(
|
| model_name=model_name or UsabilityEngine.DEFAULT_MODEL,
|
| max_tokens=4096,
|
| reasoning="hidden",
|
| request=request,
|
| )
|
| agent = create_agent(
|
| system_prompt=USABILITY_GENERATION_SYSTEM_PROMPT,
|
| model=llm,
|
| response_format=UsabilitySchema,
|
| )
|
| 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: List[Document] = await retriever._aget_relevant_documents(
|
| query="*", run_manager=None
|
| )
|
| sorted_docs = await UsabilityEngine._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 usability. 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 usability generation")
|
|
|
| final_usability_json = await UsabilityEngine.__generate_summary(
|
| agent, full_content
|
| )
|
|
|
| return final_usability_json
|
| except Exception as e:
|
| logger.error(
|
| f"Error generating usability summary for paper {arxiv_id}: {str(e)}"
|
| )
|
| raise e
|
|
|
| @classmethod
|
| async def __generate_summary(cls, agent, content: str) -> dict:
|
| """Generate a summary of the given content."""
|
| try:
|
| messages = [
|
| {"role": "user", "content": content},
|
| ]
|
| response = await agent.ainvoke(
|
| {
|
| "messages": messages,
|
| }
|
| )
|
| return response["structured_response"].model_dump()
|
| except Exception as e:
|
| logger.error(f"Error in __generate_summary: {str(e)}")
|
| raise e
|
|
|
| @classmethod
|
| async def _sort_docs(cls, docs: List[Document]) -> List[Document]:
|
| """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)
|
|
|