aurelian 0.3.3__py3-none-any.whl → 0.4.1__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 (38) hide show
  1. aurelian/agents/biblio/biblio_agent.py +1 -0
  2. aurelian/agents/checklist/checklist_agent.py +1 -0
  3. aurelian/agents/chemistry/chemistry_agent.py +2 -1
  4. aurelian/agents/d4d/__init__.py +2 -2
  5. aurelian/agents/d4d/d4d_agent.py +4 -3
  6. aurelian/agents/d4d/d4d_gradio.py +2 -2
  7. aurelian/agents/diagnosis/diagnosis_agent.py +1 -0
  8. aurelian/agents/gocam/__init__.py +10 -1
  9. aurelian/agents/gocam/gocam_agent.py +3 -0
  10. aurelian/agents/gocam/gocam_evals.py +0 -3
  11. aurelian/agents/linkml/linkml_mcp.py +1 -6
  12. aurelian/agents/literature/literature_agent.py +20 -0
  13. aurelian/agents/monarch/__init__.py +0 -25
  14. aurelian/agents/monarch/monarch_agent.py +1 -0
  15. aurelian/agents/monarch/monarch_tools.py +0 -1
  16. aurelian/agents/ontology_mapper/ontology_mapper_agent.py +1 -0
  17. aurelian/agents/paperqa/__init__.py +27 -0
  18. aurelian/agents/paperqa/paperqa_agent.py +66 -0
  19. aurelian/agents/paperqa/paperqa_cli.py +305 -0
  20. aurelian/agents/paperqa/paperqa_config.py +142 -0
  21. aurelian/agents/paperqa/paperqa_gradio.py +90 -0
  22. aurelian/agents/paperqa/paperqa_mcp.py +155 -0
  23. aurelian/agents/paperqa/paperqa_tools.py +566 -0
  24. aurelian/agents/rag/rag_agent.py +1 -0
  25. aurelian/agents/talisman/talisman_mcp.py +50 -143
  26. aurelian/agents/ubergraph/ubergraph_agent.py +1 -0
  27. aurelian/agents/uniprot/__init__.py +0 -37
  28. aurelian/agents/web/web_tools.py +16 -3
  29. aurelian/cli.py +36 -0
  30. aurelian/evaluators/model.py +9 -0
  31. aurelian/evaluators/substring_evaluator.py +30 -0
  32. aurelian/utils/async_utils.py +6 -3
  33. {aurelian-0.3.3.dist-info → aurelian-0.4.1.dist-info}/METADATA +8 -4
  34. {aurelian-0.3.3.dist-info → aurelian-0.4.1.dist-info}/RECORD +37 -28
  35. {aurelian-0.3.3.dist-info → aurelian-0.4.1.dist-info}/WHEEL +1 -1
  36. aurelian-0.4.1.dist-info/entry_points.txt +4 -0
  37. aurelian-0.3.3.dist-info/entry_points.txt +0 -3
  38. {aurelian-0.3.3.dist-info → aurelian-0.4.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,305 @@
1
+ """
2
+ CLI commands for the PaperQA agent.
3
+ """
4
+ import os
5
+ import asyncio
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+ import click
10
+ from paperqa import agent_query
11
+
12
+ from aurelian.agents.paperqa.paperqa_config import get_config
13
+ from paperqa.agents.search import get_directory_index
14
+ from paperqa.settings import IndexSettings
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ def setup_logging():
19
+ """Set up logging for the PaperQA CLI."""
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
23
+ handlers=[logging.StreamHandler(sys.stdout)]
24
+ )
25
+
26
+
27
+ def check_api_key():
28
+ """Check if the OpenAI API key is set.
29
+
30
+ Returns:
31
+ bool: True if key is set, False otherwise
32
+ """
33
+ if not os.environ.get("OPENAI_API_KEY"):
34
+ logger.error("OPENAI_API_KEY environment variable must be set.")
35
+ click.echo("Error: OPENAI_API_KEY environment variable must be set.")
36
+ return False
37
+ return True
38
+
39
+
40
+ def setup_and_configure_paper_directory(directory):
41
+ """
42
+ Setup and configure a paper directory with proper paths.
43
+
44
+ Args:
45
+ directory: Input directory path (can be relative)
46
+
47
+ Returns:
48
+ tuple: (resolved_path, settings, config) tuple with properly configured settings
49
+ """
50
+ directory = str(Path(directory).resolve())
51
+
52
+ config = get_config()
53
+ config.paper_directory = directory
54
+
55
+ os.environ["PQA_HOME"] = directory
56
+
57
+ if not os.path.exists(directory):
58
+ logger.info(f"Creating paper directory: {directory}")
59
+ os.makedirs(directory, exist_ok=True)
60
+
61
+ settings = config.set_paperqa_settings()
62
+ settings.agent.index = IndexSettings(
63
+ name=config.index_name,
64
+ paper_directory=directory,
65
+ recurse_subdirectories=False
66
+ )
67
+
68
+ return directory, settings, config
69
+
70
+
71
+ def get_document_files(directory):
72
+ """
73
+ Get all indexable document files in the given directory.
74
+
75
+ Args:
76
+ directory: Directory to search for document files
77
+
78
+ Returns:
79
+ dict: Dictionary with file lists by type and a combined list
80
+ """
81
+ document_extensions = ['.pdf', '.txt', '.html', '.md']
82
+ all_files = [f for f in os.listdir(directory)
83
+ if any(f.lower().endswith(ext) for ext in document_extensions)]
84
+
85
+ return {
86
+ 'all': all_files,
87
+ 'pdf': [f for f in all_files if f.lower().endswith('.pdf')],
88
+ 'txt': [f for f in all_files if f.lower().endswith('.txt')],
89
+ 'html': [f for f in all_files if f.lower().endswith('.html')],
90
+ 'md': [f for f in all_files if f.lower().endswith('.md')],
91
+ }
92
+
93
+
94
+ @click.group(name="paperqa")
95
+ @click.option("-v", "--verbose", count=True, help="Increase verbosity level (-v for INFO, -vv for DEBUG)")
96
+ @click.option("-q", "--quiet", is_flag=True, help="Suppress non-error output")
97
+ def paperqa_cli(verbose, quiet):
98
+ """PaperQA management commands for indexing and querying documents.
99
+
100
+ PaperQA supports PDF, TXT, HTML, and Markdown files in all operations.
101
+
102
+ Examples:
103
+ # Index documents in a directory
104
+ aurelian paperqa index -d /path/to/papers
105
+
106
+ # Ask a question about indexed papers
107
+ aurelian paperqa ask "What is the role of tau protein in Alzheimer's?" -d /path/to/papers
108
+
109
+ # List indexed papers
110
+ aurelian paperqa list -d /path/to/papers
111
+
112
+ # Run with increased verbosity
113
+ aurelian paperqa --verbose index -d /path/to/papers
114
+
115
+ # Add documents through the agent
116
+ # (Using these commands in chat modes like Gradio or MCP)
117
+ "Add the paper from /path/to/paper.pdf"
118
+ "Add all papers from the directory /path/to/papers/"
119
+ """
120
+ setup_logging()
121
+
122
+ if verbose >= 2:
123
+ logging.getLogger("aurelian.agents.paperqa").setLevel(logging.DEBUG)
124
+ elif verbose == 1:
125
+ logging.getLogger("aurelian.agents.paperqa").setLevel(logging.INFO)
126
+ else:
127
+ logging.getLogger("aurelian.agents.paperqa").setLevel(logging.WARNING)
128
+
129
+ if quiet:
130
+ logging.getLogger("aurelian.agents.paperqa").setLevel(logging.ERROR)
131
+
132
+
133
+ @paperqa_cli.command()
134
+ @click.option(
135
+ "--directory", "-d",
136
+ required=True,
137
+ help="Paper directory containing PDF, TXT, HTML, and MD files to index",
138
+ )
139
+ def index(directory):
140
+ """Index documents for search and querying.
141
+
142
+ This command scans the specified directory for documents (PDF, TXT, HTML, MD)
143
+ and creates a searchable index for them. The index is stored in the .pqa
144
+ subdirectory of the specified directory.
145
+
146
+ Example:
147
+ aurelian paperqa index -d ~/research/papers
148
+ """
149
+ if not check_api_key():
150
+ return
151
+
152
+ paper_dir, settings, _ = setup_and_configure_paper_directory(directory)
153
+
154
+ docs = get_document_files(paper_dir)
155
+
156
+ if not docs['all']:
157
+ logger.warning(f"No indexable documents found in {paper_dir}")
158
+ click.echo(f"No indexable documents found in {paper_dir}")
159
+ return
160
+
161
+ # detailed breakdown
162
+ logger.info(f"Found {len(docs['all'])} documents in {paper_dir}:")
163
+ if docs['pdf']: logger.info(f" - {len(docs['pdf'])} PDF files")
164
+ if docs['txt']: logger.info(f" - {len(docs['txt'])} text files")
165
+ if docs['html']: logger.info(f" - {len(docs['html'])} HTML files")
166
+ if docs['md']: logger.info(f" - {len(docs['md'])} Markdown files")
167
+ logger.info(f"Index will be stored in: {paper_dir}/.pqa")
168
+ logger.info("Indexing papers... (this may take a while)")
169
+
170
+ async def run_index():
171
+ try:
172
+ index = await get_directory_index(
173
+ settings=settings,
174
+ build=True,
175
+ )
176
+ index_files = await index.index_files
177
+ logger.info(f"Success! Indexed {len(index_files)} document chunks from your PDF files.")
178
+ except Exception as e:
179
+ logger.error(f"Error indexing papers: {str(e)}")
180
+
181
+ try:
182
+ asyncio.run(run_index())
183
+ except Exception as e:
184
+ logger.error(f"Error: {str(e)}")
185
+
186
+
187
+ @paperqa_cli.command()
188
+ @click.argument("query", required=True)
189
+ @click.option(
190
+ "--directory", "-d",
191
+ required=True,
192
+ help="Paper directory containing indexed documents",
193
+ )
194
+ def ask(query, directory):
195
+ """Ask a question about the indexed documents.
196
+
197
+ This command searches the indexed documents for information relevant to the
198
+ provided query and generates an AI-powered answer with references. Make sure
199
+ to run the 'index' command first to create an index.
200
+
201
+ Example:
202
+ aurelian paperqa ask "What are the key findings on tau proteins?" -d ~/research/papers
203
+ """
204
+ if not check_api_key():
205
+ return
206
+
207
+ paper_dir, settings, _ = setup_and_configure_paper_directory(directory)
208
+
209
+ async def run_query():
210
+ try:
211
+ docs = get_document_files(paper_dir)
212
+
213
+ if not docs['all']:
214
+ logger.warning(f"No indexable documents found in {paper_dir}")
215
+ logger.info(f"Add documents (PDF, TXT, HTML, MD) to the directory and then run 'aurelian paperqa index -d {paper_dir}'")
216
+ return
217
+
218
+ try:
219
+ index = await get_directory_index(settings=settings, build=False)
220
+ index_files = await index.index_files
221
+
222
+ if not index_files:
223
+ logger.warning(f"No indexed papers found. PDF files exist but haven't been indexed.")
224
+ logger.info(f"Run 'aurelian paperqa index -d {paper_dir}' to index the papers.")
225
+ return
226
+ except Exception as e:
227
+ if "was empty, please rebuild it" in str(e):
228
+ logger.warning(f"Index is empty. Run 'aurelian paperqa index -d {paper_dir}' to index papers.")
229
+ return
230
+ raise
231
+
232
+ logger.info(f"Querying {len(index_files)} papers about: {query}")
233
+ logger.info("This may take a moment...")
234
+
235
+ response = await agent_query(
236
+ query=query,
237
+ settings=settings
238
+ )
239
+
240
+ click.echo(f"Answer: {response.session.answer}" +
241
+ f"\n\nReferences: {response.session.references}")
242
+
243
+ except Exception as e:
244
+ logger.error(f"Error querying papers: {str(e)}")
245
+
246
+ loop = asyncio.get_event_loop()
247
+ try:
248
+ loop.run_until_complete(run_query())
249
+ except Exception as e:
250
+ logger.error(f"Error: {str(e)}")
251
+
252
+
253
+ @paperqa_cli.command()
254
+ @click.option(
255
+ "--directory", "-d",
256
+ required=True,
257
+ help="Paper directory containing documents",
258
+ )
259
+ def list(directory):
260
+ """List documents in the directory and their indexing status.
261
+
262
+ This command displays all documents (PDF, TXT, HTML, MD) in the specified
263
+ directory and shows which ones have been indexed. Use this to verify that
264
+ your documents are properly recognized and indexed.
265
+
266
+ Example:
267
+ aurelian paperqa list -d ~/research/papers
268
+ """
269
+ if not check_api_key():
270
+ return
271
+
272
+ paper_dir, settings, _ = setup_and_configure_paper_directory(directory)
273
+
274
+ docs = get_document_files(paper_dir)
275
+
276
+ logger.info(f"Documents in directory {paper_dir}:")
277
+ for doc in docs['all']:
278
+ if doc.lower().endswith('.pdf'):
279
+ logger.info(f" - {doc} [PDF]")
280
+ elif doc.lower().endswith('.txt'):
281
+ logger.info(f" - {doc} [TXT]")
282
+ elif doc.lower().endswith('.html'):
283
+ logger.info(f" - {doc} [HTML]")
284
+ elif doc.lower().endswith('.md'):
285
+ logger.info(f" - {doc} [MD]")
286
+
287
+ async def list_indexed():
288
+ try:
289
+ index = await get_directory_index(settings=settings, build=False)
290
+ index_files = await index.index_files
291
+ if index_files:
292
+ logger.info(f"Indexed papers ({len(index_files)}):")
293
+ for file in index_files:
294
+ logger.info(f" - {file}")
295
+ else:
296
+ logger.warning(f"No indexed papers found. Run 'aurelian paperqa index -d {paper_dir}' to index papers.")
297
+ except Exception as e:
298
+ logger.error(f"Error accessing index: {str(e)}")
299
+ logger.info(f"Run 'aurelian paperqa index -d {paper_dir}' to create or rebuild the index.")
300
+
301
+ loop = asyncio.get_event_loop()
302
+ try:
303
+ loop.run_until_complete(list_indexed())
304
+ except Exception as e:
305
+ logger.error(f"Error: {str(e)}")
@@ -0,0 +1,142 @@
1
+ """
2
+ Configuration for the PaperQA agent.
3
+ """
4
+ from dataclasses import dataclass, field
5
+ import os
6
+ from typing import Optional, List
7
+
8
+ from paperqa import Settings as PQASettings
9
+ from paperqa.settings import (
10
+ AnswerSettings,
11
+ ParsingSettings,
12
+ PromptSettings,
13
+ AgentSettings,
14
+ IndexSettings, Settings,
15
+ )
16
+
17
+ from aurelian.dependencies.workdir import HasWorkdir, WorkDir
18
+
19
+
20
+ @dataclass
21
+ class PaperQADependencies(HasWorkdir):
22
+ """Configuration for the PaperQA agent."""
23
+
24
+ paper_directory: str = field(
25
+ default_factory=lambda: os.getcwd(),
26
+ metadata={"description": "Directory containing papers to be searched"}
27
+ )
28
+ index_name: Optional[str] = field(
29
+ default=None,
30
+ metadata={"description": "Optional name for the search index. If None, it will be generated based on settings."}
31
+ )
32
+
33
+ llm: str = field(
34
+ default="gpt-4.1-2025-04-14",
35
+ metadata={"description": "LLM to use for queries and answer generation. Default is gpt-4.1-2025-04-14."}
36
+ )
37
+ summary_llm: str = field(
38
+ default="gpt-4.1-2025-04-14",
39
+ metadata={"description": "LLM to use for summarization. Default is gpt-4.1-2025-04-14."}
40
+ )
41
+ embedding: str = field(
42
+ default="text-embedding-3-small",
43
+ metadata={"description": "Embedding model to use. Default is text-embedding-3-small."}
44
+ )
45
+ temperature: float = field(
46
+ default=0.1,
47
+ metadata={"description": "Temperature for LLM generation. Default is 0.1."}
48
+ )
49
+
50
+ search_count: int = field(
51
+ default=8,
52
+ metadata={"description": "Number of papers to retrieve in searches. Default is 8."}
53
+ )
54
+
55
+ evidence_k: int = field(
56
+ default=10,
57
+ metadata={"description": "Number of evidence pieces to retrieve. Default is 10."}
58
+ )
59
+ answer_max_sources: int = field(
60
+ default=5,
61
+ metadata={"description": "Maximum number of sources to use in answers. Default is 5."}
62
+ )
63
+ max_concurrent_requests: int = field(
64
+ default=4,
65
+ metadata={"description": "Maximum number of concurrent requests to LLMs. Default is 4."}
66
+ )
67
+
68
+ chunk_size: int = field(
69
+ default=5000,
70
+ metadata={"description": "Size of document chunks for embedding. Default is 5000."}
71
+ )
72
+ overlap: int = field(
73
+ default=250,
74
+ metadata={"description": "Overlap between chunks. Default is 250."}
75
+ )
76
+
77
+ workdir: Optional[WorkDir] = None
78
+
79
+ def __post_init__(self):
80
+ """Initialize the config with default values."""
81
+ if self.workdir is None:
82
+ self.workdir = WorkDir()
83
+
84
+ def set_paperqa_settings(self) -> PQASettings:
85
+ """
86
+ Convert to PaperQA Settings object.
87
+
88
+ This allows users to customize all PaperQA settings through the dependencies object.
89
+ Any changes to the dependencies will be reflected in the returned Settings object.
90
+ """
91
+ return PQASettings(
92
+ llm=self.llm,
93
+ summary_llm=self.summary_llm,
94
+ embedding=self.embedding,
95
+ temperature=self.temperature,
96
+
97
+ answer=AnswerSettings(
98
+ evidence_k=self.evidence_k,
99
+ answer_max_sources=self.answer_max_sources,
100
+ max_concurrent_requests=self.max_concurrent_requests,
101
+ ),
102
+
103
+ parsing=ParsingSettings(
104
+ chunk_size=self.chunk_size,
105
+ overlap=self.overlap,
106
+ ),
107
+
108
+ agent=AgentSettings(
109
+ agent_llm=self.llm,
110
+ search_count=self.search_count,
111
+ index=IndexSettings(
112
+ name=self.index_name,
113
+ paper_directory=self.paper_directory,
114
+ recurse_subdirectories=False,
115
+ ),
116
+ ),
117
+ )
118
+
119
+
120
+ def get_config() -> PaperQADependencies:
121
+ """
122
+ Get the PaperQA configuration from environment variables or defaults.
123
+
124
+ Returns:
125
+ A PaperQADependencies instance with default settings.
126
+
127
+ Note:
128
+ Users can modify the returned object to customize settings.
129
+ Example:
130
+ ```python
131
+ deps = get_config()
132
+ deps.llm = "claude-3-sonnet-20240229" # Use Claude instead of default GPT-4
133
+ deps.temperature = 0.5 # Increase temperature
134
+ deps.evidence_k = 15 # Retrieve more evidence
135
+ ```
136
+ """
137
+ workdir_path = os.environ.get("AURELIAN_WORKDIR", None)
138
+ workdir = WorkDir(location=workdir_path) if workdir_path else None
139
+
140
+ return PaperQADependencies(
141
+ workdir=workdir,
142
+ )
@@ -0,0 +1,90 @@
1
+ """
2
+ Gradio interface for the PaperQA agent.
3
+ """
4
+ import os
5
+ import logging
6
+ from typing import List, Optional, Any
7
+
8
+ import gradio as gr
9
+
10
+ from aurelian.utils.async_utils import run_sync
11
+ from .paperqa_agent import paperqa_agent
12
+ from .paperqa_config import PaperQADependencies, get_config
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def get_info(query: str, history: List[str], deps: PaperQADependencies, **kwargs) -> str:
18
+ """
19
+ Process a query using the PaperQA agent.
20
+
21
+ Args:
22
+ query: The user query
23
+ history: The conversation history
24
+ deps: The dependencies for the agent
25
+ model: Optional model override
26
+
27
+ Returns:
28
+ The agent's response
29
+ """
30
+ logger.info(f"QUERY: {query}")
31
+ logger.debug(f"HISTORY: {history}")
32
+
33
+ if history:
34
+ query += "\n\n## Previous Conversation:\n"
35
+ for h in history:
36
+ query += f"\n{h}"
37
+
38
+ result = await paperqa_agent.run(query, deps=deps, **kwargs)
39
+ return result.data
40
+
41
+
42
+ def chat(deps: Optional[PaperQADependencies] = None, model=None, **kwargs):
43
+ """
44
+ Create a Gradio chat interface for the PaperQA agent.
45
+
46
+ Args:
47
+ deps: Optional dependencies configuration
48
+ model: Optional model override
49
+ kwargs: Additional keyword arguments for dependencies
50
+
51
+ Returns:
52
+ A Gradio ChatInterface
53
+ """
54
+ if deps is None:
55
+ deps = get_config()
56
+
57
+ for key, value in kwargs.items():
58
+ if hasattr(deps, key):
59
+ setattr(deps, key, value)
60
+
61
+ paper_dir = os.path.join(os.getcwd(), "pqa_source")
62
+ os.makedirs(paper_dir, exist_ok=True)
63
+ deps.paper_directory = paper_dir
64
+ os.environ["PQA_HOME"] = paper_dir
65
+ print(f"Using dedicated papers directory at: {paper_dir}")
66
+
67
+ def get_info_wrapper(query: str, history: List[str]) -> str:
68
+ """Wrapper for the async get_info function."""
69
+ import asyncio
70
+ return asyncio.run(get_info(query, history, deps, **kwargs))
71
+
72
+ return gr.ChatInterface(
73
+ fn=get_info_wrapper,
74
+ type="messages",
75
+ title="PaperQA AI Assistant",
76
+ description="""This assistant helps you search and analyze scientific papers. You can:
77
+ - Search for papers on a topic
78
+ - Ask questions about the papers in the repository
79
+ - Add specific papers by path or URL: (if paths, use absolute paths!)"
80
+ - Add multiple papers from a directory: (use absolute paths!)"
81
+ - List all papers in the collection
82
+
83
+ Supported document types: PDF, TXT, HTML, and Markdown files""",
84
+ examples=[
85
+ ["Search for papers on CRISPR gene editing"],
86
+ ["What are the main challenges in CRISPR gene editing?"],
87
+ ["What is the relationship between CRISPR and Cas9?"],
88
+ ["List all the indexed papers"],
89
+ ],
90
+ )
@@ -0,0 +1,155 @@
1
+ """
2
+ MCP tools for working with PaperQA for scientific literature search and analysis.
3
+ """
4
+ import os
5
+ from typing import Dict, List, Any, Optional
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+ import aurelian.agents.paperqa.paperqa_tools as pt
10
+ from aurelian.agents.paperqa.paperqa_agent import paperqa_agent, PAPERQA_SYSTEM_PROMPT
11
+ from aurelian.agents.paperqa.paperqa_config import PaperQADependencies
12
+ from pydantic_ai import RunContext
13
+
14
+ mcp = FastMCP("paperqa", instructions=PAPERQA_SYSTEM_PROMPT.strip())
15
+
16
+
17
+ from aurelian.dependencies.workdir import WorkDir
18
+
19
+ def deps() -> PaperQADependencies:
20
+ deps = PaperQADependencies()
21
+ loc = os.getenv("AURELIAN_WORKDIR", "/tmp/paperqa")
22
+ deps.workdir = WorkDir(loc)
23
+
24
+ paper_dir = os.getenv("PAPERQA_PAPER_DIRECTORY", os.path.join(loc, "papers"))
25
+ deps.paper_directory = paper_dir
26
+
27
+ if os.getenv("PAPERQA_LLM"):
28
+ deps.llm = os.getenv("PAPERQA_LLM")
29
+
30
+ if os.getenv("PAPERQA_EMBEDDING"):
31
+ deps.embedding = os.getenv("PAPERQA_EMBEDDING")
32
+
33
+ return deps
34
+
35
+ def ctx() -> RunContext[PaperQADependencies]:
36
+ rc: RunContext[PaperQADependencies] = RunContext[PaperQADependencies](
37
+ deps=deps(),
38
+ model=None, usage=None, prompt=None,
39
+ )
40
+ return rc
41
+
42
+
43
+ @mcp.tool()
44
+ async def search_papers(query: str, max_papers: Optional[int] = None) -> Any:
45
+ """
46
+ Search for papers relevant to the query using PaperQA.
47
+
48
+ Args:
49
+ query: The search query for scientific papers
50
+ max_papers: Maximum number of papers to return (overrides config)
51
+
52
+ Returns:
53
+ The search results with paper information
54
+
55
+ This searches for scientific papers based on your query. It returns papers
56
+ that are most relevant to the topic you're searching for. You can optionally
57
+ specify a maximum number of papers to return.
58
+ """
59
+ return await pt.search_papers(ctx(), query, max_papers)
60
+
61
+
62
+ @mcp.tool()
63
+ async def query_papers(query: str) -> Any:
64
+ """
65
+ Query the papers to answer a specific question using PaperQA.
66
+
67
+ Args:
68
+ query: The question to answer based on the papers
69
+
70
+ Returns:
71
+ Detailed answer with citations from the papers
72
+
73
+ This tool analyzes the papers in your collection to provide an evidence-based
74
+ answer to your question. It extracts relevant information from across papers
75
+ and synthesizes a response with citations to the source papers.
76
+ """
77
+ return await pt.query_papers(ctx(), query)
78
+
79
+
80
+ @mcp.tool()
81
+ async def add_paper(path: str, citation: Optional[str] = None) -> Any:
82
+ """
83
+ Add a specific paper to the collection.
84
+
85
+ Args:
86
+ path: Path to the paper file or URL
87
+ citation: Optional citation for the paper
88
+
89
+ Returns:
90
+ Information about the added paper
91
+
92
+ You can add a paper by providing its file path (PDF) or a URL to the paper
93
+ (must be accessible). The paper will be added to your collection for searching
94
+ and querying. You can optionally provide a citation string.
95
+ """
96
+ return await pt.add_paper(ctx(), path, citation)
97
+
98
+ @mcp.tool()
99
+ async def add_papers(path: str,) -> Any:
100
+ """
101
+ Add multiple papers to the collection.
102
+ Args:
103
+ path: Path to the paper file or URL
104
+ citation: Optional citation for the paper
105
+
106
+ Returns:
107
+ Informations about the added papers
108
+
109
+ You can add multiple papers by providing its file path (PDF) or a URL to the
110
+ paper (must be accessible). The paper will be added to your collection for
111
+ searching and querying.
112
+ """
113
+ return await pt.add_papers(ctx(), path)
114
+
115
+
116
+ @mcp.tool()
117
+ async def list_papers() -> Any:
118
+ """
119
+ List all papers in the current paper directory.
120
+
121
+ Args:
122
+ None
123
+
124
+ Returns:
125
+ Information about all papers in the paper directory
126
+
127
+ This lists all papers currently in your collection, showing their file paths and
128
+ any other available metadata. Use this to see what papers you have available
129
+ for searching and querying.
130
+ """
131
+ return await pt.list_papers(ctx())
132
+
133
+
134
+ @mcp.tool()
135
+ async def build_index() -> Any:
136
+ """
137
+ Rebuild the search index for papers.
138
+
139
+ Args:
140
+ None
141
+
142
+ Returns:
143
+ Information about the indexing process
144
+
145
+ This rebuilds the search index for all papers in your paper directory.
146
+ The index is required for searching and querying papers. You should run this
147
+ after adding new papers to make them searchable.
148
+ """
149
+ return await pt.build_index(ctx())
150
+
151
+
152
+ if __name__ == "__main__":
153
+ print("Running the PaperQA MCP server")
154
+ print("Use Ctrl-C to exit")
155
+ mcp.run(transport='stdio')