academia-mcp 1.1.4__tar.gz → 1.2.0__tar.gz

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 (43) hide show
  1. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/PKG-INFO +2 -1
  2. academia_mcp-1.2.0/academia_mcp/llm.py +38 -0
  3. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/server.py +9 -3
  4. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/__init__.py +3 -2
  5. academia_mcp-1.2.0/academia_mcp/tools/bitflip.py +282 -0
  6. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/document_qa.py +8 -37
  7. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/PKG-INFO +2 -1
  8. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/SOURCES.txt +1 -0
  9. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/requires.txt +1 -0
  10. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/pyproject.toml +5 -1
  11. academia_mcp-1.2.0/tests/test_bitflip.py +38 -0
  12. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_document_qa.py +4 -4
  13. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_web_search.py +0 -1
  14. academia_mcp-1.1.4/academia_mcp/tools/bitflip.py +0 -193
  15. academia_mcp-1.1.4/tests/test_bitflip.py +0 -19
  16. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/LICENSE +0 -0
  17. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/README.md +0 -0
  18. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/__init__.py +0 -0
  19. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/__main__.py +0 -0
  20. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/files.py +0 -0
  21. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/py.typed +0 -0
  22. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/anthology_search.py +0 -0
  23. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/arxiv_download.py +0 -0
  24. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/arxiv_search.py +0 -0
  25. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/hf_datasets_search.py +0 -0
  26. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/md_to_pdf.py +0 -0
  27. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/py.typed +0 -0
  28. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/s2_citations.py +0 -0
  29. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/visit_webpage.py +0 -0
  30. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/tools/web_search.py +0 -0
  31. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp/utils.py +0 -0
  32. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/dependency_links.txt +0 -0
  33. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/entry_points.txt +0 -0
  34. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/academia_mcp.egg-info/top_level.txt +0 -0
  35. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/setup.cfg +0 -0
  36. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_anthology_search.py +0 -0
  37. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_arxiv_download.py +0 -0
  38. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_arxiv_search.py +0 -0
  39. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_extract_json.py +0 -0
  40. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_hf_dataset_search.py +0 -0
  41. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_md_to_pdf.py +0 -0
  42. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_s2_citations.py +0 -0
  43. {academia_mcp-1.1.4 → academia_mcp-1.2.0}/tests/test_visit_webpage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: academia-mcp
3
- Version: 1.1.4
3
+ Version: 1.2.0
4
4
  Summary: MCP server that provides different tools to search for scientific publications
5
5
  Author-email: Ilya Gusev <phoenixilya@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/IlyaGusev/academia_mcp
@@ -31,6 +31,7 @@ Requires-Dist: pytest>=8.4.1
31
31
  Requires-Dist: openai>=1.97.1
32
32
  Requires-Dist: jinja2>=3.1.6
33
33
  Requires-Dist: datasets>=4.0.0
34
+ Requires-Dist: pytest-asyncio>=1.1.0
34
35
  Dynamic: license-file
35
36
 
36
37
  # Academia MCP
@@ -0,0 +1,38 @@
1
+ import os
2
+ from typing import List, Dict, Any
3
+
4
+ from pydantic import BaseModel
5
+ from openai import AsyncOpenAI
6
+ from openai.types.chat.chat_completion_message import ChatCompletionMessage
7
+
8
+
9
+ class ChatMessage(BaseModel): # type: ignore
10
+ role: str
11
+ content: str | List[Dict[str, Any]]
12
+
13
+
14
+ ChatMessages = List[ChatMessage]
15
+
16
+
17
+ async def llm_acall(model_name: str, prompt: str) -> str:
18
+ key = os.getenv("OPENROUTER_API_KEY", "")
19
+ assert key, "Please set OPENROUTER_API_KEY in the environment variables"
20
+ base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
21
+
22
+ messages: ChatMessages = [
23
+ ChatMessage(role="user", content=prompt),
24
+ ]
25
+ client = AsyncOpenAI(base_url=base_url, api_key=key)
26
+ response: ChatCompletionMessage = (
27
+ (
28
+ await client.chat.completions.create(
29
+ model=model_name,
30
+ messages=messages,
31
+ temperature=0.0,
32
+ )
33
+ )
34
+ .choices[0]
35
+ .message
36
+ )
37
+ assert response.content, "Response content is None"
38
+ return response.content
@@ -15,7 +15,12 @@ from .tools.document_qa import document_qa
15
15
  from .tools.md_to_pdf import md_to_pdf
16
16
  from .tools.web_search import web_search, tavily_web_search, exa_web_search, brave_web_search
17
17
  from .tools.visit_webpage import visit_webpage
18
- from .tools.bitflip import propose_improvement_idea, extract_bitflip_info
18
+ from .tools.bitflip import (
19
+ extract_bitflip_info,
20
+ generate_research_proposal,
21
+ score_research_proposals,
22
+ )
23
+
19
24
 
20
25
  load_dotenv()
21
26
 
@@ -53,6 +58,9 @@ def run(
53
58
  server.add_tool(anthology_search)
54
59
  server.add_tool(md_to_pdf)
55
60
  server.add_tool(visit_webpage)
61
+ server.add_tool(extract_bitflip_info)
62
+ server.add_tool(generate_research_proposal)
63
+ server.add_tool(score_research_proposals)
56
64
 
57
65
  if os.getenv("TAVILY_API_KEY"):
58
66
  server.add_tool(tavily_web_search)
@@ -64,8 +72,6 @@ def run(
64
72
  server.add_tool(web_search)
65
73
  if os.getenv("OPENROUTER_API_KEY"):
66
74
  server.add_tool(document_qa)
67
- server.add_tool(propose_improvement_idea)
68
- server.add_tool(extract_bitflip_info)
69
75
 
70
76
  if port is None:
71
77
  port = find_free_port()
@@ -7,7 +7,7 @@ from .document_qa import document_qa
7
7
  from .md_to_pdf import md_to_pdf
8
8
  from .web_search import web_search, tavily_web_search, exa_web_search, brave_web_search
9
9
  from .visit_webpage import visit_webpage
10
- from .bitflip import propose_improvement_idea, extract_bitflip_info
10
+ from .bitflip import extract_bitflip_info, generate_research_proposal, score_research_proposals
11
11
 
12
12
 
13
13
  __all__ = [
@@ -24,6 +24,7 @@ __all__ = [
24
24
  "exa_web_search",
25
25
  "brave_web_search",
26
26
  "visit_webpage",
27
- "propose_improvement_idea",
28
27
  "extract_bitflip_info",
28
+ "generate_research_proposal",
29
+ "score_research_proposals",
29
30
  ]
@@ -0,0 +1,282 @@
1
+ # https://arxiv.org/abs/2504.12976
2
+ # https://web.stanford.edu/class/cs197c/slides/02-literature-search.pdf
3
+
4
+ import json
5
+ import os
6
+ import random
7
+ from typing import List, Optional, Any
8
+
9
+ from pydantic import BaseModel
10
+ from datasets import load_dataset # type: ignore
11
+
12
+ from academia_mcp.tools.arxiv_download import arxiv_download
13
+ from academia_mcp.utils import extract_json, encode_prompt
14
+ from academia_mcp.llm import llm_acall
15
+
16
+
17
+ class ProposalDataset:
18
+ dataset: Optional[List[Any]] = None
19
+
20
+ @classmethod
21
+ def get_dataset(cls) -> List[Any]:
22
+ if cls.dataset is None:
23
+ cls.dataset = list(load_dataset("UniverseTBD/hypogen-dr1")["train"])
24
+ return cls.dataset
25
+
26
+
27
+ EXTRACT_PROMPT = """
28
+ You are a highly advanced research assistant.
29
+ You specialize in reading scientific papers for hypothesis generation and identifying innovative ideas.
30
+
31
+
32
+ ## Example (BERT in NLP)
33
+ Before you begin, let 's revisit the Bit-Flip concept with an example (BERT in NLP):
34
+ - Bit: Traditional NLP models (RNNs, LSTMs) process text sequentially,
35
+ limiting their ability to understand long-range dependencies and fully capture bidirectional context.
36
+ - Flip: Instead, consider entire sentences at once, allowing context from both directions. This helps capture nuanced relationships among words.
37
+ - Spark: Bidirectional context for NLP.
38
+
39
+ ## Framework
40
+ A Bit-Flip inverts a commonly held assumption,
41
+ questioning existing constraints or reapplying techniques to new domains/scales.
42
+ The "Bit" is the prevailing belief, and the "Flip" is the counterargument.
43
+
44
+ ## Guidance for analysis
45
+ 1. Bit (Technical Insight):
46
+ - Provide at least two sentences clearly stating the status quo or conventional approach.
47
+ - Highlight the limitation or problem it creates.
48
+ - Include enough detail so it is self-contained and does not rely on additional context from elsewhere.
49
+ 2. Flip (Innovation):
50
+ - Provide at least two sentences describing the novel approach or perspective.
51
+ - Explain the method or technique that enables this change.
52
+ - Include enough detail so the Flip is understandable on its own.
53
+ 3. Spark (Core Summary):
54
+ - A concise 4-6 word phrase capturing the core idea.
55
+
56
+ Now, consider this research abstract:
57
+ {{abstract}}
58
+
59
+ Your task:
60
+ Identify the Bit, Flip, and Spark from the abstract in a detailed manner:
61
+ - Bit: at least two sentences, with sufficient detail about the conventional approach and its limitation.
62
+ - Flip: at least two sentences, describing the new approach or perspective with enough detail to understand the main technique.
63
+ - Spark: a concise 4-6 word summary of the core idea.
64
+
65
+ Follow these rules:
66
+ - Do not cite the paper itself or its authors.
67
+ - Instead of saying "We/I introduced an idea", just say "An idea was introduced ...".
68
+
69
+ Return only the JSON object in this exact format (no extra text):
70
+ {
71
+ "bit": "Technical limitation or conventional approach, in at least two sentences",
72
+ "flip": "Innovative approach or solution, in at least two sentences",
73
+ "spark": "4-6 word summary"
74
+ }
75
+ """
76
+
77
+ IMPROVEMENT_PROMPT = """
78
+ You are a highly advanced research assistant.
79
+ You specialize in hypothesis generation and identifying innovative ideas.
80
+
81
+ You are given a Bit, which is a technical limitation or conventional approach of some paper.
82
+ Your task is to propose an improvement idea for the Bit called Flip and summarize it in a Spark.
83
+ Do not propose any human annotations or human-in-the-loop, the idea should be automatically verifiable.
84
+ Try to be as specific as possible.
85
+
86
+ {% for example in examples %}
87
+ ## Example {{loop.index}}
88
+ - Bit: {{example["bit"]}}
89
+ - Chain of reasoning: {{example["chain_of_reasoning"]}}
90
+ - Flip: {{example["flip"]}}
91
+ - Spark: {{example["spark"]}}
92
+ {% endfor %}
93
+
94
+ Now, please propose a chain of reasoning that leads to an improvement idea for this Bit:
95
+ {{bit}}
96
+
97
+ {% if additional_context %}Additional context:
98
+ {{additional_context}}{% endif %}
99
+
100
+ Finalize your idea by providing the idea details:
101
+ - Abstract: An abstract that summarizes the proposal in conference format (approximately 250 words).
102
+ - Experiments: A list of experiments that would be conducted to validate the proposal. Ensure these are simple and feasible. Be specific in exactly how you would test the hypothesis, and detail precise algorithmic changes. Include the evaluation metrics you would use.
103
+ - Risks and limitations: A list of potential risks and limitations of the proposal.
104
+
105
+ Return only the JSON object in this exact format (no extra text):
106
+ {
107
+ "chain_of_reasoning": "Chain of reasoning that leads to an improvement idea for this Bit. At least 5 sentences.",
108
+ "flip": "Innovative approach or solution, in at least two sentences",
109
+ "spark": "4-6 word summary",
110
+ "abstract": "An abstract that summarizes the proposal in conference format (approximately 250 words).",
111
+ "experiments": ["...", "..."],
112
+ "risks_and_limitations": "A list of potential risks and limitations of the proposal."
113
+ }
114
+ """
115
+
116
+
117
+ SCORE_PROMPT = """
118
+ You are a highly advanced research assistant.
119
+ You are given a list of research proposals.
120
+ Your task is to score the proposals.
121
+
122
+ Proposals:
123
+ {% for proposal in proposals %}
124
+ - Proposal ID: {{proposal["proposal_id"]}}
125
+ - Spark: {{proposal["spark"]}}
126
+ - Abstract: {{proposal["abstract"]}}
127
+ - Experiments: {{proposal["experiments"]}}
128
+ - Risks and limitations: {{proposal["risks_and_limitations"]}}
129
+ {% endfor %}
130
+
131
+ Here are the criteria:
132
+ - "Strengths": A list of strengths of the proposal.
133
+ - "Weaknesses": A list of weaknesses of the proposal.
134
+ - "Novelty": Is the proposal novel? A rating from 1 to 4 (low, medium, high, very high).
135
+ - "Clarity": Is the proposal clear? A rating from 1 to 4 (low, medium, high, very high).
136
+ - "Significance": Is the proposal significant? A rating from 1 to 4 (low, medium, high, very high).
137
+ - "Feasibility": Is the proposal feasible and easy to implement? A rating from 1 to 4 (low, medium, high, very high).
138
+ - "Soundness": Is the proposal sound? A rating from 1 to 4 (poor, fair, good, excellent).
139
+ - "Overall": A rating from 1 to 10 (very strong reject to award quality).
140
+
141
+ Return only scores for all proposals in this exact format (no extra text):
142
+ [
143
+ {
144
+ "proposal_id": 0,
145
+ "spark": "...",
146
+ "strengths": ["...", "..."],
147
+ "weaknesses": ["...", "..."],
148
+ "novelty": 2,
149
+ "clarity": 2,
150
+ "significance": 2,
151
+ "feasibility": 2,
152
+ "soundness": 2,
153
+ "overall": 5
154
+ },
155
+ ...
156
+ ]
157
+ """
158
+
159
+
160
+ class BitFlipInfo(BaseModel): # type: ignore
161
+ bit: str
162
+ flip: str
163
+ spark: str
164
+
165
+
166
+ class Proposal(BaseModel): # type: ignore
167
+ proposal_id: Optional[int] = None
168
+ flip: str
169
+ spark: str
170
+ abstract: str
171
+ experiments: List[str]
172
+ risks_and_limitations: List[str]
173
+
174
+
175
+ class ProposalScores(BaseModel): # type: ignore
176
+ proposal_id: int
177
+ spark: str
178
+ strengths: List[str]
179
+ weaknesses: List[str]
180
+ novelty: int
181
+ clarity: int
182
+ significance: int
183
+ feasibility: int
184
+ soundness: int
185
+ overall: int
186
+
187
+
188
+ async def extract_bitflip_info(arxiv_id: str) -> str:
189
+ """
190
+ Extracts the Bit-Flip information from the arXiv paper.
191
+
192
+ A Bit-Flip is a technique that inverts a commonly held assumption,
193
+ questioning existing constraints or reapplying techniques to new domains/scales.
194
+ The "Bit" is the prevailing belief, and the "Flip" is the counterargument.
195
+
196
+ Returns a JSON object in this format:
197
+ {
198
+ "bit": "Technical limitation or conventional approach, in at least two sentences",
199
+ "flip": "Innovative approach or solution, in at least two sentences",
200
+ "spark": "4-6 word summary of the core idea"
201
+ }
202
+ Use `json.loads` to deserialize the result if you want to get specific fields.
203
+
204
+ Args:
205
+ arxiv_id: The arXiv ID of the paper to extract the Bit-Flip information from.
206
+ """
207
+ model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
208
+ paper = arxiv_download(arxiv_id)
209
+ abstract = json.loads(paper)["abstract"]
210
+ prompt = encode_prompt(EXTRACT_PROMPT, abstract=abstract)
211
+ content = await llm_acall(model_name=model_name, prompt=prompt)
212
+ result = extract_json(content)
213
+ bitflip_info: BitFlipInfo = BitFlipInfo.model_validate(result)
214
+ return str(bitflip_info.model_dump_json())
215
+
216
+
217
+ async def generate_research_proposal(bit: str, additional_context: str = "") -> str:
218
+ """
219
+ Proposes an improvement idea for the Bit.
220
+
221
+ Args:
222
+ bit: The Bit to propose an improvement idea for. The bit is a technical limitation or conventional approach of some paper.
223
+ additional_context: Additional context to use when proposing the improvement idea.
224
+
225
+ Returns a JSON string with a research proposal in this format:
226
+ {
227
+ "proposal_id": ...,
228
+ "flip": "Innovative approach or solution, in at least two sentences",
229
+ "spark": "4-6 word summary",
230
+ "abstract": "An abstract that summarizes the proposal in conference format (approximately 250 words).",
231
+ "experiments": ["...", "..."],
232
+ "risks_and_limitations": "A list of potential risks and limitations of the proposal."
233
+ }
234
+ Use `json.loads` to deserialize the result if you want to get specific fields.
235
+ """
236
+ model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
237
+ examples = ProposalDataset.get_dataset()[:]
238
+ examples = random.choices(examples, k=4)
239
+
240
+ prompt = encode_prompt(
241
+ IMPROVEMENT_PROMPT, bit=bit, examples=examples, additional_context=additional_context
242
+ )
243
+ content = await llm_acall(model_name=model_name, prompt=prompt)
244
+ result = extract_json(content)
245
+ proposal: Proposal = Proposal.model_validate(result)
246
+ proposal.proposal_id = random.randint(0, 1000000)
247
+ return str(proposal.model_dump_json())
248
+
249
+
250
+ async def score_research_proposals(proposals: List[str]) -> str:
251
+ """
252
+ Scores a list of research proposals.
253
+ Use proposals obtained with the `generate_research_proposal` tool.
254
+
255
+ Returns a JSON string with a list of scores in this format:
256
+ [
257
+ {
258
+ "proposal_id": 0,
259
+ "spark": "...",
260
+ "strengths": ["...", "..."],
261
+ "weaknesses": ["...", "..."],
262
+ "novelty": 2,
263
+ "clarity": 2,
264
+ "significance": 2,
265
+ "feasibility": 2,
266
+ "soundness": 2,
267
+ "overall": 5
268
+ },
269
+ ...
270
+ ]
271
+ Use `json.loads` to deserialize the result if you want to get specific fields.
272
+
273
+ Args:
274
+ proposals: A list of JSON strings with research proposals.
275
+ """
276
+ model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
277
+ proposals = [Proposal.model_validate_json(proposal) for proposal in proposals]
278
+ prompt = encode_prompt(SCORE_PROMPT, proposals=proposals)
279
+ content = await llm_acall(model_name=model_name, prompt=prompt)
280
+ scores = extract_json(content)
281
+ final_scores = [ProposalScores.model_validate(score) for score in scores]
282
+ return json.dumps([s.model_dump() for s in final_scores], ensure_ascii=False)
@@ -1,18 +1,15 @@
1
1
  import os
2
- from typing import List, Any, Dict, cast
2
+ from typing import List, Any, Dict
3
3
  from dotenv import load_dotenv
4
4
 
5
5
  from pydantic import BaseModel
6
- from openai import OpenAI
7
- from openai.types.chat import ChatCompletionMessageParam, ChatCompletionMessage
8
6
 
7
+ from academia_mcp.llm import llm_acall
9
8
 
10
9
  load_dotenv()
11
10
 
12
- SYSTEM_PROMPT = (
13
- "You are a helpful assistant that answers questions about documents accurately and concisely."
14
- )
15
- PROMPT = """Please answer the following questions based solely on the provided document.
11
+ PROMPT = """You are a helpful assistant that answers questions about documents accurately and concisely.
12
+ Please answer the following questions based solely on the provided document.
16
13
  If there is no answer in the document, output "There is no answer in the provided document".
17
14
  First cite ALL relevant document fragments, then provide a final answer.
18
15
  Answer all given questions one by one.
@@ -40,7 +37,7 @@ class ChatMessage(BaseModel): # type: ignore
40
37
  ChatMessages = List[ChatMessage]
41
38
 
42
39
 
43
- def document_qa(
40
+ async def document_qa(
44
41
  document: str,
45
42
  question: str,
46
43
  ) -> str:
@@ -64,33 +61,7 @@ def document_qa(
64
61
  assert question and question.strip(), "Please provide non-empty 'question'"
65
62
  assert document and document.strip(), "Please provide non-empty 'document'"
66
63
 
67
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
68
- key = os.getenv("OPENROUTER_API_KEY", "")
69
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
70
64
  model_name = os.getenv("DOCUMENT_QA_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
71
-
72
- messages: ChatMessages = [
73
- ChatMessage(role="system", content=SYSTEM_PROMPT),
74
- ChatMessage(
75
- role="user",
76
- content=PROMPT.format(question=question, document=document),
77
- ),
78
- ]
79
-
80
- sdk_messages = [
81
- cast(ChatCompletionMessageParam, m.model_dump(exclude_none=True)) for m in messages
82
- ]
83
- client = OpenAI(base_url=base_url, api_key=key)
84
- response: ChatCompletionMessage = (
85
- client.chat.completions.create(
86
- model=model_name,
87
- messages=sdk_messages,
88
- temperature=0.0,
89
- )
90
- .choices[0]
91
- .message
92
- )
93
-
94
- if response.content is None:
95
- raise Exception("Response content is None")
96
- return response.content.strip()
65
+ prompt = PROMPT.format(question=question, document=document)
66
+ content = await llm_acall(model_name=model_name, prompt=prompt)
67
+ return content.strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: academia-mcp
3
- Version: 1.1.4
3
+ Version: 1.2.0
4
4
  Summary: MCP server that provides different tools to search for scientific publications
5
5
  Author-email: Ilya Gusev <phoenixilya@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/IlyaGusev/academia_mcp
@@ -31,6 +31,7 @@ Requires-Dist: pytest>=8.4.1
31
31
  Requires-Dist: openai>=1.97.1
32
32
  Requires-Dist: jinja2>=3.1.6
33
33
  Requires-Dist: datasets>=4.0.0
34
+ Requires-Dist: pytest-asyncio>=1.1.0
34
35
  Dynamic: license-file
35
36
 
36
37
  # Academia MCP
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  academia_mcp/__init__.py
5
5
  academia_mcp/__main__.py
6
6
  academia_mcp/files.py
7
+ academia_mcp/llm.py
7
8
  academia_mcp/py.typed
8
9
  academia_mcp/server.py
9
10
  academia_mcp/utils.py
@@ -19,3 +19,4 @@ pytest>=8.4.1
19
19
  openai>=1.97.1
20
20
  jinja2>=3.1.6
21
21
  datasets>=4.0.0
22
+ pytest-asyncio>=1.1.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "academia-mcp"
7
- version = "1.1.4"
7
+ version = "1.2.0"
8
8
  description = "MCP server that provides different tools to search for scientific publications"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -38,6 +38,7 @@ dependencies = [
38
38
  "openai>=1.97.1",
39
39
  "jinja2>=3.1.6",
40
40
  "datasets>=4.0.0",
41
+ "pytest-asyncio>=1.1.0",
41
42
  ]
42
43
 
43
44
  [project.urls]
@@ -63,3 +64,6 @@ include = ["academia_mcp*"]
63
64
  module = "pydantic.*"
64
65
  follow_imports = "skip"
65
66
 
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ asyncio_default_test_loop_scope = "function"
@@ -0,0 +1,38 @@
1
+ import json
2
+
3
+ from academia_mcp.tools.bitflip import (
4
+ extract_bitflip_info,
5
+ generate_research_proposal,
6
+ score_research_proposals,
7
+ )
8
+
9
+
10
+ async def test_bitflip_extract_info() -> None:
11
+ arxiv_id = "2409.06820"
12
+ result = json.loads(await extract_bitflip_info(arxiv_id))
13
+ assert result is not None
14
+ assert result["bit"]
15
+
16
+
17
+ async def test_bitflip_generate_research_proposal() -> None:
18
+ arxiv_id = "2503.07826"
19
+ bit = json.loads(await extract_bitflip_info(arxiv_id))["bit"]
20
+ result = json.loads(await generate_research_proposal(bit=bit))
21
+ assert result is not None
22
+ assert result["flip"]
23
+
24
+
25
+ async def test_bitflip_score_research_proposals() -> None:
26
+ arxiv_id = "2503.07826"
27
+ bit = json.loads(await extract_bitflip_info(arxiv_id))["bit"]
28
+ proposal1 = await generate_research_proposal(bit=bit)
29
+ proposal2 = await generate_research_proposal(bit=bit)
30
+ scores = json.loads(await score_research_proposals([proposal1, proposal2]))
31
+ assert scores
32
+ assert len(scores) == 2
33
+ assert scores[0]["spark"] is not None
34
+ assert scores[1]["spark"] is not None
35
+ assert scores[0]["strengths"] is not None
36
+ assert scores[1]["strengths"] is not None
37
+ assert scores[0]["weaknesses"] is not None
38
+ assert scores[1]["weaknesses"] is not None
@@ -17,16 +17,16 @@ English constituency parsing both with large and limited training data.
17
17
  """
18
18
 
19
19
 
20
- def test_document_qa_base() -> None:
21
- answer = document_qa(
20
+ async def test_document_qa_base() -> None:
21
+ answer = await document_qa(
22
22
  question="What is BLEU on the WMT 2014 English-to-German translation task?",
23
23
  document=DOCUMENT1,
24
24
  )
25
25
  assert "28.4" in answer
26
26
 
27
27
 
28
- def test_document_qa_real_question() -> None:
28
+ async def test_document_qa_real_question() -> None:
29
29
  questions = "What is the best model for the Russian language according to the role-play benchmark and its final score?"
30
30
  document = arxiv_download("2409.06820")
31
- answer = document_qa(question=questions, document=document)
31
+ answer = await document_qa(question=questions, document=document)
32
32
  assert "4.62" in answer or "4.68" in answer
@@ -6,7 +6,6 @@ from academia_mcp.tools import web_search
6
6
  def test_web_search_base() -> None:
7
7
  result = web_search("autoregressive models path-star graphs", limit=20)
8
8
  assert "The Mystery of the Pathological" in result
9
- assert "The Pitfalls of Next-Token Prediction" in result
10
9
  results = json.loads(result)
11
10
  assert results
12
11
  assert "score" not in str(results)
@@ -1,193 +0,0 @@
1
- # https://arxiv.org/abs/2504.12976
2
- # https://web.stanford.edu/class/cs197c/slides/02-literature-search.pdf
3
-
4
- import json
5
- import os
6
- import random
7
- from typing import List, Dict, Any
8
-
9
- from openai import OpenAI
10
- from pydantic import BaseModel
11
- from openai.types.chat import ChatCompletionMessage
12
- from datasets import load_dataset # type: ignore
13
-
14
- from academia_mcp.tools.arxiv_download import arxiv_download
15
- from academia_mcp.utils import extract_json, encode_prompt
16
-
17
-
18
- EXTRACT_PROMPT = """
19
- You are a highly advanced research assistant.
20
- You specialize in reading scientific papers for hypothesis generation and identifying innovative ideas.
21
-
22
-
23
- ## Example (BERT in NLP)
24
- Before you begin, let 's revisit the Bit-Flip concept with an example (BERT in NLP):
25
- - Bit: Traditional NLP models (RNNs, LSTMs) process text sequentially,
26
- limiting their ability to understand long-range dependencies and fully capture bidirectional context.
27
- - Flip: Instead, consider entire sentences at once, allowing context from both directions. This helps capture nuanced relationships among words.
28
- - Spark: Bidirectional context for NLP.
29
-
30
- ## Framework
31
- A Bit-Flip inverts a commonly held assumption,
32
- questioning existing constraints or reapplying techniques to new domains/scales.
33
- The "Bit" is the prevailing belief, and the "Flip" is the counterargument.
34
-
35
- ## Guidance for analysis
36
- 1. Bit (Technical Insight):
37
- - Provide at least two sentences clearly stating the status quo or conventional approach.
38
- - Highlight the limitation or problem it creates.
39
- - Include enough detail so it is self-contained and does not rely on additional context from elsewhere.
40
- 2. Flip (Innovation):
41
- - Provide at least two sentences describing the novel approach or perspective.
42
- - Explain the method or technique that enables this change.
43
- - Include enough detail so the Flip is understandable on its own.
44
- 3. Spark (Core Summary):
45
- - A concise 4-6 word phrase capturing the core idea.
46
-
47
- Now, consider this research abstract:
48
- {{abstract}}
49
-
50
- Your task:
51
- Identify the Bit, Flip, and Spark from the abstract in a detailed manner:
52
- - Bit: at least two sentences, with sufficient detail about the conventional approach and its limitation.
53
- - Flip: at least two sentences, describing the new approach or perspective with enough detail to understand the main technique.
54
- - Spark: a concise 4-6 word summary of the core idea.
55
-
56
- Follow these rules:
57
- - Do not cite the paper itself or its authors.
58
- - Instead of saying "We/I introduced an idea", just say "An idea was introduced ...".
59
-
60
- Return only the JSON object in this exact format (no extra text):
61
- {
62
- "bit": "Technical limitation or conventional approach, in at least two sentences",
63
- "flip": "Innovative approach or solution, in at least two sentences",
64
- "spark": "4-6 word summary"
65
- }
66
- """
67
-
68
- IMPROVEMENT_PROMPT = """
69
- You are a highly advanced research assistant.
70
- You specialize in hypothesis generation and identifying innovative ideas.
71
-
72
- You are given a Bit, which is a technical limitation or conventional approach of some paper.
73
- Your task is to propose an improvement idea for the Bit called Flip and summarize it in a Spark.
74
- Do not propose any human annotations or human-in-the-loop, the idea should be automatically verifiable.
75
- Try to be as specific as possible.
76
-
77
- {% for example in examples %}
78
- ## Example {{loop.index}}
79
- - Bit: {{example["bit"]}}
80
- - Chain of reasoning: {{example["chain_of_reasoning"]}}
81
- - Flip: {{example["flip"]}}
82
- - Spark: {{example["spark"]}}
83
- {% endfor %}
84
-
85
- Now, please propose a chain of reasoning that leads to an improvement idea for this Bit:
86
- {{bit}}
87
-
88
- Return only the JSON object in this exact format (no extra text):
89
- {
90
- "chain_of_reasoning": "Chain of reasoning that leads to an improvement idea for this Bit. At least 5 sentences.",
91
- "flip": "Innovative approach or solution, in at least two sentences",
92
- "spark": "4-6 word summary"
93
- }
94
- """
95
-
96
-
97
- class ChatMessage(BaseModel): # type: ignore
98
- role: str
99
- content: str | List[Dict[str, Any]]
100
-
101
-
102
- ChatMessages = List[ChatMessage]
103
-
104
-
105
- def extract_bitflip_info(arxiv_id: str) -> str:
106
- """
107
- Extracts the Bit-Flip information from the arXiv paper.
108
-
109
- A Bit-Flip is a technique that inverts a commonly held assumption,
110
- questioning existing constraints or reapplying techniques to new domains/scales.
111
- The "Bit" is the prevailing belief, and the "Flip" is the counterargument.
112
-
113
- Returns a JSON object in this format:
114
- {
115
- "bit": "Technical limitation or conventional approach, in at least two sentences",
116
- "flip": "Innovative approach or solution, in at least two sentences",
117
- "spark": "4-6 word summary of the core idea"
118
- }
119
- Use `json.loads` to deserialize the result if you want to get specific fields.
120
-
121
- Args:
122
- arxiv_id: The arXiv ID of the paper to extract the Bit-Flip information from.
123
- """
124
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
125
- key = os.getenv("OPENROUTER_API_KEY", "")
126
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
127
- model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
128
-
129
- paper = arxiv_download(arxiv_id)
130
- abstract = json.loads(paper)["abstract"]
131
- prompt = encode_prompt(EXTRACT_PROMPT, abstract=abstract)
132
- messages: ChatMessages = [
133
- ChatMessage(role="user", content=prompt),
134
- ]
135
- client = OpenAI(base_url=base_url, api_key=key)
136
- response: ChatCompletionMessage = (
137
- client.chat.completions.create(
138
- model=model_name,
139
- messages=messages,
140
- temperature=0.0,
141
- )
142
- .choices[0]
143
- .message
144
- )
145
- assert response.content, "Response content is None"
146
- result = extract_json(response.content)
147
- return json.dumps(result, ensure_ascii=False)
148
-
149
-
150
- def propose_improvement_idea(arxiv_id: str) -> str:
151
- """
152
- Proposes an improvement idea for the arXiv paper.
153
-
154
- Returns a JSON object in this format:
155
- {
156
- "chain_of_reasoning": "Chain of reasoning that leads to an improvement idea.",
157
- "flip": "Innovative approach or solution",
158
- "spark": "4-6 word summary"
159
- }
160
- Use `json.loads` to deserialize the result if you want to get specific fields.
161
-
162
- Args:
163
- arxiv_id: The arXiv ID of the paper to propose an improvement idea for.
164
- """
165
- base_url = os.getenv("BASE_URL", "https://openrouter.ai/api/v1")
166
- key = os.getenv("OPENROUTER_API_KEY", "")
167
- assert key, "Please set OPENROUTER_API_KEY in the environment variables"
168
- model_name = os.getenv("BITFLIP_MODEL_NAME", "deepseek/deepseek-chat-v3-0324")
169
-
170
- bitflip_info = json.loads(extract_bitflip_info(arxiv_id))
171
- bit = bitflip_info["bit"]
172
-
173
- examples = list(load_dataset("UniverseTBD/hypogen-dr1")["train"])
174
- random.shuffle(examples)
175
- examples = examples[:4]
176
-
177
- prompt = encode_prompt(IMPROVEMENT_PROMPT, bit=bit, examples=examples)
178
- messages: ChatMessages = [
179
- ChatMessage(role="user", content=prompt),
180
- ]
181
- client = OpenAI(base_url=base_url, api_key=key)
182
- response: ChatCompletionMessage = (
183
- client.chat.completions.create(
184
- model=model_name,
185
- messages=messages,
186
- temperature=0.0,
187
- )
188
- .choices[0]
189
- .message
190
- )
191
- assert response.content, "Response content is None"
192
- result = extract_json(response.content)
193
- return json.dumps(result, ensure_ascii=False)
@@ -1,19 +0,0 @@
1
- from academia_mcp.tools.bitflip import extract_bitflip_info, propose_improvement_idea
2
-
3
-
4
- def test_bitflip_extract_info() -> None:
5
- arxiv_id = "2409.06820"
6
- result = extract_bitflip_info(arxiv_id)
7
- assert result is not None
8
- assert "bit" in result
9
- assert "flip" in result
10
- assert "spark" in result
11
-
12
-
13
- def test_bitflip_propose_improvement_idea() -> None:
14
- arxiv_id = "2503.07826"
15
- result = propose_improvement_idea(arxiv_id)
16
- assert result is not None
17
- assert "chain_of_reasoning" in result
18
- assert "flip" in result
19
- assert "spark" in result
File without changes
File without changes
File without changes