aiagents4pharma 1.40.0__py3-none-any.whl → 1.41.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +4 -0
  2. aiagents4pharma/talk2scholars/configs/tools/question_and_answer/default.yaml +44 -4
  3. aiagents4pharma/talk2scholars/tests/test_nvidia_nim_reranker.py +127 -0
  4. aiagents4pharma/talk2scholars/tests/test_pdf_answer_formatter.py +66 -0
  5. aiagents4pharma/talk2scholars/tests/test_pdf_batch_processor.py +101 -0
  6. aiagents4pharma/talk2scholars/tests/test_pdf_collection_manager.py +150 -0
  7. aiagents4pharma/talk2scholars/tests/test_pdf_document_processor.py +69 -0
  8. aiagents4pharma/talk2scholars/tests/test_pdf_generate_answer.py +75 -0
  9. aiagents4pharma/talk2scholars/tests/test_pdf_gpu_detection.py +140 -0
  10. aiagents4pharma/talk2scholars/tests/test_pdf_paper_loader.py +116 -0
  11. aiagents4pharma/talk2scholars/tests/test_pdf_rag_pipeline.py +98 -0
  12. aiagents4pharma/talk2scholars/tests/test_pdf_retrieve_chunks.py +197 -0
  13. aiagents4pharma/talk2scholars/tests/test_pdf_singleton_manager.py +156 -0
  14. aiagents4pharma/talk2scholars/tests/test_pdf_vector_normalization.py +121 -0
  15. aiagents4pharma/talk2scholars/tests/test_pdf_vector_store.py +434 -0
  16. aiagents4pharma/talk2scholars/tests/test_question_and_answer_tool.py +89 -509
  17. aiagents4pharma/talk2scholars/tests/test_tool_helper_utils.py +34 -89
  18. aiagents4pharma/talk2scholars/tools/paper_download/download_biorxiv_input.py +8 -6
  19. aiagents4pharma/talk2scholars/tools/paper_download/download_medrxiv_input.py +6 -4
  20. aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +74 -40
  21. aiagents4pharma/talk2scholars/tools/pdf/utils/__init__.py +26 -1
  22. aiagents4pharma/talk2scholars/tools/pdf/utils/answer_formatter.py +62 -0
  23. aiagents4pharma/talk2scholars/tools/pdf/utils/batch_processor.py +200 -0
  24. aiagents4pharma/talk2scholars/tools/pdf/utils/collection_manager.py +172 -0
  25. aiagents4pharma/talk2scholars/tools/pdf/utils/document_processor.py +76 -0
  26. aiagents4pharma/talk2scholars/tools/pdf/utils/generate_answer.py +14 -14
  27. aiagents4pharma/talk2scholars/tools/pdf/utils/get_vectorstore.py +63 -0
  28. aiagents4pharma/talk2scholars/tools/pdf/utils/gpu_detection.py +154 -0
  29. aiagents4pharma/talk2scholars/tools/pdf/utils/nvidia_nim_reranker.py +60 -40
  30. aiagents4pharma/talk2scholars/tools/pdf/utils/paper_loader.py +123 -0
  31. aiagents4pharma/talk2scholars/tools/pdf/utils/rag_pipeline.py +122 -0
  32. aiagents4pharma/talk2scholars/tools/pdf/utils/retrieve_chunks.py +162 -40
  33. aiagents4pharma/talk2scholars/tools/pdf/utils/singleton_manager.py +140 -0
  34. aiagents4pharma/talk2scholars/tools/pdf/utils/tool_helper.py +40 -78
  35. aiagents4pharma/talk2scholars/tools/pdf/utils/vector_normalization.py +159 -0
  36. aiagents4pharma/talk2scholars/tools/pdf/utils/vector_store.py +277 -96
  37. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +12 -9
  38. aiagents4pharma/talk2scholars/tools/s2/query_dataframe.py +0 -1
  39. aiagents4pharma/talk2scholars/tools/s2/retrieve_semantic_scholar_paper_id.py +9 -8
  40. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +5 -5
  41. {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/METADATA +27 -115
  42. {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/RECORD +45 -23
  43. aiagents4pharma/talk2scholars/tests/test_nvidia_nim_reranker_utils.py +0 -28
  44. {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/WHEEL +0 -0
  45. {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/licenses/LICENSE +0 -0
  46. {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/top_level.txt +0 -0
@@ -10,28 +10,34 @@ from aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper import QAToolHelp
10
10
 
11
11
 
12
12
  class TestQAToolHelper(unittest.TestCase):
13
- """tests for QAToolHelper routines in tool_helper.py"""
13
+ """tests for QAToolHelper routines"""
14
14
 
15
15
  def setUp(self):
16
- """set up test case"""
16
+ """setup for each test"""
17
17
  self.helper = QAToolHelper()
18
18
 
19
19
  def test_start_call_sets_config_and_call_id(self):
20
- """test start_call sets config and call_id"""
20
+ """start_call should set config and call_id"""
21
21
  cfg = SimpleNamespace(foo="bar")
22
22
  self.helper.start_call(cfg, "call123")
23
23
  self.assertIs(self.helper.config, cfg)
24
24
  self.assertEqual(self.helper.call_id, "call123")
25
25
 
26
- def test_init_vector_store_reuse(self):
27
- """test init_vector_store reuses existing instance"""
26
+ @patch("aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper.get_vectorstore")
27
+ def test_init_vector_store_reuse(self, mock_get_vectorstore):
28
+ """Mock vector store reuse test"""
28
29
  emb_model = MagicMock()
30
+ mock_instance = MagicMock()
31
+ mock_get_vectorstore.return_value = mock_instance
32
+
29
33
  first = self.helper.init_vector_store(emb_model)
30
34
  second = self.helper.init_vector_store(emb_model)
31
- self.assertIs(second, first)
35
+
36
+ self.assertIs(first, second)
37
+ self.assertIs(second, mock_instance)
32
38
 
33
39
  def test_get_state_models_and_data_success(self):
34
- """test get_state_models_and_data returns models and data"""
40
+ """get_state_models_and_data should return models and data from state"""
35
41
  emb = MagicMock()
36
42
  llm = MagicMock()
37
43
  articles = {"p": {}}
@@ -46,95 +52,34 @@ class TestQAToolHelper(unittest.TestCase):
46
52
  self.assertIs(ret_articles, articles)
47
53
 
48
54
  def test_get_state_models_and_data_missing_text_embedding(self):
49
- """test get_state_models_and_data raises ValueError if missing text embedding"""
55
+ """get_state_models_and_data should raise ValueError if text_embedding_model is missing"""
50
56
  state = {"llm_model": MagicMock(), "article_data": {"p": {}}}
51
- with self.assertRaises(ValueError) as cm:
57
+ with self.assertRaises(ValueError):
52
58
  self.helper.get_state_models_and_data(state)
53
- self.assertEqual(str(cm.exception), "No text embedding model found in state.")
54
59
 
55
60
  def test_get_state_models_and_data_missing_llm(self):
56
- """test get_state_models_and_data raises ValueError if missing LLM"""
61
+ """should raise ValueError if llm_model is missing"""
57
62
  state = {"text_embedding_model": MagicMock(), "article_data": {"p": {}}}
58
- with self.assertRaises(ValueError) as cm:
63
+ with self.assertRaises(ValueError):
59
64
  self.helper.get_state_models_and_data(state)
60
- self.assertEqual(str(cm.exception), "No LLM model found in state.")
61
65
 
62
66
  def test_get_state_models_and_data_missing_article_data(self):
63
- """test get_state_models_and_data raises ValueError if missing article data"""
67
+ """get_state_models_and_data should raise ValueError if article_data is missing"""
64
68
  state = {"text_embedding_model": MagicMock(), "llm_model": MagicMock()}
65
- with self.assertRaises(ValueError) as cm:
69
+ with self.assertRaises(ValueError):
66
70
  self.helper.get_state_models_and_data(state)
67
- self.assertEqual(str(cm.exception), "No article_data found in state.")
68
-
69
- def test_load_candidate_papers_calls_add_paper_only_for_valid(self):
70
- """test load_candidate_papers calls add_paper only for valid candidates"""
71
- vs = SimpleNamespace(loaded_papers=set(), add_paper=MagicMock())
72
- articles = {"p1": {"pdf_url": "url1"}, "p2": {}, "p3": {"pdf_url": None}}
73
- candidates = ["p1", "p2", "p3"]
74
- self.helper.load_candidate_papers(vs, articles, candidates)
75
- vs.add_paper.assert_called_once_with("p1", "url1", articles["p1"])
76
-
77
- def test_load_candidate_papers_handles_add_paper_exception(self):
78
- """test load_candidate_papers handles add_paper exception"""
79
- # If add_paper raises, it should be caught and not propagate
80
- vs = SimpleNamespace(
81
- loaded_papers=set(), add_paper=MagicMock(side_effect=ValueError("oops"))
82
- )
83
- articles = {"p1": {"pdf_url": "url1"}}
84
- # Start call to set call_id (used in logging)
85
- self.helper.start_call(SimpleNamespace(), "call001")
86
- # Should not raise despite exception
87
- self.helper.load_candidate_papers(vs, articles, ["p1"])
88
- vs.add_paper.assert_called_once_with("p1", "url1", articles["p1"])
89
-
90
- def test_run_reranker_success_and_filtering(self):
91
- """test run_reranker success and filtering"""
92
- # Successful rerank returns filtered candidates
93
- cfg = SimpleNamespace(top_k_papers=2)
94
- self.helper.config = cfg
95
- vs = MagicMock()
96
- with patch(
97
- "aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper.rank_papers_by_query",
98
- return_value=["a", "c"],
99
- ):
100
- out = self.helper.run_reranker(vs, "q", ["a", "b"])
101
- self.assertEqual(out, ["a"])
102
-
103
- def test_run_reranker_exception_fallback(self):
104
- """test run_reranker exception fallback"""
105
- # On reranker failure, should return original candidates
106
- cfg = SimpleNamespace(top_k_papers=5)
107
- self.helper.config = cfg
108
- vs = MagicMock()
109
-
110
- def fail(*args, **kwargs):
111
- raise RuntimeError("fail")
112
-
113
- with patch(
114
- "aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper.rank_papers_by_query",
115
- side_effect=fail,
116
- ):
117
- candidates = ["x", "y"]
118
- out = self.helper.run_reranker(vs, "q", candidates)
119
- self.assertEqual(out, candidates)
120
-
121
- def test_format_answer_with_and_without_sources(self):
122
- """test format_answer with and without sources"""
123
- articles = {"p1": {"Title": "T1"}, "p2": {"Title": "T2"}}
124
- # With sources
125
- with patch(
126
- "aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper.generate_answer",
127
- return_value={"output_text": "ans", "papers_used": ["p1", "p2"]},
128
- ):
129
- res = self.helper.format_answer("q", [], MagicMock(), articles)
130
- self.assertIn("ans", res)
131
- self.assertIn("Sources:", res)
132
- self.assertIn("- T1", res)
133
- self.assertIn("- T2", res)
134
- # Without sources
135
- with patch(
136
- "aiagents4pharma.talk2scholars.tools.pdf.utils.tool_helper.generate_answer",
137
- return_value={"output_text": "ans", "papers_used": []},
138
- ):
139
- res2 = self.helper.format_answer("q", [], MagicMock(), {})
140
- self.assertEqual(res2, "ans")
71
+
72
+ def test_get_hardware_stats(self):
73
+ """get_hardware_stats should return correct GPU and hardware mode"""
74
+ helper = QAToolHelper()
75
+ helper.call_id = "test_call"
76
+
77
+ helper.has_gpu = False
78
+ stats = helper.get_hardware_stats()
79
+ self.assertEqual(stats["gpu_available"], False)
80
+ self.assertEqual(stats["hardware_mode"], "CPU-only")
81
+
82
+ helper.has_gpu = True
83
+ stats = helper.get_hardware_stats()
84
+ self.assertEqual(stats["gpu_available"], True)
85
+ self.assertEqual(stats["hardware_mode"], "GPU-accelerated")
@@ -22,20 +22,20 @@ logger = logging.getLogger(__name__)
22
22
  class DownloadBiorxivPaperInput(BaseModel):
23
23
  """Input schema for the bioRxiv paper download tool."""
24
24
 
25
- doi: str = Field(description=
26
- """The bioRxiv DOI, from search_helper or multi_helper or single_helper,
25
+ doi: str = Field(
26
+ description="""The bioRxiv DOI, from search_helper or multi_helper or single_helper,
27
27
  used to retrieve the paper details and PDF URL."""
28
28
  )
29
- logger.info("DOI Received: %s", doi)
30
29
  tool_call_id: Annotated[str, InjectedToolCallId]
31
30
 
31
+
32
32
  def fetch_biorxiv_metadata(doi: str, api_url: str, request_timeout: int) -> dict:
33
33
  """
34
34
  Fetch metadata for a bioRxiv paper using its DOI and extract relevant fields.
35
-
35
+
36
36
  Parameters:
37
37
  doi (str): The DOI of the bioRxiv paper.
38
-
38
+
39
39
  Returns:
40
40
  dict: A dictionary containing the title, authors, abstract, publication date, and URLs.
41
41
  """
@@ -55,6 +55,7 @@ def fetch_biorxiv_metadata(doi: str, api_url: str, request_timeout: int) -> dict
55
55
 
56
56
  return data["collection"][0]
57
57
 
58
+
58
59
  def extract_metadata(paper: dict, doi: str) -> dict:
59
60
  """
60
61
  Extract relevant metadata fields from a bioRxiv paper entry.
@@ -75,9 +76,10 @@ def extract_metadata(paper: dict, doi: str) -> dict:
75
76
  "pdf_url": pdf_url,
76
77
  "filename": f"{doi_suffix}.pdf",
77
78
  "source": "biorxiv",
78
- "biorxiv_id": doi
79
+ "biorxiv_id": doi,
79
80
  }
80
81
 
82
+
81
83
  @tool(args_schema=DownloadBiorxivPaperInput, parse_docstring=True)
82
84
  def download_biorxiv_paper(
83
85
  doi: str,
@@ -22,13 +22,13 @@ logger = logging.getLogger(__name__)
22
22
  class DownloadMedrxivPaperInput(BaseModel):
23
23
  """Input schema for the medRxiv paper download tool."""
24
24
 
25
- doi: str = Field(description=
26
- """The medRxiv DOI, from search_helper or multi_helper or single_helper,
25
+ doi: str = Field(
26
+ description="""The medRxiv DOI, from search_helper or multi_helper or single_helper,
27
27
  used to retrieve the paper details and PDF URL."""
28
28
  )
29
- logger.info("DOI Received: %s", doi)
30
29
  tool_call_id: Annotated[str, InjectedToolCallId]
31
30
 
31
+
32
32
  # Fetching raw metadata from medRxiv API for a given DOI
33
33
  def fetch_medrxiv_metadata(doi: str, api_url: str, request_timeout: int) -> dict:
34
34
  """
@@ -54,6 +54,7 @@ def fetch_medrxiv_metadata(doi: str, api_url: str, request_timeout: int) -> dict
54
54
 
55
55
  return data["collection"][0]
56
56
 
57
+
57
58
  # Extracting relevant metadata fields from the raw data
58
59
  def extract_metadata(paper: dict, doi: str) -> dict:
59
60
  """
@@ -75,9 +76,10 @@ def extract_metadata(paper: dict, doi: str) -> dict:
75
76
  "pdf_url": pdf_url,
76
77
  "filename": f"{doi_suffix}.pdf",
77
78
  "source": "medrxiv",
78
- "medrxiv_id": doi
79
+ "medrxiv_id": doi,
79
80
  }
80
81
 
82
+
81
83
  # Tool to download medRxiv paper metadata and PDF URL
82
84
  @tool(args_schema=DownloadMedrxivPaperInput, parse_docstring=True)
83
85
  def download_medrxiv_paper(
@@ -1,17 +1,16 @@
1
1
  """
2
2
  LangGraph PDF Retrieval-Augmented Generation (RAG) Tool
3
3
 
4
- This tool answers user questions by retrieving and ranking relevant text chunks from PDFs
5
- and invoking an LLM to generate a concise, source-attributed response. It supports
6
- single or multiple PDF sources—such as Zotero libraries, arXiv papers, or direct uploads.
7
-
8
- Workflow:
9
- 1. (Optional) Load PDFs from diverse sources into a FAISS vector store of embeddings.
10
- 2. Rerank candidate papers using NVIDIA NIM semantic re-ranker.
11
- 3. Retrieve top-K diverse text chunks via Maximal Marginal Relevance (MMR).
12
- 4. Build a context-rich prompt combining retrieved chunks and the user question.
13
- 5. Invoke the LLM to craft a clear answer with source citations.
14
- 6. Return the answer in a ToolMessage for LangGraph to dispatch.
4
+ This tool answers user questions using the traditional RAG pipeline:
5
+ 1. Retrieve relevant chunks from ALL papers in the vector store
6
+ 2. Rerank chunks using NVIDIA NIM reranker to find the most relevant ones
7
+ 3. Generate answer using the top reranked chunks
8
+
9
+ Traditional RAG Pipeline Flow:
10
+ Query Retrieve chunks from ALL papers Rerank chunks Generate answer
11
+
12
+ This ensures the best possible chunks are selected across all available papers,
13
+ not just from pre-selected papers.
15
14
  """
16
15
 
17
16
  import logging
@@ -27,8 +26,10 @@ from langgraph.types import Command
27
26
  from pydantic import BaseModel, Field
28
27
 
29
28
  from .utils.generate_answer import load_hydra_config
30
- from .utils.retrieve_chunks import retrieve_relevant_chunks
31
29
  from .utils.tool_helper import QAToolHelper
30
+ from .utils.paper_loader import load_all_papers
31
+ from .utils.rag_pipeline import retrieve_and_rerank_chunks
32
+ from .utils.answer_formatter import format_answer
32
33
 
33
34
  # Helper for managing state, vectorstore, reranking, and formatting
34
35
  helper = QAToolHelper()
@@ -53,7 +54,6 @@ class QuestionAndAnswerInput(BaseModel):
53
54
  - article_data: metadata mapping of paper IDs to info (e.g., 'pdf_url', title).
54
55
  - text_embedding_model: embedding model instance for chunk indexing.
55
56
  - llm_model: chat/LLM instance for answer generation.
56
- - vector_store: optional pre-built Vectorstore for retrieval.
57
57
  """
58
58
 
59
59
  question: str = Field(
@@ -70,17 +70,16 @@ def question_and_answer(
70
70
  tool_call_id: Annotated[str, InjectedToolCallId],
71
71
  ) -> Command[Any]:
72
72
  """
73
- LangGraph tool for Retrieval-Augmented Generation over PDFs.
74
-
75
- Given a user question, this tool applies the following pipeline:
76
- 1. Validates that embedding and LLM models, plus article metadata, are in state.
77
- 2. Initializes or reuses a FAISS-based Vectorstore for PDF embeddings.
78
- 3. Loads one or more PDFs (from Zotero, arXiv, uploads) as text chunks into the store.
79
- 4. Uses NVIDIA NIM semantic re-ranker to select top candidate papers.
80
- 5. Retrieves the most relevant and diverse text chunks via Maximal Marginal Relevance.
81
- 6. Constructs an LLM prompt combining contextual chunks and the query.
82
- 7. Invokes the LLM to generate an answer, appending source attributions.
83
- 8. Returns a LangGraph Command with a ToolMessage containing the answer.
73
+ LangGraph tool for Retrieval-Augmented Generation over PDFs using traditional RAG pipeline.
74
+
75
+ Traditional RAG Pipeline Implementation:
76
+ 1. Load ALL available PDFs into Milvus vector store (if not already loaded)
77
+ 2. Retrieve relevant chunks from ALL papers using vector similarity search
78
+ 3. Rerank retrieved chunks using NVIDIA NIM semantic reranker
79
+ 4. Generate answer using top reranked chunks with source attribution
80
+
81
+ This approach ensures the best chunks are selected across all available papers,
82
+ rather than pre-selecting papers and potentially missing relevant information.
84
83
 
85
84
  Args:
86
85
  question (str): The free-text question to answer.
@@ -99,35 +98,70 @@ def question_and_answer(
99
98
  """
100
99
  call_id = f"qa_call_{time.time()}"
101
100
  logger.info(
102
- "Starting PDF Question and Answer tool call %s for question: %s",
101
+ "Starting PDF Question and Answer tool (Traditional RAG Pipeline) - Call %s",
103
102
  call_id,
104
- question,
105
103
  )
104
+ logger.info("%s: Question: '%s'", call_id, question)
105
+
106
106
  helper.start_call(config, call_id)
107
107
 
108
108
  # Extract models and article metadata
109
109
  text_emb, llm_model, article_data = helper.get_state_models_and_data(state)
110
110
 
111
- # Initialize or reuse vector store, then load candidate papers
111
+ # Initialize or reuse Milvus vector store
112
+ logger.info("%s: Initializing vector store", call_id)
112
113
  vs = helper.init_vector_store(text_emb)
113
- candidate_ids = list(article_data.keys())
114
- logger.info("%s: Candidate paper IDs for reranking: %s", call_id, candidate_ids)
115
- helper.load_candidate_papers(vs, article_data, candidate_ids)
116
-
117
- # Rerank papers and retrieve top chunks
118
- selected_ids = helper.run_reranker(vs, question, candidate_ids)
119
- relevant_chunks = retrieve_relevant_chunks(
120
- vs, query=question, paper_ids=selected_ids, top_k=config.top_k_chunks
114
+
115
+ # Load ALL papers (traditional RAG approach)
116
+ logger.info(
117
+ "%s: Loading all %d papers into vector store (traditional RAG approach)",
118
+ call_id,
119
+ len(article_data),
120
+ )
121
+ load_all_papers(
122
+ vector_store=vs,
123
+ articles=article_data,
124
+ call_id=call_id,
125
+ config=config,
126
+ has_gpu=helper.has_gpu,
127
+ )
128
+
129
+ # Traditional RAG Pipeline: Retrieve from ALL papers, then rerank
130
+ logger.info(
131
+ "%s: Starting traditional RAG pipeline: retrieve → rerank → generate",
132
+ call_id,
133
+ )
134
+
135
+ # Retrieve and rerank chunks in one step
136
+ reranked_chunks = retrieve_and_rerank_chunks(
137
+ vs, question, config, call_id, helper.has_gpu
121
138
  )
122
- if not relevant_chunks:
139
+
140
+ if not reranked_chunks:
123
141
  msg = f"No relevant chunks found for question: '{question}'"
124
142
  logger.warning("%s: %s", call_id, msg)
125
- raise RuntimeError(msg)
126
143
 
127
- # Generate answer and format with sources
128
- response_text = helper.format_answer(
129
- question, relevant_chunks, llm_model, article_data
144
+ # Generate answer using reranked chunks
145
+ logger.info(
146
+ "%s: Generating answer using %d reranked chunks",
147
+ call_id,
148
+ len(reranked_chunks),
130
149
  )
150
+ response_text = format_answer(
151
+ question,
152
+ reranked_chunks,
153
+ llm_model,
154
+ article_data,
155
+ config,
156
+ call_id=call_id,
157
+ has_gpu=helper.has_gpu,
158
+ )
159
+
160
+ logger.info(
161
+ "%s: Successfully traditional completed RAG pipeline",
162
+ call_id,
163
+ )
164
+
131
165
  return Command(
132
166
  update={
133
167
  "messages": [
@@ -2,9 +2,34 @@
2
2
  Utility modules for the PDF question_and_answer tool.
3
3
  """
4
4
 
5
+ from . import answer_formatter
6
+ from . import batch_processor
7
+ from . import collection_manager
5
8
  from . import generate_answer
9
+ from . import get_vectorstore
10
+ from . import gpu_detection
6
11
  from . import nvidia_nim_reranker
12
+ from . import paper_loader
13
+ from . import rag_pipeline
7
14
  from . import retrieve_chunks
15
+ from . import singleton_manager
16
+ from . import tool_helper
17
+ from . import vector_normalization
8
18
  from . import vector_store
9
19
 
10
- __all__ = ["generate_answer", "nvidia_nim_reranker", "retrieve_chunks", "vector_store"]
20
+ __all__ = [
21
+ "answer_formatter",
22
+ "batch_processor",
23
+ "collection_manager",
24
+ "generate_answer",
25
+ "get_vectorstore",
26
+ "gpu_detection",
27
+ "nvidia_nim_reranker",
28
+ "paper_loader",
29
+ "rag_pipeline",
30
+ "retrieve_chunks",
31
+ "singleton_manager",
32
+ "tool_helper",
33
+ "vector_normalization",
34
+ "vector_store",
35
+ ]
@@ -0,0 +1,62 @@
1
+ """
2
+ Format the final answer text with source attributions and hardware info.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Dict, List
7
+
8
+ from .generate_answer import generate_answer
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def format_answer(
14
+ question: str,
15
+ chunks: List[Any],
16
+ llm: Any,
17
+ articles: Dict[str, Any],
18
+ config: Any,
19
+ **kwargs: Any,
20
+ ) -> str:
21
+ """
22
+ Generate the final answer text with source attributions and hardware info.
23
+
24
+ Expects `call_id` and `has_gpu` in kwargs.
25
+ """
26
+ result = generate_answer(question, chunks, llm, config)
27
+ answer = result.get("output_text", "No answer generated.")
28
+
29
+ # Get unique paper titles for source attribution
30
+ titles: Dict[str, str] = {}
31
+ for pid in result.get("papers_used", []):
32
+ if pid in articles:
33
+ titles[pid] = articles[pid].get("Title", "Unknown paper")
34
+
35
+ # Format sources
36
+ if titles:
37
+ srcs = "\n\nSources:\n" + "\n".join(f"- {t}" for t in titles.values())
38
+ else:
39
+ srcs = ""
40
+
41
+ # Extract logging metadata
42
+ call_id = kwargs.get("call_id", "<no-call-id>")
43
+ has_gpu = kwargs.get("has_gpu", False)
44
+ hardware_info = "GPU-accelerated" if has_gpu else "CPU-processed"
45
+
46
+ # Log final statistics with hardware info
47
+ logger.info(
48
+ "%s: Generated answer using %d chunks from %d papers (%s)",
49
+ call_id,
50
+ len(chunks),
51
+ len(titles),
52
+ hardware_info,
53
+ )
54
+
55
+ # Add subtle hardware info to logs but not to user output
56
+ logger.debug(
57
+ "%s: Answer generation completed with %s processing",
58
+ call_id,
59
+ hardware_info,
60
+ )
61
+
62
+ return f"{answer}{srcs}"