
Scientific Papers Summarized for You
- Giacomo Vaccario
- July 13, 2026
Table of Contents
Reading scientific literature is tough. Research papers are dense, heavily technical, and often excruciatingly long. To make matters worse, different readers need completely different takeaways from the exact same paper—a developer needs technical details, while a decision-maker just needs the high-level impact.
The solution? SummaryAgent, an automated framework built using AI agents designed to break down dense research papers into tailormade, digestible insights.
This project was built during “AI for Science Communication” course at ETH Zurich (January 2025) led by Mirko Bischofberger, focusing on using AI to bridge the gap between complex research and accessible information.
The Experimentation Phase
Building a reliable summarizer isn’t as simple as asking a chatbot to “summarize this paper.” Finding the sweet spot required trial and error across models and techniques:
- Models & LLMs: Tested options including ChatGPT, LLaMA 2, and Mistral.
- Prompting Strategies: Experimented with Zero-Shot, Few-Shot, and Chain-of-Thought prompting to force models to reason through complex methodologies before rendering a final summary.
- Tone & Persona Crafting: Fine-tuned prompts to instruct agents to adopt specific language styles and output formats depending on the target audience.
Disclaimer: I decided to make explicit that the summaries were generated by an AI agent, and to make people aware that the summaries are not perfect and may contain errors. Please refer to the original paper for critical research tasks.
Final System Persona
First, we define the persona
You are an expert science communicator. You will receive the full text of a scientific paper.
Your task:
Generate structured output in valid JSON only (no markdown, no explanations).
General rules:
* Focus on results and implications, not background
* Do NOT use em dashes
* Add exactly one spelling mistake per output
* Do NOT use a serial comma
STRICT rules:
* Describe results impersonally (e.g., “The analysis shows…”, “Results indicate…”).
* DO NOT mention authors or use pronouns like “they”
* DO NOT use human actors as grammatical subjects (e.g., authors, researchers, scientists, “they”).
* DO NOT personify the paper or attribute actions to people or groups.
Second, we define the output format
Output JSON schema:
{
"audience_1": {
"summary": "...",
"quote": "..."
},
"audience_2": {
"part_1": "...",
"part_2": "...",
"quote": "..."
},
"audience_3": {
"summary": "...",
"quote": "..."
}
}
Third, we define the requirements for each audience
Requirements:
Audience 1:
* General audience
* ~120 words
* Explain motivation, topic, key results
* Kincaid-Flesh Reading Ease of 60
* Do not add a title to the summary
Audience 2:
Part 1:
* Why it matters for scientists
* ≤100 words
* Career/research implications
* Direct tone ("you may want to...")
Part 2:
* Background, methods, novelty
* ≤100 words
Audience 3:
* Policy/stakeholders
* ~100-120 words
* Focus on implications and applications
Quotes:
* One per audience
* Define the key message (1 sentence)
* Select 1 quote (1-2 sentences, exact text) that directly supports it
Quote must:
* Contain a specific result or conclusion
* Not be background or vague
* Be understandable on its own
* 1-2 sentences
* Must be exact text from the paper
Return ONLY valid JSON.
Note that we added a request to generate valid JSON only, to avoid the model generating any text outside of the JSON structure. This is important for downstream processing and automation.
Automation: Moving Beyond Manual Uploads
Everyone is good at uploading a PDF on a chatbot and asking for a summary. The hard part is automating everything—having a system that can read the PDF, extract the text, summarize it, push it to a database, and display the summary nicely on a front-end interface.
To keep full control over what gets summarized (and keep costs predictable), I opted for a local Python script that checks my current lists of papers and compares it with the list of papers that have already been summarized. If a paper is new, it will be summarized; if it has already been processed, it will be skipped. This file is executed locally and manages the ingestion workflow. The summaries are then pushed to a GitHub repository, which is automatically deployed to the website.
I will past here excerpts of the code that I used to build this system, which is available on GitHub:
I use the HuggingFace API to access LLMs, which allows me to use different models and switch between them easily.
API_URL = "https://router.huggingface.co/v1/chat/completions"
HEADERS = {
"Authorization": f"Bearer {HF_TOKEN}",
"Content-Type": "application/json",
}
PDF_FOLDER = "content/english/publications"
def extract_text(pdf_path):
reader = PdfReader(pdf_path)
text = ""
for page in reader.pages:
text += page.extract_text() or ""
return text[:8000] # keep it safe for API limits
def summarize(text, model="meta-llama/Llama-3.1-8B-Instruct", max_tokens=7000):
def build_payload(model, text):
# Detect Mistral vs chat-based models
is_mistral = "mistral" in model.lower()
if is_mistral:
prompt = f"[INST] {BOT_ROLE}\n\n{text} [/INST]"
return {
"model": model,
"inputs": prompt,
"parameters": {
"temperature": 0.7,
"max_new_tokens": max_tokens
}
}
else:
return {
"model": model,
"messages": [
{"role": "system", "content": BOT_ROLE},
{"role": "user", "content": text}
],
"temperature": 0.7,
"max_tokens": max_tokens,
"response_format": {"type": "json_object"}
}
payload = build_payload(model, text)
response = requests.post(API_URL, headers=HEADERS, json=payload)
print("Status:", response.status_code)
#print(response.text)
response.raise_for_status()
data = response.json()
# Handle different response formats
if "choices" in data:
choice = data["choices"][0]
return (
choice.get("message", {}).get("content") or
choice.get("text")
)
elif "generated_text" in data:
return data["generated_text"]
else:
return str(data)
The script also handles the output formatting, saving the summaries in a structured way that can be easily displayed on a website.
def json_to_html(data):
return f"""
<p>{data['audience_1']['summary']}</p>
<pre>
{data['audience_1']['quote']}
</pre>
<details class="custom-details">
<summary><strong>Why This Matters for Scientists</strong></summary>
<p>{data['audience_2']['part_1']}</p>
</details>
<details class="custom-details">
<summary><strong>Quick Technical Overview</strong></summary>
<p>{data['audience_2']['part_2']}</p>
<pre>
{data['audience_2']['quote']}
</pre>
</details>
<details class="custom-details">
<summary><strong>Summary for Policy Makers</strong></summary>
<p>{data['audience_3']['summary']}</p>
<pre>
{data['audience_3']['quote']}
</pre>
</details>
<details class="custom-details" data-sentinel="ai-disclaimer-v1">
<summary><strong>Disclaimer</strong></summary>
<p>The above summaries were generated with the assistance of an AI system.</p>
</details>
"""
Disclaimer: I decided to make explicit that the summaries were generated by an AI agent, and to make people aware that the summaries are not perfect and may contain errors.
def main():
for root, dirs, files in os.walk(PDF_FOLDER):
for file in files:
if file.lower().endswith(".pdf"):
# check if index.md already exists
if os.path.exists(os.path.join(root, "index.md")):
# check if it contains the ai-disclaimer
summary_is_done = has_sentinel(os.path.join(root, "index.md"))
if summary_is_done:
print(f"Summary already exists for {file}, skipping...")
continue
path = os.path.join(root, file)
print(f"Processing {file}...")
text = extract_text(path)
summary = summarize(text)
title = file.replace(".pdf", "")
save_markdown(summary, root, filename="index.md")
# os.system("git add .")
# os.system('git commit -m "auto content update"')
# os.system("git push")
if __name__ == "__main__":
main()
Note: The processing script automatically handles
git pushfor you, pushing updated summaries and database logs seamlessly once processing is complete. I have commented out the git commands in the code snippet above to avoid accidental pushes during testing.


