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.
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +4 -0
- aiagents4pharma/talk2scholars/configs/tools/question_and_answer/default.yaml +44 -4
- aiagents4pharma/talk2scholars/tests/test_nvidia_nim_reranker.py +127 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_answer_formatter.py +66 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_batch_processor.py +101 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_collection_manager.py +150 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_document_processor.py +69 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_generate_answer.py +75 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_gpu_detection.py +140 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_paper_loader.py +116 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_rag_pipeline.py +98 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_retrieve_chunks.py +197 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_singleton_manager.py +156 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_vector_normalization.py +121 -0
- aiagents4pharma/talk2scholars/tests/test_pdf_vector_store.py +434 -0
- aiagents4pharma/talk2scholars/tests/test_question_and_answer_tool.py +89 -509
- aiagents4pharma/talk2scholars/tests/test_tool_helper_utils.py +34 -89
- aiagents4pharma/talk2scholars/tools/paper_download/download_biorxiv_input.py +8 -6
- aiagents4pharma/talk2scholars/tools/paper_download/download_medrxiv_input.py +6 -4
- aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +74 -40
- aiagents4pharma/talk2scholars/tools/pdf/utils/__init__.py +26 -1
- aiagents4pharma/talk2scholars/tools/pdf/utils/answer_formatter.py +62 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/batch_processor.py +200 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/collection_manager.py +172 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/document_processor.py +76 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/generate_answer.py +14 -14
- aiagents4pharma/talk2scholars/tools/pdf/utils/get_vectorstore.py +63 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/gpu_detection.py +154 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/nvidia_nim_reranker.py +60 -40
- aiagents4pharma/talk2scholars/tools/pdf/utils/paper_loader.py +123 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/rag_pipeline.py +122 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/retrieve_chunks.py +162 -40
- aiagents4pharma/talk2scholars/tools/pdf/utils/singleton_manager.py +140 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/tool_helper.py +40 -78
- aiagents4pharma/talk2scholars/tools/pdf/utils/vector_normalization.py +159 -0
- aiagents4pharma/talk2scholars/tools/pdf/utils/vector_store.py +277 -96
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +12 -9
- aiagents4pharma/talk2scholars/tools/s2/query_dataframe.py +0 -1
- aiagents4pharma/talk2scholars/tools/s2/retrieve_semantic_scholar_paper_id.py +9 -8
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +5 -5
- {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/METADATA +27 -115
- {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/RECORD +45 -23
- aiagents4pharma/talk2scholars/tests/test_nvidia_nim_reranker_utils.py +0 -28
- {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.40.0.dist-info → aiagents4pharma-1.41.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
13
|
+
"""tests for QAToolHelper routines"""
|
14
14
|
|
15
15
|
def setUp(self):
|
16
|
-
"""
|
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
|
-
"""
|
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
|
-
|
27
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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)
|
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
|
-
"""
|
61
|
+
"""should raise ValueError if llm_model is missing"""
|
57
62
|
state = {"text_embedding_model": MagicMock(), "article_data": {"p": {}}}
|
58
|
-
with self.assertRaises(ValueError)
|
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
|
-
"""
|
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)
|
69
|
+
with self.assertRaises(ValueError):
|
66
70
|
self.helper.get_state_models_and_data(state)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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(
|
26
|
-
|
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(
|
26
|
-
|
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
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
76
|
-
1.
|
77
|
-
2.
|
78
|
-
3.
|
79
|
-
4.
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
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
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
128
|
-
|
129
|
-
|
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__ = [
|
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}"
|