
Research Lookup
- 861 installs
- 32.7k repo stars
- Updated August 3, 2026
- k-dense-ai/scientific-agent-skills
research-lookup is a scientific agent skill that routes research queries across parallel-cli web search, the Parallel Chat API for deep research, and Perplexity sonar-pro-search for academic papers so developers get curr
About
research-lookup is a MIT-licensed skill from k-dense-ai/scientific-agent-skills that automatically routes research queries to the best backend available. parallel-cli provides the primary fast web search path; the Parallel Chat API handles deep research when PARALLEL_API_KEY is set; and Perplexity sonar-pro-search via openrouter.ai covers academic paper searches when OPENROUTER_API_KEY is configured. Query text is transmitted to api.parallel.ai and, for academic searches, to openrouter.ai, so developers must supply the appropriate API keys. The skill supports finding papers, gathering research data, and verifying scientific information inside agent sessions with Read, Write, Edit, and Bash tool access. Developers reach for research-lookup during literature reviews, hypothesis checks, or when agent workflows need cited, up-to-date scientific sources.
- Intelligent backend routing across parallel-cli, Parallel Chat API, and Perplexity Sonar Pro
- Specialized for finding papers and verifying scientific information
- Primary fast web search with optional deep and academic modes
- Transmits queries only to configured backends (parallel.ai and/or openrouter.ai)
- Works with parallel-cli as the required base
Research Lookup by the numbers
- 861 all-time installs (skills.sh)
- +42 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,269 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/k-dense-ai/scientific-agent-skills --skill research-lookupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 861 |
|---|---|
| repo stars | ★ 32.7k |
| Last updated | August 3, 2026 |
| Repository | k-dense-ai/scientific-agent-skills ↗ |
How do you find current scientific papers inside a coding agent?
Get accurate, up-to-date scientific papers, web data, and research summaries without switching tools.
Who is it for?
Developers and researchers who need literature lookup and scientific fact-checking inside Claude or Codex sessions with parallel-cli and optional API keys.
Skip if: Teams that need bibliographic reference manager exports, PDF full-text extraction pipelines, or wet-lab protocol generation without web search APIs.
When should I use this skill?
The user needs current scientific papers, research data verification, or deep literature search during an agent coding or analysis session.
What you get
Research summaries, paper references, and verified scientific data routed from parallel-cli, Parallel Chat API, or Perplexity academic search backends.
- Research summaries
- Paper references and verified scientific data
By the numbers
- Routes across 3 backends: parallel-cli, Parallel Chat API, and Perplexity sonar-pro-search
Files
Research Information Lookup
Overview
This skill provides real-time research information lookup with intelligent backend routing:
- parallel-cli search (parallel-web skill): Primary and default backend for all research queries. Fast, cost-effective web search with academic source prioritization. Uses
parallel-cli searchwith--include-domainsfor scholarly sources. - Parallel Chat API (
coremodel): Secondary backend for complex, multi-source deep research requiring extended synthesis (60s-5min latency). Use only when explicitly needed. - Perplexity sonar-pro-search (via OpenRouter): Used only for academic-specific paper searches where scholarly database access is critical.
The skill automatically detects query type and routes to the optimal backend.
When to Use This Skill
Use this skill when you need:
- Current Research Information: Latest studies, papers, and findings
- Literature Verification: Check facts, statistics, or claims against current research
- Background Research: Gather context and supporting evidence for scientific writing
- Citation Sources: Find relevant papers and studies to cite
- Technical Documentation: Look up specifications, protocols, or methodologies
- Market/Industry Data: Current statistics, trends, competitive intelligence
- Recent Developments: Emerging trends, breakthroughs, announcements
Visual Enhancement with Scientific Schematics
When creating documents with this skill, always consider adding scientific diagrams and schematics to enhance visual communication.
If your document does not already contain schematics or diagrams:
- Use the scientific-schematics skill to generate AI-powered publication-quality diagrams
- Simply describe your desired diagram in natural language
python scripts/generate_schematic.py "your diagram description" -o figures/output.png---
Automatic Backend Selection
The skill automatically routes queries to the best backend based on content:
Routing Logic
Query arrives
|
+-- Contains academic keywords? (papers, DOI, journal, peer-reviewed, etc.)
| YES --> Perplexity sonar-pro-search (academic search mode)
|
+-- Needs deep multi-source synthesis? (user says "deep research", "exhaustive")
| YES --> Parallel Chat API (core model, 60s-5min)
|
+-- Everything else (general research, market data, technical info, analysis)
--> parallel-cli search (fast, default)Default: parallel-cli search (parallel-web skill)
Primary backend for all standard research queries. Fast, cost-effective, and supports academic source prioritization.
For scientific/technical queries, run two searches to ensure academic coverage:
# 1. Academic-focused search
parallel-cli search "your research query" -q "keyword1" -q "keyword2" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--include-domains "scholar.google.com,arxiv.org,pubmed.ncbi.nlm.nih.gov,semanticscholar.org,biorxiv.org,medrxiv.org,ncbi.nlm.nih.gov,nature.com,science.org,ieee.org,acm.org,springer.com,wiley.com,cell.com,pnas.org,nih.gov" \
-o sources/research_<topic>-academic.json
# 2. General search (catches non-academic sources)
parallel-cli search "your research query" -q "keyword1" -q "keyword2" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_<topic>-general.jsonOptions:
--after-date YYYY-MM-DDfor time-sensitive queries--include-domains domain1.com,domain2.comto limit to specific sources
Merge results, leading with academic sources. For non-scientific queries, a single general search is sufficient.
All other queries route here by default, including:
- General research questions
- Market and industry analysis
- Technical information and documentation
- Current events and recent developments
- Comparative analysis
- Statistical data retrieval
- Fact-checking and verification
Academic Keywords (Routes to Perplexity)
Queries containing these terms are routed to Perplexity for academic-focused search:
- Paper finding:
find papers,find articles,research papers on,published studies - Citations:
cite,citation,doi,pubmed,pmid - Academic sources:
peer-reviewed,journal article,scholarly,arxiv,preprint - Review types:
systematic review,meta-analysis,literature search - Paper quality:
foundational papers,seminal papers,landmark papers,highly cited
Deep Research (Routes to Parallel Chat API)
Only used when the user explicitly requests deep, exhaustive, or comprehensive research. Much slower and more expensive than parallel-cli search.
Manual Override
You can force a specific backend:
# Force parallel-cli search (fast web search)
parallel-cli search "your query" -q "keyword" --json --max-results 10 -o sources/research_<topic>.json
# Force Parallel Deep Research (slow, exhaustive)
python research_lookup.py "your query" --force-backend parallel
# Force Perplexity academic search
python research_lookup.py "your query" --force-backend perplexity---
Core Capabilities
1. General Research Queries (parallel-cli search — DEFAULT)
Primary backend. Fast, cost-effective web search with academic source prioritization via the parallel-web skill.
Query Examples:
- "Recent advances in CRISPR gene editing 2025"
- "Compare mRNA vaccines vs traditional vaccines for cancer treatment"
- "AI adoption in healthcare industry statistics"
- "Global renewable energy market trends and projections"
- "Explain the mechanism underlying gut microbiome and depression"# Example: research on CRISPR advances
parallel-cli search "Recent advances in CRISPR gene editing 2025" \
-q "CRISPR" -q "gene editing" -q "2025" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--include-domains "scholar.google.com,arxiv.org,pubmed.ncbi.nlm.nih.gov,nature.com,science.org,cell.com,pnas.org,nih.gov" \
-o sources/research_crispr_advances-academic.json
parallel-cli search "Recent advances in CRISPR gene editing 2025" \
-q "CRISPR" -q "gene editing" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_crispr_advances-general.jsonResponse includes:
- Synthesized findings with inline citations from search results
- Academic sources prioritized (peer-reviewed, preprints)
- Specific facts, numbers, and dates
- Sources section listing all referenced URLs grouped by type
2. Academic Paper Search (Perplexity sonar-pro-search)
Used for academic-specific queries. Prioritizes scholarly databases and peer-reviewed sources. Use when queries specifically ask for papers, citations, or DOIs.
Query Examples:
- "Find papers on transformer attention mechanisms in NeurIPS 2024"
- "Foundational papers on quantum error correction"
- "Systematic review of immunotherapy in non-small cell lung cancer"
- "Cite the original BERT paper and its most influential follow-ups"
- "Published studies on CRISPR off-target effects in clinical trials"Response includes:
- Summary of key findings from academic literature
- 5-8 high-quality citations with authors, titles, journals, years, DOIs
- Citation counts and venue tier indicators
- Key statistics and methodology highlights
- Research gaps and future directions
3. Deep Research (Parallel Chat API — on request only)
Used only when user explicitly requests deep/exhaustive research. Provides comprehensive, multi-source synthesis via the Chat API (core model). 60s-5min latency.
Query Examples:
- "Deep research on the current state of quantum computing error correction"
- "Exhaustive analysis of mRNA vaccine platforms for cancer immunotherapy"4. Technical and Methodological Information
Use parallel-cli search (default) for quick lookups:
parallel-cli search "Western blot protocol for protein detection" \
-q "western blot" -q "protocol" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_western_blot.json5. Statistical and Market Data
Use parallel-cli search (default) for current data:
parallel-cli search "Global AI market size and growth projections 2025" \
-q "AI market" -q "statistics" -q "growth" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--after-date 2024-01-01 \
-o sources/research_ai_market.json---
Paper Quality and Popularity Prioritization
CRITICAL: When searching for papers, ALWAYS prioritize high-quality, influential papers.
Citation-Based Ranking
| Paper Age | Citation Threshold | Classification |
|---|---|---|
| 0-3 years | 20+ citations | Noteworthy |
| 0-3 years | 100+ citations | Highly Influential |
| 3-7 years | 100+ citations | Significant |
| 3-7 years | 500+ citations | Landmark Paper |
| 7+ years | 500+ citations | Seminal Work |
| 7+ years | 1000+ citations | Foundational |
Venue Quality Tiers
Tier 1 - Premier Venues (Always prefer):
- General Science: Nature, Science, Cell, PNAS
- Medicine: NEJM, Lancet, JAMA, BMJ
- Field-Specific: Nature Medicine, Nature Biotechnology, Nature Methods
- Top CS/AI: NeurIPS, ICML, ICLR, ACL, CVPR
Tier 2 - High-Impact Specialized (Strong preference):
- Journals with Impact Factor > 10
- Top conferences in subfields (EMNLP, NAACL, ECCV, MICCAI)
Tier 3 - Respected Specialized (Include when relevant):
- Journals with Impact Factor 5-10
---
Technical Integration
Prerequisites
# Primary backend (parallel-cli) - REQUIRED
# Install parallel-cli if not already available:
curl -fsSL https://parallel.ai/install.sh | bash
# Or: uv tool install "parallel-web-tools[cli]"
# Authenticate:
parallel-cli auth
# Or: export PARALLEL_API_KEY="your_parallel_api_key"Environment Variables
# Primary backend (parallel-cli search) - REQUIRED
export PARALLEL_API_KEY="your_parallel_api_key"
# Deep research backend (Parallel Chat API) - optional, for deep research only
# Uses the same PARALLEL_API_KEY
# Academic search backend (Perplexity) - optional, for academic paper queries
export OPENROUTER_API_KEY="your_openrouter_api_key"API Specifications
parallel-cli search (PRIMARY):
- Command:
parallel-cli searchwith--jsonoutput - Latency: 2-10 seconds (fast)
- Output: JSON with title, URL, publish_date, excerpts
- Academic domains: Use
--include-domainsfor scholarly sources - Saves results:
-o filename.jsonfor follow-up and reproducibility
Parallel Chat API (deep research only):
- Endpoint:
https://api.parallel.ai(OpenAI SDK compatible) - Model:
core(60s-5min latency, complex multi-source synthesis) - Output: Markdown text with inline citations
- Citations: Research basis with URLs, reasoning, and confidence levels
- Rate limits: 300 req/min
- Python package:
openai
Perplexity sonar-pro-search (academic only):
- Model:
perplexity/sonar-pro-search(via OpenRouter) - Search mode: Academic (prioritizes peer-reviewed sources)
- Search context: High (comprehensive research)
- Response time: 5-15 seconds
Command-Line Usage
# Fast web search via parallel-cli (DEFAULT — recommended) — ALWAYS save to sources/
parallel-cli search "your query" -q "keyword1" -q "keyword2" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_<topic>.json
# Academic-focused search via parallel-cli — ALWAYS save to sources/
parallel-cli search "your query" -q "keyword1" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--include-domains "scholar.google.com,arxiv.org,pubmed.ncbi.nlm.nih.gov,semanticscholar.org,biorxiv.org,medrxiv.org,nature.com,science.org,cell.com,pnas.org,nih.gov" \
-o sources/research_<topic>-academic.json
# Time-sensitive search via parallel-cli
parallel-cli search "your query" -q "keyword" \
--json --max-results 10 --after-date 2024-01-01 \
-o sources/research_<topic>.json
# Extract full content from a specific URL (use parallel-web extract)
parallel-cli extract "https://example.com/paper" --json
# Force Parallel Deep Research (slow, exhaustive) — via research_lookup.py
python research_lookup.py "your query" --force-backend parallel -o sources/research_<topic>.md
# Force Perplexity academic search — via research_lookup.py
python research_lookup.py "your query" --force-backend perplexity -o sources/papers_<topic>.md
# Auto-routed via research_lookup.py (legacy) — ALWAYS save to sources/
python research_lookup.py "your query" -o sources/research_YYYYMMDD_HHMMSS_<topic>.md
# Batch queries via research_lookup.py — ALWAYS save to sources/
python research_lookup.py --batch "query 1" "query 2" "query 3" -o sources/batch_research_<topic>.md---
MANDATORY: Save All Results to Sources Folder
Every research-lookup result MUST be saved to the project's `sources/` folder.
This is non-negotiable. Research results are expensive to obtain and critical for reproducibility.
Saving Rules
| Backend | -o Flag Target | Filename Pattern |
|---|---|---|
| parallel-cli search (default) | sources/research_<topic>.json | research_<brief_topic>.json or research_<brief_topic>-academic.json |
| Parallel Deep Research | sources/research_<topic>.md | research_YYYYMMDD_HHMMSS_<brief_topic>.md |
| Perplexity (academic) | sources/papers_<topic>.md | papers_YYYYMMDD_HHMMSS_<brief_topic>.md |
| Batch queries | sources/batch_<topic>.md | batch_research_YYYYMMDD_HHMMSS_<brief_topic>.md |
How to Save
CRITICAL: Every search MUST save results to the `sources/` folder using the `-o` flag.
CRITICAL: Saved files MUST preserve all citations, source URLs, and DOIs.
# parallel-cli search (DEFAULT) — save JSON to sources/
parallel-cli search "Recent advances in CRISPR gene editing 2025" \
-q "CRISPR" -q "gene editing" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--include-domains "scholar.google.com,arxiv.org,pubmed.ncbi.nlm.nih.gov,nature.com,science.org,cell.com,pnas.org,nih.gov" \
-o sources/research_crispr_advances-academic.json
parallel-cli search "Recent advances in CRISPR gene editing 2025" \
-q "CRISPR" -q "gene editing" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_crispr_advances-general.json
# Academic paper search via Perplexity — save to sources/
python research_lookup.py "Find papers on transformer attention mechanisms in NeurIPS 2024" \
-o sources/papers_20250217_143500_transformer_attention.md
# Deep research via Parallel Chat API — save to sources/
python research_lookup.py "AI regulation landscape" --force-backend parallel \
-o sources/research_20250217_144000_ai_regulation.md
# Batch queries — save to sources/
python research_lookup.py --batch "mRNA vaccines efficacy" "mRNA vaccines safety" \
-o sources/batch_research_20250217_144500_mrna_vaccines.mdCitation Preservation in Saved Files
Each output format preserves citations differently:
| Format | Citations Included | When to Use |
|---|---|---|
| parallel-cli JSON (default) | Full result objects: title, url, publish_date, excerpts | Standard use — structured, parseable, fast |
| Text (research_lookup.py) | Sources (N): section with [title] (date) + URL + Additional References (N): with DOIs and academic URLs | Deep research / Perplexity — human-readable |
JSON (--json via research_lookup.py) | Full citation objects: url, title, date, snippet, doi, type | When you need maximum citation metadata from deep research |
For parallel-cli search, saved JSON files include: full search results with title, URL, publish date, and content excerpts for each result. For Parallel Chat API backend, saved files include: research report + Sources list (title, URL) + Additional References (DOIs, academic URLs). For Perplexity backend, saved files include: academic summary + Sources list (title, date, URL, snippet) + Additional References (DOIs, academic URLs).
Use `--json` when you need to:
- Parse citation metadata programmatically
- Preserve full DOI and URL data for BibTeX generation
- Maintain the structured citation objects for cross-referencing
Why Save Everything
1. Reproducibility: Every citation and claim can be traced back to its raw research source 2. Context Window Recovery: If context is compacted, saved results can be re-read without re-querying 3. Audit Trail: The sources/ folder documents exactly how all research information was gathered 4. Reuse Across Sections: Multiple sections can reference the same saved research without duplicate queries 5. Cost Efficiency: Check sources/ for existing results before making new API calls 6. Peer Review Support: Reviewers can verify the research backing every citation
Before Making a New Query, Check Sources First
Before calling research_lookup.py, check if a relevant result already exists:
ls sources/ # Check existing saved resultsIf a prior lookup covers the same topic, re-read the saved file instead of making a new API call.
Logging
When saving research results, always log:
[HH:MM:SS] SAVED: Research lookup to sources/research_20250217_143000_crispr_advances.md (3,800 words, 8 citations)
[HH:MM:SS] SAVED: Paper search to sources/papers_20250217_143500_transformer_attention.md (6 papers found)---
Integration with Scientific Writing
This skill enhances scientific writing by providing:
1. Literature Review Support: Gather current research for introduction and discussion — save to `sources/` 2. Methods Validation: Verify protocols against current standards — save to `sources/` 3. Results Contextualization: Compare findings with recent similar studies — save to `sources/` 4. Discussion Enhancement: Support arguments with latest evidence — save to `sources/` 5. Citation Management: Provide properly formatted citations — save to `sources/`
Complementary Tools
| Task | Tool |
|---|---|
| General web search (fast) | parallel-cli search (built into this skill) |
| Academic-focused web search | parallel-cli search --include-domains (built into this skill) |
| URL content extraction | parallel-cli extract (parallel-web skill) |
| Deep research (exhaustive) | research-lookup via Parallel Chat API or parallel-web deep research |
| Academic paper search | research-lookup (auto-routes to Perplexity) |
| Google Scholar search | citation-management skill |
| PubMed search | citation-management skill |
| DOI to BibTeX | citation-management skill |
| Metadata verification | parallel-cli extract (parallel-web skill) |
---
Error Handling and Limitations
Known Limitations:
- parallel-cli search: Requires
parallel-clito be installed and authenticated - Parallel Chat API (core model): Complex queries may take up to 5 minutes
- Perplexity: Information cutoff, may not access full text behind paywalls
- All backends: Cannot access proprietary or restricted databases
Fallback Behavior:
- If
parallel-cliis not found, install withcurl -fsSL https://parallel.ai/install.sh | bashoruv tool install "parallel-web-tools[cli]" - If parallel-cli search returns insufficient results, fall back to Perplexity or Parallel Chat API
- If the selected backend's API key is missing, tries the other backend
- If all backends fail, returns structured error response
- Rephrase queries for better results if initial response is insufficient
---
Usage Examples
Example 1: General Research (Routes to parallel-cli search)
Query: "Recent advances in transformer attention mechanisms 2025"
Backend: parallel-cli search (default, fast)
Commands:
parallel-cli search "Recent advances in transformer attention mechanisms 2025" \
-q "transformer" -q "attention" -q "2025" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--include-domains "arxiv.org,semanticscholar.org,nature.com,science.org,ieee.org,acm.org" \
-o sources/research_transformer_attention-academic.json
parallel-cli search "Recent advances in transformer attention mechanisms 2025" \
-q "transformer" -q "attention" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
-o sources/research_transformer_attention-general.jsonResponse: Synthesized findings with inline citations from academic and general sources, covering recent papers, key innovations, and performance benchmarks.
Example 2: Academic Paper Search (Routes to Perplexity)
Query: "Find papers on CRISPR off-target effects in clinical trials"
Backend: Perplexity sonar-pro-search (academic mode)
Response: Curated list of 5-8 high-impact papers with full citations, DOIs, citation counts, and venue tier indicators.
Example 3: Comparative Analysis (Routes to parallel-cli search)
Query: "Compare and contrast mRNA vaccines vs traditional vaccines for cancer treatment"
Backend: parallel-cli search (default, fast)
Response: Synthesized comparison from multiple web sources with inline citations, structured analysis, and evidence quality notes.
Example 4: Market Data (Routes to parallel-cli search)
Query: "Global AI adoption in healthcare statistics 2025"
Backend: parallel-cli search (default, fast)
parallel-cli search "Global AI adoption in healthcare statistics 2025" \
-q "AI healthcare" -q "adoption statistics" \
--json --max-results 10 --excerpt-max-chars-total 27000 \
--after-date 2024-01-01 \
-o sources/research_ai_healthcare_adoption.jsonResponse: Current market data, adoption rates, growth projections, and regional analysis with source citations.
---
Summary
This skill serves as the primary research interface with intelligent tri-backend routing:
- parallel-cli search (default): Fast, cost-effective web search with academic source prioritization via the parallel-web skill
- Parallel Chat API (
coremodel): Deep, exhaustive multi-source synthesis (on explicit request only) - Perplexity sonar-pro-search: Academic-specific paper searches only
- Automatic routing: Detects query type and routes to the optimal backend
- Manual override: Force any backend when needed
- Academic prioritization: Two-search pattern ensures scholarly sources surface for scientific queries
#!/usr/bin/env python3
"""
Example usage of the Research Lookup skill with automatic model selection.
This script demonstrates:
1. Automatic model selection based on query complexity
2. Manual model override options
3. Batch query processing
4. Integration with scientific writing workflows
"""
import os
from research_lookup import ResearchLookup
def example_automatic_selection():
"""Demonstrate automatic model selection."""
print("=" * 80)
print("EXAMPLE 1: Automatic Model Selection")
print("=" * 80)
print()
research = ResearchLookup()
# Simple lookup - will use Sonar Pro Search
query1 = "Recent advances in CRISPR gene editing 2024"
print(f"Query: {query1}")
print(f"Expected model: Sonar Pro Search (fast lookup)")
result1 = research.lookup(query1)
print(f"Actual model: {result1.get('model')}")
print()
# Complex analysis - will use Sonar Reasoning Pro
query2 = "Compare and contrast the efficacy of mRNA vaccines versus traditional vaccines"
print(f"Query: {query2}")
print(f"Expected model: Sonar Reasoning Pro (analytical)")
result2 = research.lookup(query2)
print(f"Actual model: {result2.get('model')}")
print()
def example_manual_override():
"""Demonstrate manual model override."""
print("=" * 80)
print("EXAMPLE 2: Manual Model Override")
print("=" * 80)
print()
# Force Sonar Pro Search for budget-constrained rapid lookup
research_pro = ResearchLookup(force_model='pro')
query = "Explain the mechanism of CRISPR-Cas9"
print(f"Query: {query}")
print(f"Forced model: Sonar Pro Search")
result = research_pro.lookup(query)
print(f"Model used: {result.get('model')}")
print()
# Force Sonar Reasoning Pro for critical analysis
research_reasoning = ResearchLookup(force_model='reasoning')
print(f"Query: {query}")
print(f"Forced model: Sonar Reasoning Pro")
result = research_reasoning.lookup(query)
print(f"Model used: {result.get('model')}")
print()
def example_batch_queries():
"""Demonstrate batch query processing."""
print("=" * 80)
print("EXAMPLE 3: Batch Query Processing")
print("=" * 80)
print()
research = ResearchLookup()
# Mix of simple and complex queries
queries = [
"Recent clinical trials for Alzheimer's disease", # Sonar Pro Search
"Compare deep learning vs traditional ML in drug discovery", # Sonar Reasoning Pro
"Statistical power analysis methods", # Sonar Pro Search
]
print("Processing batch queries...")
print("Each query will automatically select the appropriate model")
print()
results = research.batch_lookup(queries, delay=1.0)
for i, result in enumerate(results):
print(f"Query {i+1}: {result['query'][:50]}...")
print(f" Model: {result.get('model')}")
print(f" Type: {result.get('model_type')}")
print()
def example_scientific_writing_workflow():
"""Demonstrate integration with scientific writing workflow."""
print("=" * 80)
print("EXAMPLE 4: Scientific Writing Workflow")
print("=" * 80)
print()
research = ResearchLookup()
# Literature review phase - use Pro for breadth
print("PHASE 1: Literature Review (Breadth)")
lit_queries = [
"Recent papers on machine learning in genomics 2024",
"Clinical applications of AI in radiology",
"RNA sequencing analysis methods"
]
for query in lit_queries:
print(f" - {query}")
# These will automatically use Sonar Pro Search
print()
# Discussion phase - use Reasoning Pro for synthesis
print("PHASE 2: Discussion (Synthesis & Analysis)")
discussion_queries = [
"Compare the advantages and limitations of different ML approaches in genomics",
"Explain the relationship between model interpretability and clinical adoption",
"Analyze the ethical implications of AI in medical diagnosis"
]
for query in discussion_queries:
print(f" - {query}")
# These will automatically use Sonar Reasoning Pro
print()
def main():
"""Run all examples (requires OPENROUTER_API_KEY to be set)."""
if not os.getenv("OPENROUTER_API_KEY"):
print("Note: Set OPENROUTER_API_KEY environment variable to run live queries")
print("These examples show the structure without making actual API calls")
print()
# Uncomment to run examples (requires API key)
# example_automatic_selection()
# example_manual_override()
# example_batch_queries()
# example_scientific_writing_workflow()
# Show complexity assessment without API calls
print("=" * 80)
print("COMPLEXITY ASSESSMENT EXAMPLES (No API calls required)")
print("=" * 80)
print()
os.environ.setdefault("OPENROUTER_API_KEY", "test")
research = ResearchLookup()
test_queries = [
("Recent CRISPR studies", "pro"),
("Compare CRISPR vs TALENs", "reasoning"),
("Explain how CRISPR works", "reasoning"),
("Western blot protocol", "pro"),
("Pros and cons of different sequencing methods", "reasoning"),
]
for query, expected in test_queries:
complexity = research._assess_query_complexity(query)
model_name = "Sonar Reasoning Pro" if complexity == "reasoning" else "Sonar Pro Search"
status = "✓" if complexity == expected else "✗"
print(f"{status} '{query}'")
print(f" → {model_name}")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Research Lookup Tool for Claude Code
Performs research queries using Perplexity Sonar Pro Search via OpenRouter.
"""
import os
import sys
import json
from typing import Dict, List, Optional
# Import the main research lookup class
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts'))
from research_lookup import ResearchLookup
def format_response(result: Dict) -> str:
"""Format the research result for display."""
if not result["success"]:
return f"❌ Research lookup failed: {result['error']}"
response = result["response"]
citations = result["citations"]
sources = result.get("sources", [])
# Format the output for Claude Code
output = f"""🔍 **Research Results**
**Query:** {result['query']}
**Model:** {result['model']}
**Timestamp:** {result['timestamp']}
**Note:** Results prioritized by citation count, venue prestige, and author reputation
---
{response}
"""
# Display API-provided sources with venue/citation info
if sources:
output += f"\n📚 **Sources ({len(sources)}):**\n"
output += "_Prioritized by venue quality and citation impact_\n\n"
for i, source in enumerate(sources, 1):
title = source.get("title", "Untitled")
url = source.get("url", "")
date = source.get("date", "")
snippet = source.get("snippet", "")
# Format source entry with available metadata
date_str = f" ({date})" if date else ""
output += f"{i}. **{title}**{date_str}\n"
# Add venue indicator if detectable from URL
venue_indicator = _detect_venue_tier(url)
if venue_indicator:
output += f" 📊 Venue: {venue_indicator}\n"
if url:
output += f" 🔗 {url}\n"
if snippet:
output += f" _{snippet[:150]}{'...' if len(snippet) > 150 else ''}_\n"
output += "\n"
# Display extracted citations (DOIs, etc.)
if citations:
doi_citations = [c for c in citations if c.get("type") == "doi"]
url_citations = [c for c in citations if c.get("type") == "url"]
if doi_citations:
output += f"\n🔗 **DOI References ({len(doi_citations)}):**\n"
for i, citation in enumerate(doi_citations, 1):
output += f"{i}. DOI: {citation.get('doi', '')} → {citation.get('url', '')}\n"
if url_citations:
output += f"\n🌐 **Additional URLs ({len(url_citations)}):**\n"
for i, citation in enumerate(url_citations, 1):
url = citation.get('url', '')
venue = _detect_venue_tier(url)
venue_str = f" [{venue}]" if venue else ""
output += f"{i}. {url}{venue_str}\n"
if result.get("usage"):
usage = result["usage"]
output += f"\n**Usage:** {usage.get('total_tokens', 'N/A')} tokens"
return output
def _detect_venue_tier(url: str) -> Optional[str]:
"""Detect venue tier from URL to indicate source quality."""
if not url:
return None
url_lower = url.lower()
# Tier 1 - Premier venues
tier1_indicators = {
"nature.com": "Nature (Tier 1)",
"science.org": "Science (Tier 1)",
"cell.com": "Cell Press (Tier 1)",
"nejm.org": "NEJM (Tier 1)",
"thelancet.com": "Lancet (Tier 1)",
"jamanetwork.com": "JAMA (Tier 1)",
"pnas.org": "PNAS (Tier 1)",
}
# Tier 2 - High-impact specialized
tier2_indicators = {
"neurips.cc": "NeurIPS (Tier 2 - Top ML)",
"icml.cc": "ICML (Tier 2 - Top ML)",
"openreview.net": "Top ML Conference (Tier 2)",
"aacrjournals.org": "AACR Journals (Tier 2)",
"ahajournals.org": "AHA Journals (Tier 2)",
"bloodjournal.org": "Blood (Tier 2)",
"jci.org": "JCI (Tier 2)",
}
# Tier 3 - Respected academic sources
tier3_indicators = {
"springer.com": "Springer",
"wiley.com": "Wiley",
"elsevier.com": "Elsevier",
"oup.com": "Oxford University Press",
"arxiv.org": "arXiv (Preprint)",
"biorxiv.org": "bioRxiv (Preprint)",
"medrxiv.org": "medRxiv (Preprint)",
"pubmed": "PubMed",
"ncbi.nlm.nih.gov": "NCBI/PubMed",
"ieee.org": "IEEE",
"acm.org": "ACM",
}
for domain, label in tier1_indicators.items():
if domain in url_lower:
return label
for domain, label in tier2_indicators.items():
if domain in url_lower:
return label
for domain, label in tier3_indicators.items():
if domain in url_lower:
return label
return None
def main():
"""Main entry point for Claude Code tool."""
# Check for API key
if not os.getenv("OPENROUTER_API_KEY"):
print("❌ Error: OPENROUTER_API_KEY environment variable not set")
print("Please set it in your .env file or export it:")
print(" export OPENROUTER_API_KEY='your_openrouter_api_key'")
return 1
# Get query from command line arguments
if len(sys.argv) < 2:
print("❌ Error: No query provided")
print("Usage: python lookup.py 'your research query here'")
return 1
query = " ".join(sys.argv[1:])
try:
# Initialize research tool
research = ResearchLookup()
# Perform lookup
print(f"🔍 Researching: {query}")
result = research.lookup(query)
# Format and output result
formatted_output = format_response(result)
print(formatted_output)
# Return success code
return 0 if result["success"] else 1
except Exception as e:
print(f"❌ Error: {str(e)}")
return 1
if __name__ == "__main__":
exit(main())
Research Lookup Skill
This skill provides real-time research information lookup using Perplexity's Sonar Pro Search model through OpenRouter.
Setup
1. Get OpenRouter API Key:
- Visit openrouter.ai
- Create account and generate API key
- Add credits to your account
2. Configure Environment:
export OPENROUTER_API_KEY="your_api_key_here"3. Test Setup:
python scripts/research_lookup.py --model-infoUsage
Command Line Usage
# Single research query
python scripts/research_lookup.py "Recent advances in CRISPR gene editing 2024"
# Multiple queries with delay
python scripts/research_lookup.py --batch "CRISPR applications" "gene therapy trials" "ethical considerations"
# Claude Code integration (called automatically)
python lookup.py "your research query here"Claude Code Integration
The research lookup tool is automatically available in Claude Code when you:
1. Ask research questions: "Research recent advances in quantum computing" 2. Request literature reviews: "Find current studies on climate change impacts" 3. Need citations: "What are the latest papers on transformer attention mechanisms?" 4. Want technical information: "Standard protocols for flow cytometry"
Features
- Academic Focus: Prioritizes peer-reviewed papers and reputable sources
- Current Information: Focuses on recent publications (2020-2024)
- Complete Citations: Provides full bibliographic information with DOIs
- Multiple Formats: Supports various query types and research needs
- High Search Context: Always uses high search context for deeper, more comprehensive research
- Quality Prioritization: Automatically prioritizes highly-cited papers from top venues
- Cost Effective: Typically $0.01-0.05 per research query
Paper Quality Prioritization
This skill always prioritizes high-impact, influential papers over obscure publications. Results are ranked by:
Citation-Based Ranking
| Paper Age | Citation Threshold | Classification |
|---|---|---|
| 0-3 years | 20+ citations | Noteworthy |
| 0-3 years | 100+ citations | Highly Influential |
| 3-7 years | 100+ citations | Significant |
| 3-7 years | 500+ citations | Landmark |
| 7+ years | 500+ citations | Seminal |
| 7+ years | 1000+ citations | Foundational |
Venue Quality Tiers
Papers from higher-tier venues are always preferred:
- Tier 1 (Highest Priority): Nature, Science, Cell, NEJM, Lancet, JAMA, PNAS, Nature Medicine, Nature Biotechnology
- Tier 2 (High Priority): High-impact journals (IF>10), top conferences (NeurIPS, ICML, ICLR for ML/AI)
- Tier 3 (Good): Respected specialized journals (IF 5-10)
- Tier 4 (Use Sparingly): Other peer-reviewed venues
Author Reputation
The skill prefers papers from:
- Senior researchers with high h-index
- Established research groups at recognized institutions
- Authors with multiple publications in Tier-1 venues
- Researchers with recognized expertise (awards, editorial positions)
Relevance Priority
1. Papers directly addressing the research question 2. Papers with applicable methods/data 3. Tangentially related papers (only from top venues or highly cited)
Query Examples
Academic Research
- "Recent systematic reviews on AI in medical diagnosis 2024"
- "Meta-analysis of randomized controlled trials for depression treatment"
- "Current state of quantum computing error correction research"
Technical Methods
- "Standard protocols for immunohistochemistry in tissue samples"
- "Best practices for machine learning model validation"
- "Statistical methods for analyzing longitudinal data"
Statistical Data
- "Global renewable energy adoption statistics 2024"
- "Prevalence of diabetes in different populations"
- "Market size for autonomous vehicles industry"
Response Format
Each research result includes:
- Summary: Brief overview of key findings
- Key Studies: 3-5 most relevant recent papers
- Citations: Complete bibliographic information
- Usage Stats: Token usage for cost tracking
- Timestamp: When the research was performed
Integration with Scientific Writing
This skill enhances the scientific writing process by providing:
1. Literature Reviews: Current research for introduction sections 2. Methods Validation: Verify protocols against current standards 3. Results Context: Compare findings with recent similar studies 4. Discussion Support: Latest evidence for arguments 5. Citation Management: Properly formatted references
Troubleshooting
"API key not found"
- Ensure
OPENROUTER_API_KEYenvironment variable is set - Check that you have credits in your OpenRouter account
"Model not available"
- Verify your API key has access to Perplexity models
- Check OpenRouter status page for service issues
"Rate limit exceeded"
- Add delays between requests using
--delayoption - Check your OpenRouter account limits
"No relevant results"
- Try more specific or broader queries
- Include time frames (e.g., "2023-2024")
- Use academic keywords and technical terms
Cost Management
- Monitor usage through OpenRouter dashboard
- Typical costs: $0.01-0.05 per research query
- Batch processing available for multiple queries
- Consider query specificity to optimize token usage
This skill is designed for academic and research purposes, providing high-quality, cited information to support scientific writing and research activities.
#!/usr/bin/env python3
"""
Research Information Lookup Tool
Routes research queries to the best backend:
- Parallel Chat API (core model): Default for all general research queries
- Perplexity sonar-pro-search (via OpenRouter): Academic-specific paper searches
Environment variables:
PARALLEL_API_KEY - Required for Parallel Chat API (primary backend)
OPENROUTER_API_KEY - Required for Perplexity academic searches (fallback)
"""
import os
import sys
import json
import re
import time
import requests
from datetime import datetime
from typing import Any, Dict, List, Optional
class ResearchLookup:
"""Research information lookup with intelligent backend routing.
Routes queries to the Parallel Chat API (default) or Perplexity
sonar-pro-search (academic paper searches only).
"""
ACADEMIC_KEYWORDS = [
"find papers", "find paper", "find articles", "find article",
"cite ", "citation", "citations for",
"doi ", "doi:", "pubmed", "pmid",
"journal article", "peer-reviewed",
"systematic review", "meta-analysis",
"literature search", "literature on",
"academic papers", "academic paper",
"research papers on", "research paper on",
"published studies", "published study",
"scholarly", "scholar",
"arxiv", "preprint",
"foundational papers", "seminal papers", "landmark papers",
"highly cited", "most cited",
]
PARALLEL_SYSTEM_PROMPT = (
"You are a deep research analyst. Provide a comprehensive, well-cited "
"research report on the user's topic. Include:\n"
"- Key findings with specific data, statistics, and quantitative evidence\n"
"- Detailed analysis organized by themes\n"
"- Multiple authoritative sources cited inline\n"
"- Methodologies and implications where relevant\n"
"- Future outlook and research gaps\n"
"Use markdown formatting with clear section headers. "
"Prioritize authoritative and recent sources."
)
CHAT_BASE_URL = "https://api.parallel.ai"
def __init__(self, force_backend: Optional[str] = None):
"""Initialize the research lookup tool.
Args:
force_backend: Force a specific backend ('parallel' or 'perplexity').
If None, backend is auto-selected based on query content.
"""
self.force_backend = force_backend
self.parallel_available = bool(os.getenv("PARALLEL_API_KEY"))
self.perplexity_available = bool(os.getenv("OPENROUTER_API_KEY"))
if not self.parallel_available and not self.perplexity_available:
raise ValueError(
"No API keys found. Set at least one of:\n"
" PARALLEL_API_KEY (for Parallel Chat API - primary)\n"
" OPENROUTER_API_KEY (for Perplexity academic search - fallback)"
)
def _select_backend(self, query: str) -> str:
"""Select the best backend for a query."""
if self.force_backend:
if self.force_backend == "perplexity" and self.perplexity_available:
return "perplexity"
if self.force_backend == "parallel" and self.parallel_available:
return "parallel"
query_lower = query.lower()
is_academic = any(kw in query_lower for kw in self.ACADEMIC_KEYWORDS)
if is_academic and self.perplexity_available:
return "perplexity"
if self.parallel_available:
return "parallel"
if self.perplexity_available:
return "perplexity"
raise ValueError("No backend available. Check API keys.")
# ------------------------------------------------------------------
# Parallel Chat API backend
# ------------------------------------------------------------------
def _get_chat_client(self):
"""Lazy-load and cache the OpenAI client for Parallel Chat API."""
if not hasattr(self, "_chat_client"):
try:
from openai import OpenAI
except ImportError:
raise ImportError(
"The 'openai' package is required for Parallel Chat API.\n"
"Install it with: pip install openai"
)
self._chat_client = OpenAI(
api_key=os.getenv("PARALLEL_API_KEY"),
base_url=self.CHAT_BASE_URL,
)
return self._chat_client
def _parallel_lookup(self, query: str) -> Dict[str, Any]:
"""Run research via the Parallel Chat API (core model)."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
model = "core"
try:
client = self._get_chat_client()
print(f"[Research] Parallel Chat API (model={model})...", file=sys.stderr)
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": self.PARALLEL_SYSTEM_PROMPT},
{"role": "user", "content": query},
],
stream=False,
)
content = ""
if response.choices and len(response.choices) > 0:
content = response.choices[0].message.content or ""
api_citations = self._extract_basis_citations(response)
text_citations = self._extract_citations_from_text(content)
return {
"success": True,
"query": query,
"response": content,
"citations": api_citations + text_citations,
"sources": api_citations,
"timestamp": timestamp,
"backend": "parallel",
"model": f"parallel-chat/{model}",
}
except Exception as e:
return {
"success": False,
"query": query,
"error": str(e),
"timestamp": timestamp,
"backend": "parallel",
"model": f"parallel-chat/{model}",
}
def _extract_basis_citations(self, response) -> List[Dict[str, str]]:
"""Extract citation sources from the Chat API research basis."""
citations = []
basis = getattr(response, "basis", None)
if not basis:
return citations
seen_urls = set()
if isinstance(basis, list):
for item in basis:
cits = (
item.get("citations", []) if isinstance(item, dict)
else getattr(item, "citations", None) or []
)
for cit in cits:
url = cit.get("url", "") if isinstance(cit, dict) else getattr(cit, "url", "")
if url and url not in seen_urls:
seen_urls.add(url)
title = cit.get("title", "") if isinstance(cit, dict) else getattr(cit, "title", "")
excerpts = cit.get("excerpts", []) if isinstance(cit, dict) else getattr(cit, "excerpts", [])
citations.append({
"type": "source",
"url": url,
"title": title,
"excerpts": excerpts,
})
return citations
# ------------------------------------------------------------------
# Perplexity academic search backend
# ------------------------------------------------------------------
def _perplexity_lookup(self, query: str) -> Dict[str, Any]:
"""Run academic search via Perplexity sonar-pro-search through OpenRouter."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
api_key = os.getenv("OPENROUTER_API_KEY")
model = "perplexity/sonar-pro-search"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://scientific-writer.local",
"X-Title": "Scientific Writer Research Tool",
}
research_prompt = self._format_academic_prompt(query)
messages = [
{
"role": "system",
"content": (
"You are an academic research assistant specializing in finding "
"HIGH-IMPACT, INFLUENTIAL research.\n\n"
"QUALITY PRIORITIZATION (CRITICAL):\n"
"- ALWAYS prefer highly-cited papers over obscure publications\n"
"- ALWAYS prioritize Tier-1 venues: Nature, Science, Cell, NEJM, Lancet, JAMA, PNAS\n"
"- ALWAYS prefer papers from established researchers\n"
"- Include citation counts when known (e.g., 'cited 500+ times')\n"
"- Quality matters more than quantity\n\n"
"VENUE HIERARCHY:\n"
"1. Nature/Science/Cell family, NEJM, Lancet, JAMA (highest)\n"
"2. High-impact specialized journals (IF>10), top conferences (NeurIPS, ICML, ICLR)\n"
"3. Respected field-specific journals (IF 5-10)\n"
"4. Other peer-reviewed sources (only if no better option)\n\n"
"Focus exclusively on scholarly sources. Prioritize recent literature (2020-2026) "
"and provide complete citations with DOIs."
),
},
{"role": "user", "content": research_prompt},
]
data = {
"model": model,
"messages": messages,
"max_tokens": 8000,
"temperature": 0.1,
"search_mode": "academic",
"search_context_size": "high",
}
try:
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers=headers,
json=data,
timeout=90,
)
response.raise_for_status()
resp_json = response.json()
if "choices" in resp_json and len(resp_json["choices"]) > 0:
choice = resp_json["choices"][0]
if "message" in choice and "content" in choice["message"]:
content = choice["message"]["content"]
api_citations = self._extract_api_citations(resp_json, choice)
text_citations = self._extract_citations_from_text(content)
citations = api_citations + text_citations
return {
"success": True,
"query": query,
"response": content,
"citations": citations,
"sources": api_citations,
"timestamp": timestamp,
"backend": "perplexity",
"model": model,
"usage": resp_json.get("usage", {}),
}
else:
raise Exception("Invalid response format from API")
else:
raise Exception("No response choices received from API")
except Exception as e:
return {
"success": False,
"query": query,
"error": str(e),
"timestamp": timestamp,
"backend": "perplexity",
"model": model,
}
# ------------------------------------------------------------------
# Shared utilities
# ------------------------------------------------------------------
def _format_academic_prompt(self, query: str) -> str:
"""Format a query for academic research results via Perplexity."""
return f"""You are an expert research assistant. Please provide comprehensive, accurate research information for the following query: "{query}"
IMPORTANT INSTRUCTIONS:
1. Focus on ACADEMIC and SCIENTIFIC sources (peer-reviewed papers, reputable journals, institutional research)
2. Include RECENT information (prioritize 2020-2026 publications)
3. Provide COMPLETE citations with authors, title, journal/conference, year, and DOI when available
4. Structure your response with clear sections and proper attribution
5. Be comprehensive but concise - aim for 800-1200 words
6. Include key findings, methodologies, and implications when relevant
7. Note any controversies, limitations, or conflicting evidence
PAPER QUALITY PRIORITIZATION (CRITICAL):
8. ALWAYS prioritize HIGHLY-CITED papers over obscure publications
9. ALWAYS prioritize papers from TOP-TIER VENUES (Nature, Science, Cell, NEJM, Lancet, JAMA, PNAS)
10. PREFER papers from ESTABLISHED, REPUTABLE AUTHORS
11. For EACH citation include when available: citation count, venue tier, author credentials
12. PRIORITIZE papers that DIRECTLY address the research question
RESPONSE FORMAT:
- Start with a brief summary (2-3 sentences)
- Present key findings and studies in organized sections
- Rank papers by impact: most influential/cited first
- End with future directions or research gaps if applicable
- Include 5-8 high-quality citations
Remember: Quality over quantity. Prioritize influential, highly-cited papers from prestigious venues."""
def _extract_api_citations(self, response: Dict[str, Any], choice: Dict[str, Any]) -> List[Dict[str, str]]:
"""Extract citations from Perplexity API response fields."""
citations = []
search_results = (
response.get("search_results")
or choice.get("search_results")
or choice.get("message", {}).get("search_results")
or []
)
for result in search_results:
citation = {
"type": "source",
"title": result.get("title", ""),
"url": result.get("url", ""),
"date": result.get("date", ""),
}
if result.get("snippet"):
citation["snippet"] = result["snippet"]
citations.append(citation)
legacy_citations = (
response.get("citations")
or choice.get("citations")
or choice.get("message", {}).get("citations")
or []
)
for url in legacy_citations:
if isinstance(url, str):
citations.append({"type": "source", "url": url, "title": "", "date": ""})
elif isinstance(url, dict):
citations.append({
"type": "source",
"url": url.get("url", ""),
"title": url.get("title", ""),
"date": url.get("date", ""),
})
return citations
def _extract_citations_from_text(self, text: str) -> List[Dict[str, str]]:
"""Extract DOIs and academic URLs from response text as fallback."""
citations = []
doi_pattern = r'(?:doi[:\s]*|https?://(?:dx\.)?doi\.org/)(10\.[0-9]{4,}/[^\s\)\]\,\[\<\>]+)'
doi_matches = re.findall(doi_pattern, text, re.IGNORECASE)
seen_dois = set()
for doi in doi_matches:
doi_clean = doi.strip().rstrip(".,;:)]")
if doi_clean and doi_clean not in seen_dois:
seen_dois.add(doi_clean)
citations.append({
"type": "doi",
"doi": doi_clean,
"url": f"https://doi.org/{doi_clean}",
})
url_pattern = (
r'https?://[^\s\)\]\,\<\>\"\']+(?:arxiv\.org|pubmed|ncbi\.nlm\.nih\.gov|'
r'nature\.com|science\.org|wiley\.com|springer\.com|ieee\.org|acm\.org)'
r'[^\s\)\]\,\<\>\"\']*'
)
url_matches = re.findall(url_pattern, text, re.IGNORECASE)
seen_urls = set()
for url in url_matches:
url_clean = url.rstrip(".")
if url_clean not in seen_urls:
seen_urls.add(url_clean)
citations.append({"type": "url", "url": url_clean})
return citations
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def lookup(self, query: str) -> Dict[str, Any]:
"""Perform a research lookup, routing to the best backend.
Parallel Chat API is used by default. Perplexity sonar-pro-search
is used only for academic-specific queries (paper searches, DOI lookups).
"""
backend = self._select_backend(query)
print(f"[Research] Backend: {backend} | Query: {query[:80]}...", file=sys.stderr)
if backend == "parallel":
return self._parallel_lookup(query)
else:
return self._perplexity_lookup(query)
def batch_lookup(self, queries: List[str], delay: float = 1.0) -> List[Dict[str, Any]]:
"""Perform multiple research lookups with delay between requests."""
results = []
for i, query in enumerate(queries):
if i > 0 and delay > 0:
time.sleep(delay)
result = self.lookup(query)
results.append(result)
print(f"[Research] Completed query {i+1}/{len(queries)}: {query[:50]}...", file=sys.stderr)
return results
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
"""Command-line interface for the research lookup tool."""
import argparse
parser = argparse.ArgumentParser(
description="Research Information Lookup Tool (Parallel Chat API + Perplexity)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# General research (uses Parallel Chat API, core model)
python research_lookup.py "latest advances in quantum computing 2025"
# Academic paper search (auto-routes to Perplexity)
python research_lookup.py "find papers on CRISPR gene editing clinical trials"
# Force a specific backend
python research_lookup.py "topic" --force-backend parallel
python research_lookup.py "topic" --force-backend perplexity
# Save output to file
python research_lookup.py "topic" -o results.txt
# JSON output
python research_lookup.py "topic" --json -o results.json
""",
)
parser.add_argument("query", nargs="?", help="Research query to look up")
parser.add_argument("--batch", nargs="+", help="Run multiple queries")
parser.add_argument(
"--force-backend",
choices=["parallel", "perplexity"],
help="Force a specific backend (default: auto-select)",
)
parser.add_argument("-o", "--output", help="Write output to file")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
output_file = None
if args.output:
output_file = open(args.output, "w", encoding="utf-8")
def write_output(text):
if output_file:
output_file.write(text + "\n")
else:
print(text)
has_parallel = bool(os.getenv("PARALLEL_API_KEY"))
has_perplexity = bool(os.getenv("OPENROUTER_API_KEY"))
if not has_parallel and not has_perplexity:
print("Error: No API keys found. Set at least one:", file=sys.stderr)
print(" export PARALLEL_API_KEY='...' (primary - Parallel Chat API)", file=sys.stderr)
print(" export OPENROUTER_API_KEY='...' (fallback - Perplexity academic)", file=sys.stderr)
if output_file:
output_file.close()
return 1
if not args.query and not args.batch:
parser.print_help()
if output_file:
output_file.close()
return 1
try:
research = ResearchLookup(force_backend=args.force_backend)
if args.batch:
print(f"Running batch research for {len(args.batch)} queries...", file=sys.stderr)
results = research.batch_lookup(args.batch)
else:
print(f"Researching: {args.query}", file=sys.stderr)
results = [research.lookup(args.query)]
if args.json:
write_output(json.dumps(results, indent=2, ensure_ascii=False, default=str))
if output_file:
output_file.close()
return 0
for i, result in enumerate(results):
if result["success"]:
write_output(f"\n{'='*80}")
write_output(f"Query {i+1}: {result['query']}")
write_output(f"Timestamp: {result['timestamp']}")
write_output(f"Backend: {result.get('backend', 'unknown')} | Model: {result.get('model', 'unknown')}")
write_output(f"{'='*80}")
write_output(result["response"])
sources = result.get("sources", [])
if sources:
write_output(f"\nSources ({len(sources)}):")
for j, source in enumerate(sources):
title = source.get("title", "Untitled")
url = source.get("url", "")
date = source.get("date", "")
date_str = f" ({date})" if date else ""
write_output(f" [{j+1}] {title}{date_str}")
if url:
write_output(f" {url}")
citations = result.get("citations", [])
text_citations = [c for c in citations if c.get("type") in ("doi", "url")]
if text_citations:
write_output(f"\nAdditional References ({len(text_citations)}):")
for j, citation in enumerate(text_citations):
if citation.get("type") == "doi":
write_output(f" [{j+1}] DOI: {citation.get('doi', '')} - {citation.get('url', '')}")
elif citation.get("type") == "url":
write_output(f" [{j+1}] {citation.get('url', '')}")
if result.get("usage"):
write_output(f"\nUsage: {result['usage']}")
else:
write_output(f"\nError in query {i+1}: {result['error']}")
if output_file:
output_file.close()
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
if output_file:
output_file.close()
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
AI-powered scientific schematic generation using Nano Banana 2.
This script uses a smart iterative refinement approach:
1. Generate initial image with Nano Banana 2
2. AI quality review using Gemini 3.1 Pro Preview for scientific critique
3. Only regenerate if quality is below threshold for document type
4. Repeat until quality meets standards (max iterations)
Requirements:
- OPENROUTER_API_KEY environment variable
- requests library
Usage:
python generate_schematic_ai.py "Create a flowchart showing CONSORT participant flow" -o flowchart.png
python generate_schematic_ai.py "Neural network architecture diagram" -o architecture.png --iterations 2
python generate_schematic_ai.py "Simple block diagram" -o diagram.png --doc-type poster
"""
import argparse
import base64
import json
import os
import sys
import time
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple
try:
import requests
except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
return False
class ScientificSchematicGenerator:
"""Generate scientific schematics using AI with smart iterative refinement.
Uses Gemini 3.1 Pro Preview for quality review to determine if regeneration is needed.
Multiple passes only occur if the generated schematic doesn't meet the
quality threshold for the target document type.
"""
# Quality thresholds by document type (score out of 10)
# Higher thresholds for more formal publications
QUALITY_THRESHOLDS = {
"journal": 8.5, # Nature, Science, etc. - highest standards
"conference": 8.0, # Conference papers - high standards
"poster": 7.0, # Academic posters - good quality
"presentation": 6.5, # Slides/talks - clear but less formal
"report": 7.5, # Technical reports - professional
"grant": 8.0, # Grant proposals - must be compelling
"thesis": 8.0, # Dissertations - formal academic
"preprint": 7.5, # arXiv, etc. - good quality
"default": 7.5, # Default threshold
}
# Scientific diagram best practices prompt template
SCIENTIFIC_DIAGRAM_GUIDELINES = """
Create a high-quality scientific diagram with these requirements:
VISUAL QUALITY:
- Clean white or light background (no textures or gradients)
- High contrast for readability and printing
- Professional, publication-ready appearance
- Sharp, clear lines and text
- Adequate spacing between elements to prevent crowding
TYPOGRAPHY:
- Clear, readable sans-serif fonts (Arial, Helvetica style)
- Minimum 10pt font size for all labels
- Consistent font sizes throughout
- All text horizontal or clearly readable
- No overlapping text
SCIENTIFIC STANDARDS:
- Accurate representation of concepts
- Clear labels for all components
- Include scale bars, legends, or axes where appropriate
- Use standard scientific notation and symbols
- Include units where applicable
ACCESSIBILITY:
- Colorblind-friendly color palette (use Okabe-Ito colors if using color)
- High contrast between elements
- Redundant encoding (shapes + colors, not just colors)
- Works well in grayscale
LAYOUT:
- Logical flow (left-to-right or top-to-bottom)
- Clear visual hierarchy
- Balanced composition
- Appropriate use of whitespace
- No clutter or unnecessary decorative elements
IMPORTANT - NO FIGURE NUMBERS:
- Do NOT include "Figure 1:", "Fig. 1", or any figure numbering in the image
- Do NOT add captions or titles like "Figure: ..." at the top or bottom
- Figure numbers and captions are added separately in the document/LaTeX
- The diagram should contain only the visual content itself
"""
def __init__(self, api_key: Optional[str] = None, verbose: bool = False):
"""
Initialize the generator.
Args:
api_key: OpenRouter API key (or use OPENROUTER_API_KEY env var)
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
" 1. Set the OPENROUTER_API_KEY environment variable\n"
" 2. Add OPENROUTER_API_KEY to your .env file\n"
" 3. Pass api_key parameter to the constructor\n"
"Get your API key from: https://openrouter.ai/keys"
)
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3-pro-image-preview
self.image_model = "google/gemini-3.1-flash-image-preview"
# Gemini 3.1 Pro Preview for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.1-pro-preview"
def _log(self, message: str):
"""Log message if verbose mode is enabled."""
if self.verbose:
print(f"[{time.strftime('%H:%M:%S')}] {message}")
def _make_request(self, model: str, messages: List[Dict[str, Any]],
modalities: Optional[List[str]] = None) -> Dict[str, Any]:
"""
Make a request to OpenRouter API.
Args:
model: Model identifier
messages: List of message dictionaries
modalities: Optional list of modalities (e.g., ["image", "text"])
Returns:
API response as dictionary
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/scientific-writer",
"X-Title": "Scientific Schematic Generator"
}
payload = {
"model": model,
"messages": messages
}
if modalities:
payload["modalities"] = modalities
self._log(f"Making request to {model}...")
try:
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload,
timeout=120
)
# Try to get response body even on error
try:
response_json = response.json()
except json.JSONDecodeError:
response_json = {"raw_text": response.text[:500]}
# Check for HTTP errors but include response body in error message
if response.status_code != 200:
error_detail = response_json.get("error", response_json)
self._log(f"HTTP {response.status_code}: {error_detail}")
raise RuntimeError(f"API request failed (HTTP {response.status_code}): {error_detail}")
return response_json
except requests.exceptions.Timeout:
raise RuntimeError("API request timed out after 120 seconds")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"API request failed: {str(e)}")
def _extract_image_from_response(self, response: Dict[str, Any]) -> Optional[bytes]:
"""
Extract base64-encoded image from API response.
For Nano Banana 2, images are returned in the 'images' field of the message,
not in the 'content' field.
Args:
response: API response dictionary
Returns:
Image bytes or None if not found
"""
try:
choices = response.get("choices", [])
if not choices:
self._log("No choices in response")
return None
message = choices[0].get("message", {})
# IMPORTANT: Nano Banana 2 returns images in the 'images' field
images = message.get("images", [])
if images and len(images) > 0:
self._log(f"Found {len(images)} image(s) in 'images' field")
# Get first image
first_image = images[0]
if isinstance(first_image, dict):
# Extract image_url
if first_image.get("type") == "image_url":
url = first_image.get("image_url", {})
if isinstance(url, dict):
url = url.get("url", "")
if url and url.startswith("data:image"):
# Extract base64 data after comma
if "," in url:
base64_str = url.split(",", 1)[1]
# Clean whitespace
base64_str = base64_str.replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Extracted base64 data (length: {len(base64_str)})")
return base64.b64decode(base64_str)
# Fallback: check content field (for other models or future changes)
content = message.get("content", "")
if self.verbose:
self._log(f"Content type: {type(content)}, length: {len(str(content))}")
# Handle string content
if isinstance(content, str) and "data:image" in content:
import re
match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=\n\r]+)', content, re.DOTALL)
if match:
base64_str = match.group(1).replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Found image in content field (length: {len(base64_str)})")
return base64.b64decode(base64_str)
# Handle list content
if isinstance(content, list):
for i, block in enumerate(content):
if isinstance(block, dict) and block.get("type") == "image_url":
url = block.get("image_url", {})
if isinstance(url, dict):
url = url.get("url", "")
if url and url.startswith("data:image") and "," in url:
base64_str = url.split(",", 1)[1].replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Found image in content block {i}")
return base64.b64decode(base64_str)
self._log("No image data found in response")
return None
except Exception as e:
self._log(f"Error extracting image: {str(e)}")
import traceback
if self.verbose:
traceback.print_exc()
return None
def _image_to_base64(self, image_path: str) -> str:
"""
Convert image file to base64 data URL.
Args:
image_path: Path to image file
Returns:
Base64 data URL string
"""
with open(image_path, "rb") as f:
image_data = f.read()
# Determine image type from extension
ext = Path(image_path).suffix.lower()
mime_type = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp"
}.get(ext, "image/png")
base64_data = base64.b64encode(image_data).decode("utf-8")
return f"data:{mime_type};base64,{base64_data}"
def generate_image(self, prompt: str) -> Optional[bytes]:
"""
Generate an image using Nano Banana 2.
Args:
prompt: Description of the diagram to generate
Returns:
Image bytes or None if generation failed
"""
self._last_error = None # Reset error
messages = [
{
"role": "user",
"content": prompt
}
]
try:
response = self._make_request(
model=self.image_model,
messages=messages,
modalities=["image", "text"]
)
# Debug: print response structure if verbose
if self.verbose:
self._log(f"Response keys: {response.keys()}")
if "error" in response:
self._log(f"API Error: {response['error']}")
if "choices" in response and response["choices"]:
msg = response["choices"][0].get("message", {})
self._log(f"Message keys: {msg.keys()}")
# Show content preview without printing huge base64 data
content = msg.get("content", "")
if isinstance(content, str):
preview = content[:200] + "..." if len(content) > 200 else content
self._log(f"Content preview: {preview}")
elif isinstance(content, list):
self._log(f"Content is list with {len(content)} items")
for i, item in enumerate(content[:3]):
if isinstance(item, dict):
self._log(f" Item {i}: type={item.get('type')}")
# Check for API errors in response
if "error" in response:
error_msg = response["error"]
if isinstance(error_msg, dict):
error_msg = error_msg.get("message", str(error_msg))
self._last_error = f"API Error: {error_msg}"
print(f"✗ {self._last_error}")
return None
image_data = self._extract_image_from_response(response)
if image_data:
self._log(f"✓ Generated image ({len(image_data)} bytes)")
else:
self._last_error = "No image data in API response - model may not support image generation"
self._log(f"✗ {self._last_error}")
# Additional debug info when image extraction fails
if self.verbose and "choices" in response:
msg = response["choices"][0].get("message", {})
self._log(f"Full message structure: {json.dumps({k: type(v).__name__ for k, v in msg.items()})}")
return image_data
except RuntimeError as e:
self._last_error = str(e)
self._log(f"✗ Generation failed: {self._last_error}")
return None
except Exception as e:
self._last_error = f"Unexpected error: {str(e)}"
self._log(f"✗ Generation failed: {self._last_error}")
import traceback
if self.verbose:
traceback.print_exc()
return None
def review_image(self, image_path: str, original_prompt: str,
iteration: int, doc_type: str = "default",
max_iterations: int = 2) -> Tuple[str, float, bool]:
"""
Review generated image using Gemini 3.1 Pro Preview for quality analysis.
Uses Gemini 3.1 Pro Preview's superior vision and reasoning capabilities to
evaluate the schematic quality and determine if regeneration is needed.
Args:
image_path: Path to the generated image
original_prompt: Original user prompt
iteration: Current iteration number
doc_type: Document type (journal, poster, presentation, etc.)
max_iterations: Maximum iterations allowed
Returns:
Tuple of (critique text, quality score 0-10, needs_improvement bool)
"""
# Use Gemini 3.1 Pro Preview for review - excellent vision and analysis
image_data_url = self._image_to_base64(image_path)
# Get quality threshold for this document type
threshold = self.QUALITY_THRESHOLDS.get(doc_type.lower(),
self.QUALITY_THRESHOLDS["default"])
review_prompt = f"""You are an expert reviewer evaluating a scientific diagram for publication quality.
ORIGINAL REQUEST: {original_prompt}
DOCUMENT TYPE: {doc_type} (quality threshold: {threshold}/10)
ITERATION: {iteration}/{max_iterations}
Carefully evaluate this diagram on these criteria:
1. **Scientific Accuracy** (0-2 points)
- Correct representation of concepts
- Proper notation and symbols
- Accurate relationships shown
2. **Clarity and Readability** (0-2 points)
- Easy to understand at a glance
- Clear visual hierarchy
- No ambiguous elements
3. **Label Quality** (0-2 points)
- All important elements labeled
- Labels are readable (appropriate font size)
- Consistent labeling style
4. **Layout and Composition** (0-2 points)
- Logical flow (top-to-bottom or left-to-right)
- Balanced use of space
- No overlapping elements
5. **Professional Appearance** (0-2 points)
- Publication-ready quality
- Clean, crisp lines and shapes
- Appropriate colors/contrast
RESPOND IN THIS EXACT FORMAT:
SCORE: [total score 0-10]
STRENGTHS:
- [strength 1]
- [strength 2]
ISSUES:
- [issue 1 if any]
- [issue 2 if any]
VERDICT: [ACCEPTABLE or NEEDS_IMPROVEMENT]
If score >= {threshold}, the diagram is ACCEPTABLE for {doc_type} publication.
If score < {threshold}, mark as NEEDS_IMPROVEMENT with specific suggestions."""
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": review_prompt
},
{
"type": "image_url",
"image_url": {
"url": image_data_url
}
}
]
}
]
try:
# Use Gemini 3.1 Pro Preview for high-quality review
response = self._make_request(
model=self.review_model,
messages=messages
)
# Extract text response
choices = response.get("choices", [])
if not choices:
return "Image generated successfully", 8.0
message = choices[0].get("message", {})
content = message.get("content", "")
# Check reasoning field (Nano Banana 2 puts analysis here)
reasoning = message.get("reasoning", "")
if reasoning and not content:
content = reasoning
if isinstance(content, list):
# Extract text from content blocks
text_parts = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
content = "\n".join(text_parts)
# Try to extract score
score = 7.5 # Default score if extraction fails
import re
# Look for SCORE: X or SCORE: X/10 format
score_match = re.search(r'SCORE:\s*(\d+(?:\.\d+)?)', content, re.IGNORECASE)
if score_match:
score = float(score_match.group(1))
else:
# Fallback: look for any score pattern
score_match = re.search(r'(?:score|rating|quality)[:\s]+(\d+(?:\.\d+)?)\s*(?:/\s*10)?', content, re.IGNORECASE)
if score_match:
score = float(score_match.group(1))
# Determine if improvement is needed based on verdict or score
needs_improvement = False
if "NEEDS_IMPROVEMENT" in content.upper():
needs_improvement = True
elif score < threshold:
needs_improvement = True
self._log(f"✓ Review complete (Score: {score}/10, Threshold: {threshold}/10)")
self._log(f" Verdict: {'Needs improvement' if needs_improvement else 'Acceptable'}")
return (content if content else "Image generated successfully",
score,
needs_improvement)
except Exception as e:
self._log(f"Review skipped: {str(e)}")
# Don't fail the whole process if review fails - assume acceptable
return "Image generated successfully (review skipped)", 7.5, False
def improve_prompt(self, original_prompt: str, critique: str,
iteration: int) -> str:
"""
Improve the generation prompt based on critique.
Args:
original_prompt: Original user prompt
critique: Review critique from previous iteration
iteration: Current iteration number
Returns:
Improved prompt for next generation
"""
improved_prompt = f"""{self.SCIENTIFIC_DIAGRAM_GUIDELINES}
USER REQUEST: {original_prompt}
ITERATION {iteration}: Based on previous feedback, address these specific improvements:
{critique}
Generate an improved version that addresses all the critique points while maintaining scientific accuracy and professional quality."""
return improved_prompt
def generate_iterative(self, user_prompt: str, output_path: str,
iterations: int = 2,
doc_type: str = "default") -> Dict[str, Any]:
"""
Generate scientific schematic with smart iterative refinement.
Only regenerates if the quality score is below the threshold for the
specified document type. This saves API calls and time when the first
generation is already good enough.
Args:
user_prompt: User's description of desired diagram
output_path: Path to save final image
iterations: Maximum refinement iterations (default: 2, max: 2)
doc_type: Document type for quality threshold (journal, poster, etc.)
Returns:
Dictionary with generation results and metadata
"""
output_path = Path(output_path)
output_dir = output_path.parent
output_dir.mkdir(parents=True, exist_ok=True)
base_name = output_path.stem
extension = output_path.suffix or ".png"
# Get quality threshold for this document type
threshold = self.QUALITY_THRESHOLDS.get(doc_type.lower(),
self.QUALITY_THRESHOLDS["default"])
results = {
"user_prompt": user_prompt,
"doc_type": doc_type,
"quality_threshold": threshold,
"iterations": [],
"final_image": None,
"final_score": 0.0,
"success": False,
"early_stop": False,
"early_stop_reason": None
}
current_prompt = f"""{self.SCIENTIFIC_DIAGRAM_GUIDELINES}
USER REQUEST: {user_prompt}
Generate a publication-quality scientific diagram that meets all the guidelines above."""
print(f"\n{'='*60}")
print(f"Generating Scientific Schematic")
print(f"{'='*60}")
print(f"Description: {user_prompt}")
print(f"Document Type: {doc_type}")
print(f"Quality Threshold: {threshold}/10")
print(f"Max Iterations: {iterations}")
print(f"Output: {output_path}")
print(f"{'='*60}\n")
for i in range(1, iterations + 1):
print(f"\n[Iteration {i}/{iterations}]")
print("-" * 40)
# Generate image
print(f"Generating image...")
image_data = self.generate_image(current_prompt)
if not image_data:
error_msg = getattr(self, '_last_error', 'Image generation failed - no image data returned')
print(f"✗ Generation failed: {error_msg}")
results["iterations"].append({
"iteration": i,
"success": False,
"error": error_msg
})
continue
# Save iteration image
iter_path = output_dir / f"{base_name}_v{i}{extension}"
with open(iter_path, "wb") as f:
f.write(image_data)
print(f"✓ Saved: {iter_path}")
# Review image using Gemini 3.1 Pro Preview
print(f"Reviewing image with Gemini 3.1 Pro Preview...")
critique, score, needs_improvement = self.review_image(
str(iter_path), user_prompt, i, doc_type, iterations
)
print(f"✓ Score: {score}/10 (threshold: {threshold}/10)")
# Save iteration results
iteration_result = {
"iteration": i,
"image_path": str(iter_path),
"prompt": current_prompt,
"critique": critique,
"score": score,
"needs_improvement": needs_improvement,
"success": True
}
results["iterations"].append(iteration_result)
# Check if quality is acceptable - STOP EARLY if so
if not needs_improvement:
print(f"\n✓ Quality meets {doc_type} threshold ({score} >= {threshold})")
print(f" No further iterations needed!")
results["final_image"] = str(iter_path)
results["final_score"] = score
results["success"] = True
results["early_stop"] = True
results["early_stop_reason"] = f"Quality score {score} meets threshold {threshold} for {doc_type}"
break
# If this is the last iteration, we're done regardless
if i == iterations:
print(f"\n⚠ Maximum iterations reached")
results["final_image"] = str(iter_path)
results["final_score"] = score
results["success"] = True
break
# Quality below threshold - improve prompt for next iteration
print(f"\n⚠ Quality below threshold ({score} < {threshold})")
print(f"Improving prompt based on feedback...")
current_prompt = self.improve_prompt(user_prompt, critique, i + 1)
# Copy final version to output path
if results["success"] and results["final_image"]:
final_iter_path = Path(results["final_image"])
if final_iter_path != output_path:
import shutil
shutil.copy(final_iter_path, output_path)
print(f"\n✓ Final image: {output_path}")
# Save review log
log_path = output_dir / f"{base_name}_review_log.json"
with open(log_path, "w") as f:
json.dump(results, f, indent=2)
print(f"✓ Review log: {log_path}")
print(f"\n{'='*60}")
print(f"Generation Complete!")
print(f"Final Score: {results['final_score']}/10")
if results["early_stop"]:
print(f"Iterations Used: {len([r for r in results['iterations'] if r.get('success')])}/{iterations} (early stop)")
print(f"{'='*60}\n")
return results
def main():
"""Command-line interface."""
parser = argparse.ArgumentParser(
description="Generate scientific schematics using AI with smart iterative refinement",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate a flowchart for a journal paper
python generate_schematic_ai.py "CONSORT participant flow diagram" -o flowchart.png --doc-type journal
# Generate neural network architecture for presentation (lower threshold)
python generate_schematic_ai.py "Transformer encoder-decoder architecture" -o transformer.png --doc-type presentation
# Generate with custom max iterations for poster
python generate_schematic_ai.py "Biological signaling pathway" -o pathway.png --iterations 2 --doc-type poster
# Verbose output
python generate_schematic_ai.py "Circuit diagram" -o circuit.png -v
Document Types (quality thresholds):
journal 8.5/10 - Nature, Science, peer-reviewed journals
conference 8.0/10 - Conference papers
thesis 8.0/10 - Dissertations, theses
grant 8.0/10 - Grant proposals
preprint 7.5/10 - arXiv, bioRxiv, etc.
report 7.5/10 - Technical reports
poster 7.0/10 - Academic posters
presentation 6.5/10 - Slides, talks
default 7.5/10 - General purpose
Note: Multiple iterations only occur if quality is BELOW the threshold.
If the first generation meets the threshold, no extra API calls are made.
Environment:
OPENROUTER_API_KEY OpenRouter API key (required)
"""
)
parser.add_argument("prompt", help="Description of the diagram to generate")
parser.add_argument("-o", "--output", required=True,
help="Output image path (e.g., diagram.png)")
parser.add_argument("--iterations", type=int, default=2,
help="Maximum refinement iterations (default: 2, max: 2)")
parser.add_argument("--doc-type", default="default",
choices=["journal", "conference", "poster", "presentation",
"report", "grant", "thesis", "preprint", "default"],
help="Document type for quality threshold (default: default)")
parser.add_argument("--api-key", help="OpenRouter API key (or set OPENROUTER_API_KEY)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Verbose output")
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2
if args.iterations < 1 or args.iterations > 2:
print("Error: Iterations must be between 1 and 2")
sys.exit(1)
try:
generator = ScientificSchematicGenerator(api_key=api_key, verbose=args.verbose)
results = generator.generate_iterative(
user_prompt=args.prompt,
output_path=args.output,
iterations=args.iterations,
doc_type=args.doc_type
)
if results["success"]:
print(f"\n✓ Success! Image saved to: {args.output}")
if results.get("early_stop"):
print(f" (Completed in {len([r for r in results['iterations'] if r.get('success')])} iteration(s) - quality threshold met)")
sys.exit(0)
else:
print(f"\n✗ Generation failed. Check review log for details.")
sys.exit(1)
except Exception as e:
print(f"\n✗ Error: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Scientific schematic generation using Nano Banana 2.
Generate any scientific diagram by describing it in natural language.
Nano Banana 2 handles everything automatically with smart iterative refinement.
Smart iteration: Only regenerates if quality is below threshold for your document type.
Quality review: Uses Gemini 3.1 Pro Preview for professional scientific evaluation.
Usage:
# Generate for journal paper (highest quality threshold)
python generate_schematic.py "CONSORT flowchart" -o flowchart.png --doc-type journal
# Generate for presentation (lower threshold, faster)
python generate_schematic.py "Transformer architecture" -o transformer.png --doc-type presentation
# Generate for poster
python generate_schematic.py "MAPK signaling pathway" -o pathway.png --doc-type poster
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
def main():
"""Command-line interface."""
parser = argparse.ArgumentParser(
description="Generate scientific schematics using AI with smart iterative refinement",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
How it works:
Simply describe your diagram in natural language
Nano Banana 2 generates it automatically with:
- Smart iteration (only regenerates if quality is below threshold)
- Quality review by Gemini 3.1 Pro Preview
- Document-type aware quality thresholds
- Publication-ready output
Document Types (quality thresholds):
journal 8.5/10 - Nature, Science, peer-reviewed journals
conference 8.0/10 - Conference papers
thesis 8.0/10 - Dissertations, theses
grant 8.0/10 - Grant proposals
preprint 7.5/10 - arXiv, bioRxiv, etc.
report 7.5/10 - Technical reports
poster 7.0/10 - Academic posters
presentation 6.5/10 - Slides, talks
default 7.5/10 - General purpose
Examples:
# Generate for journal paper (strict quality)
python generate_schematic.py "CONSORT participant flow" -o flowchart.png --doc-type journal
# Generate for poster (moderate quality)
python generate_schematic.py "Transformer architecture" -o arch.png --doc-type poster
# Generate for slides (faster, lower threshold)
python generate_schematic.py "System diagram" -o system.png --doc-type presentation
# Custom max iterations
python generate_schematic.py "Complex pathway" -o pathway.png --iterations 2
# Verbose output
python generate_schematic.py "Circuit diagram" -o circuit.png -v
Environment Variables:
OPENROUTER_API_KEY Required for AI generation
"""
)
parser.add_argument("prompt",
help="Description of the diagram to generate")
parser.add_argument("-o", "--output", required=True,
help="Output file path")
parser.add_argument("--doc-type", default="default",
choices=["journal", "conference", "poster", "presentation",
"report", "grant", "thesis", "preprint", "default"],
help="Document type for quality threshold (default: default)")
parser.add_argument("--iterations", type=int, default=2,
help="Maximum refinement iterations (default: 2, max: 2)")
parser.add_argument("--api-key",
help="OpenRouter API key (or use OPENROUTER_API_KEY env var)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Verbose output")
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
sys.exit(1)
# Find AI generation script
script_dir = Path(__file__).parent
ai_script = script_dir / "generate_schematic_ai.py"
if not ai_script.exists():
print(f"Error: AI generation script not found: {ai_script}")
sys.exit(1)
# Build command
cmd = [sys.executable, str(ai_script), args.prompt, "-o", args.output]
if args.doc_type != "default":
cmd.extend(["--doc-type", args.doc_type])
# Enforce max 2 iterations
iterations = min(args.iterations, 2)
if iterations != 2:
cmd.extend(["--iterations", str(iterations)])
if args.verbose:
cmd.append("-v")
# Execute — pass API key via environment to avoid exposure in process listings
try:
env = os.environ.copy()
if api_key:
env["OPENROUTER_API_KEY"] = api_key
result = subprocess.run(cmd, check=False, env=env)
sys.exit(result.returncode)
except Exception as e:
print(f"Error executing AI generation: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Research Information Lookup Tool
Routes research queries to the best backend:
- Parallel Chat API (core model): Default for all general research queries
- Perplexity sonar-pro-search (via OpenRouter): Academic-specific paper searches
Environment variables:
PARALLEL_API_KEY - Required for Parallel Chat API (primary backend)
OPENROUTER_API_KEY - Required for Perplexity academic searches (fallback)
"""
import os
import sys
import json
import re
import time
import requests
from datetime import datetime
from typing import Any, Dict, List, Optional
class ResearchLookup:
"""Research information lookup with intelligent backend routing.
Routes queries to the Parallel Chat API (default) or Perplexity
sonar-pro-search (academic paper searches only).
"""
ACADEMIC_KEYWORDS = [
"find papers", "find paper", "find articles", "find article",
"cite ", "citation", "citations for",
"doi ", "doi:", "pubmed", "pmid",
"journal article", "peer-reviewed",
"systematic review", "meta-analysis",
"literature search", "literature on",
"academic papers", "academic paper",
"research papers on", "research paper on",
"published studies", "published study",
"scholarly", "scholar",
"arxiv", "preprint",
"foundational papers", "seminal papers", "landmark papers",
"highly cited", "most cited",
]
PARALLEL_SYSTEM_PROMPT = (
"You are a deep research analyst. Provide a comprehensive, well-cited "
"research report on the user's topic. Include:\n"
"- Key findings with specific data, statistics, and quantitative evidence\n"
"- Detailed analysis organized by themes\n"
"- Multiple authoritative sources cited inline\n"
"- Methodologies and implications where relevant\n"
"- Future outlook and research gaps\n"
"Use markdown formatting with clear section headers. "
"Prioritize authoritative and recent sources."
)
CHAT_BASE_URL = "https://api.parallel.ai"
def __init__(self, force_backend: Optional[str] = None):
"""Initialize the research lookup tool.
Args:
force_backend: Force a specific backend ('parallel' or 'perplexity').
If None, backend is auto-selected based on query content.
"""
self.force_backend = force_backend
self.parallel_available = bool(os.getenv("PARALLEL_API_KEY"))
self.perplexity_available = bool(os.getenv("OPENROUTER_API_KEY"))
if not self.parallel_available and not self.perplexity_available:
raise ValueError(
"No API keys found. Set at least one of:\n"
" PARALLEL_API_KEY (for Parallel Chat API - primary)\n"
" OPENROUTER_API_KEY (for Perplexity academic search - fallback)"
)
def _select_backend(self, query: str) -> str:
"""Select the best backend for a query."""
if self.force_backend:
if self.force_backend == "perplexity" and self.perplexity_available:
return "perplexity"
if self.force_backend == "parallel" and self.parallel_available:
return "parallel"
query_lower = query.lower()
is_academic = any(kw in query_lower for kw in self.ACADEMIC_KEYWORDS)
if is_academic and self.perplexity_available:
return "perplexity"
if self.parallel_available:
return "parallel"
if self.perplexity_available:
return "perplexity"
raise ValueError("No backend available. Check API keys.")
# ------------------------------------------------------------------
# Parallel Chat API backend
# ------------------------------------------------------------------
def _get_chat_client(self):
"""Lazy-load and cache the OpenAI client for Parallel Chat API."""
if not hasattr(self, "_chat_client"):
try:
from openai import OpenAI
except ImportError:
raise ImportError(
"The 'openai' package is required for Parallel Chat API.\n"
"Install it with: pip install openai"
)
self._chat_client = OpenAI(
api_key=os.getenv("PARALLEL_API_KEY"),
base_url=self.CHAT_BASE_URL,
)
return self._chat_client
def _parallel_lookup(self, query: str) -> Dict[str, Any]:
"""Run research via the Parallel Chat API (core model)."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
model = "core"
try:
client = self._get_chat_client()
print(f"[Research] Parallel Chat API (model={model})...", file=sys.stderr)
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": self.PARALLEL_SYSTEM_PROMPT},
{"role": "user", "content": query},
],
stream=False,
)
content = ""
if response.choices and len(response.choices) > 0:
content = response.choices[0].message.content or ""
api_citations = self._extract_basis_citations(response)
text_citations = self._extract_citations_from_text(content)
return {
"success": True,
"query": query,
"response": content,
"citations": api_citations + text_citations,
"sources": api_citations,
"timestamp": timestamp,
"backend": "parallel",
"model": f"parallel-chat/{model}",
}
except Exception as e:
return {
"success": False,
"query": query,
"error": str(e),
"timestamp": timestamp,
"backend": "parallel",
"model": f"parallel-chat/{model}",
}
def _extract_basis_citations(self, response) -> List[Dict[str, str]]:
"""Extract citation sources from the Chat API research basis."""
citations = []
basis = getattr(response, "basis", None)
if not basis:
return citations
seen_urls = set()
if isinstance(basis, list):
for item in basis:
cits = (
item.get("citations", []) if isinstance(item, dict)
else getattr(item, "citations", None) or []
)
for cit in cits:
url = cit.get("url", "") if isinstance(cit, dict) else getattr(cit, "url", "")
if url and url not in seen_urls:
seen_urls.add(url)
title = cit.get("title", "") if isinstance(cit, dict) else getattr(cit, "title", "")
excerpts = cit.get("excerpts", []) if isinstance(cit, dict) else getattr(cit, "excerpts", [])
citations.append({
"type": "source",
"url": url,
"title": title,
"excerpts": excerpts,
})
return citations
# ------------------------------------------------------------------
# Perplexity academic search backend
# ------------------------------------------------------------------
def _perplexity_lookup(self, query: str) -> Dict[str, Any]:
"""Run academic search via Perplexity sonar-pro-search through OpenRouter."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
api_key = os.getenv("OPENROUTER_API_KEY")
model = "perplexity/sonar-pro-search"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://scientific-writer.local",
"X-Title": "Scientific Writer Research Tool",
}
research_prompt = self._format_academic_prompt(query)
messages = [
{
"role": "system",
"content": (
"You are an academic research assistant specializing in finding "
"HIGH-IMPACT, INFLUENTIAL research.\n\n"
"QUALITY PRIORITIZATION (CRITICAL):\n"
"- ALWAYS prefer highly-cited papers over obscure publications\n"
"- ALWAYS prioritize Tier-1 venues: Nature, Science, Cell, NEJM, Lancet, JAMA, PNAS\n"
"- ALWAYS prefer papers from established researchers\n"
"- Include citation counts when known (e.g., 'cited 500+ times')\n"
"- Quality matters more than quantity\n\n"
"VENUE HIERARCHY:\n"
"1. Nature/Science/Cell family, NEJM, Lancet, JAMA (highest)\n"
"2. High-impact specialized journals (IF>10), top conferences (NeurIPS, ICML, ICLR)\n"
"3. Respected field-specific journals (IF 5-10)\n"
"4. Other peer-reviewed sources (only if no better option)\n\n"
"Focus exclusively on scholarly sources. Prioritize recent literature (2020-2026) "
"and provide complete citations with DOIs."
),
},
{"role": "user", "content": research_prompt},
]
data = {
"model": model,
"messages": messages,
"max_tokens": 8000,
"temperature": 0.1,
"search_mode": "academic",
"search_context_size": "high",
}
try:
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers=headers,
json=data,
timeout=90,
)
response.raise_for_status()
resp_json = response.json()
if "choices" in resp_json and len(resp_json["choices"]) > 0:
choice = resp_json["choices"][0]
if "message" in choice and "content" in choice["message"]:
content = choice["message"]["content"]
api_citations = self._extract_api_citations(resp_json, choice)
text_citations = self._extract_citations_from_text(content)
citations = api_citations + text_citations
return {
"success": True,
"query": query,
"response": content,
"citations": citations,
"sources": api_citations,
"timestamp": timestamp,
"backend": "perplexity",
"model": model,
"usage": resp_json.get("usage", {}),
}
else:
raise Exception("Invalid response format from API")
else:
raise Exception("No response choices received from API")
except Exception as e:
return {
"success": False,
"query": query,
"error": str(e),
"timestamp": timestamp,
"backend": "perplexity",
"model": model,
}
# ------------------------------------------------------------------
# Shared utilities
# ------------------------------------------------------------------
def _format_academic_prompt(self, query: str) -> str:
"""Format a query for academic research results via Perplexity."""
return f"""You are an expert research assistant. Please provide comprehensive, accurate research information for the following query: "{query}"
IMPORTANT INSTRUCTIONS:
1. Focus on ACADEMIC and SCIENTIFIC sources (peer-reviewed papers, reputable journals, institutional research)
2. Include RECENT information (prioritize 2020-2026 publications)
3. Provide COMPLETE citations with authors, title, journal/conference, year, and DOI when available
4. Structure your response with clear sections and proper attribution
5. Be comprehensive but concise - aim for 800-1200 words
6. Include key findings, methodologies, and implications when relevant
7. Note any controversies, limitations, or conflicting evidence
PAPER QUALITY PRIORITIZATION (CRITICAL):
8. ALWAYS prioritize HIGHLY-CITED papers over obscure publications
9. ALWAYS prioritize papers from TOP-TIER VENUES (Nature, Science, Cell, NEJM, Lancet, JAMA, PNAS)
10. PREFER papers from ESTABLISHED, REPUTABLE AUTHORS
11. For EACH citation include when available: citation count, venue tier, author credentials
12. PRIORITIZE papers that DIRECTLY address the research question
RESPONSE FORMAT:
- Start with a brief summary (2-3 sentences)
- Present key findings and studies in organized sections
- Rank papers by impact: most influential/cited first
- End with future directions or research gaps if applicable
- Include 5-8 high-quality citations
Remember: Quality over quantity. Prioritize influential, highly-cited papers from prestigious venues."""
def _extract_api_citations(self, response: Dict[str, Any], choice: Dict[str, Any]) -> List[Dict[str, str]]:
"""Extract citations from Perplexity API response fields."""
citations = []
search_results = (
response.get("search_results")
or choice.get("search_results")
or choice.get("message", {}).get("search_results")
or []
)
for result in search_results:
citation = {
"type": "source",
"title": result.get("title", ""),
"url": result.get("url", ""),
"date": result.get("date", ""),
}
if result.get("snippet"):
citation["snippet"] = result["snippet"]
citations.append(citation)
legacy_citations = (
response.get("citations")
or choice.get("citations")
or choice.get("message", {}).get("citations")
or []
)
for url in legacy_citations:
if isinstance(url, str):
citations.append({"type": "source", "url": url, "title": "", "date": ""})
elif isinstance(url, dict):
citations.append({
"type": "source",
"url": url.get("url", ""),
"title": url.get("title", ""),
"date": url.get("date", ""),
})
return citations
def _extract_citations_from_text(self, text: str) -> List[Dict[str, str]]:
"""Extract DOIs and academic URLs from response text as fallback."""
citations = []
doi_pattern = r'(?:doi[:\s]*|https?://(?:dx\.)?doi\.org/)(10\.[0-9]{4,}/[^\s\)\]\,\[\<\>]+)'
doi_matches = re.findall(doi_pattern, text, re.IGNORECASE)
seen_dois = set()
for doi in doi_matches:
doi_clean = doi.strip().rstrip(".,;:)]")
if doi_clean and doi_clean not in seen_dois:
seen_dois.add(doi_clean)
citations.append({
"type": "doi",
"doi": doi_clean,
"url": f"https://doi.org/{doi_clean}",
})
url_pattern = (
r'https?://[^\s\)\]\,\<\>\"\']+(?:arxiv\.org|pubmed|ncbi\.nlm\.nih\.gov|'
r'nature\.com|science\.org|wiley\.com|springer\.com|ieee\.org|acm\.org)'
r'[^\s\)\]\,\<\>\"\']*'
)
url_matches = re.findall(url_pattern, text, re.IGNORECASE)
seen_urls = set()
for url in url_matches:
url_clean = url.rstrip(".")
if url_clean not in seen_urls:
seen_urls.add(url_clean)
citations.append({"type": "url", "url": url_clean})
return citations
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def lookup(self, query: str) -> Dict[str, Any]:
"""Perform a research lookup, routing to the best backend.
Parallel Chat API is used by default. Perplexity sonar-pro-search
is used only for academic-specific queries (paper searches, DOI lookups).
"""
backend = self._select_backend(query)
print(f"[Research] Backend: {backend} | Query: {query[:80]}...", file=sys.stderr)
if backend == "parallel":
return self._parallel_lookup(query)
else:
return self._perplexity_lookup(query)
def batch_lookup(self, queries: List[str], delay: float = 1.0) -> List[Dict[str, Any]]:
"""Perform multiple research lookups with delay between requests."""
results = []
for i, query in enumerate(queries):
if i > 0 and delay > 0:
time.sleep(delay)
result = self.lookup(query)
results.append(result)
print(f"[Research] Completed query {i+1}/{len(queries)}: {query[:50]}...", file=sys.stderr)
return results
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
"""Command-line interface for the research lookup tool."""
import argparse
parser = argparse.ArgumentParser(
description="Research Information Lookup Tool (Parallel Chat API + Perplexity)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# General research (uses Parallel Chat API, core model)
python research_lookup.py "latest advances in quantum computing 2025"
# Academic paper search (auto-routes to Perplexity)
python research_lookup.py "find papers on CRISPR gene editing clinical trials"
# Force a specific backend
python research_lookup.py "topic" --force-backend parallel
python research_lookup.py "topic" --force-backend perplexity
# Save output to file
python research_lookup.py "topic" -o results.txt
# JSON output
python research_lookup.py "topic" --json -o results.json
""",
)
parser.add_argument("query", nargs="?", help="Research query to look up")
parser.add_argument("--batch", nargs="+", help="Run multiple queries")
parser.add_argument(
"--force-backend",
choices=["parallel", "perplexity"],
help="Force a specific backend (default: auto-select)",
)
parser.add_argument("-o", "--output", help="Write output to file")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
output_file = None
if args.output:
output_file = open(args.output, "w", encoding="utf-8")
def write_output(text):
if output_file:
output_file.write(text + "\n")
else:
print(text)
has_parallel = bool(os.getenv("PARALLEL_API_KEY"))
has_perplexity = bool(os.getenv("OPENROUTER_API_KEY"))
if not has_parallel and not has_perplexity:
print("Error: No API keys found. Set at least one:", file=sys.stderr)
print(" export PARALLEL_API_KEY='...' (primary - Parallel Chat API)", file=sys.stderr)
print(" export OPENROUTER_API_KEY='...' (fallback - Perplexity academic)", file=sys.stderr)
if output_file:
output_file.close()
return 1
if not args.query and not args.batch:
parser.print_help()
if output_file:
output_file.close()
return 1
try:
research = ResearchLookup(force_backend=args.force_backend)
if args.batch:
print(f"Running batch research for {len(args.batch)} queries...", file=sys.stderr)
results = research.batch_lookup(args.batch)
else:
print(f"Researching: {args.query}", file=sys.stderr)
results = [research.lookup(args.query)]
if args.json:
write_output(json.dumps(results, indent=2, ensure_ascii=False, default=str))
if output_file:
output_file.close()
return 0
for i, result in enumerate(results):
if result["success"]:
write_output(f"\n{'='*80}")
write_output(f"Query {i+1}: {result['query']}")
write_output(f"Timestamp: {result['timestamp']}")
write_output(f"Backend: {result.get('backend', 'unknown')} | Model: {result.get('model', 'unknown')}")
write_output(f"{'='*80}")
write_output(result["response"])
sources = result.get("sources", [])
if sources:
write_output(f"\nSources ({len(sources)}):")
for j, source in enumerate(sources):
title = source.get("title", "Untitled")
url = source.get("url", "")
date = source.get("date", "")
date_str = f" ({date})" if date else ""
write_output(f" [{j+1}] {title}{date_str}")
if url:
write_output(f" {url}")
citations = result.get("citations", [])
text_citations = [c for c in citations if c.get("type") in ("doi", "url")]
if text_citations:
write_output(f"\nAdditional References ({len(text_citations)}):")
for j, citation in enumerate(text_citations):
if citation.get("type") == "doi":
write_output(f" [{j+1}] DOI: {citation.get('doi', '')} - {citation.get('url', '')}")
elif citation.get("type") == "url":
write_output(f" [{j+1}] {citation.get('url', '')}")
if result.get("usage"):
write_output(f"\nUsage: {result['usage']}")
else:
write_output(f"\nError in query {i+1}: {result['error']}")
if output_file:
output_file.close()
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
if output_file:
output_file.close()
return 1
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Pick research-lookup for agent-native scientific search with automatic backend routing; use manual browser research when API keys or parallel-cli are unavailable.
FAQ
Which APIs does research-lookup require?
research-lookup requires parallel-cli as the primary backend. PARALLEL_API_KEY enables deep research via api.parallel.ai, and OPENROUTER_API_KEY enables academic paper search through Perplexity sonar-pro-search on openrouter.ai.
How does research-lookup choose a search backend?
research-lookup automatically routes queries to parallel-cli for fast web search, the Parallel Chat API for deep research, or Perplexity sonar-pro-search for academic papers based on query type and available API keys.