An AI research assistant built to say when the paper doesn't support the answer.
Upload an academic PDF and get a structured summary, a research-gap analysis where every gap carries the evidence it rests on, and a literature review scoped to the prior work the paper itself discusses. A Python/FastAPI backend and a Next.js frontend deployed as one project behind a single origin.
How it flows
- 01Upload a PDF
- 02Validate and extract
- 03Analyse
- 04Read the structured result
Overview
ResearchForge reads an academic PDF and produces three things: a structured summary covering the research problem, methodology, key findings and conclusion; a research-gap analysis where each gap is shown alongside the wording in the paper that supports calling it a gap; and a literature review of the prior work the paper itself discusses.
It is deployed on its own subdomain as a single Vercel project running two services behind one origin.
Problem
The failure mode of an AI summarising tool is not that it produces nothing. It is that it produces something plausible for a section the source never covered: an invented methodology for a position paper, a confidently stated gap with nothing behind it.
For a tool meant to help with research, that failure is worse than no answer at all, because it is indistinguishable from a correct one unless the reader already knows the paper.
Context
This is an independent project, built to work through a problem properly rather than to serve a client: what does it actually take to ship an LLM feature that behaves honestly under real platform constraints?
That framing shaped the scope. It stores nothing, has no accounts, and says so on screen. Those are defensible for a stateless analysis tool and would not be for a product.
Engineering Approach
The founding principle is that every claim must be grounded in the uploaded paper, and it is enforced in three separate places rather than requested once in a prompt. The system prompt states it. The response schema carries fields the model uses to decline, a list naming any section the paper did not support, and a boolean plus explanation where a whole analysis cannot be grounded. The interface prints those fields rather than hiding them.
Structured output is the mechanism, not a convenience. Each response model is converted to a JSON Schema and handed to the model as the required output format, with additional properties forbidden. Every reply is validated on return, and a truncated or malformed answer is refused outright rather than partially rendered.
The three analyses run as three separate model calls. They are different tasks with different evidence rules, so separating them means a failure in one does not corrupt the others, and each can be improved on its own. They run sequentially on purpose: running them in parallel would multiply the peak rate-limit burden for a latency win that does not matter on a single upload.
Architecture
One Vercel project runs two services. A Next.js frontend serves everything except the API, and a FastAPI backend serves /health and /api/*, with routing declared in the project configuration. Because both share one origin, the frontend calls the API with a relative path, which is what makes the custom domain, the .vercel.app domain and every preview URL work from the same build.
Generation sits behind a provider interface. The analysis service depends on that interface and never on a vendor SDK, each vendor's SDK is imported only inside its own provider module, and the concrete provider is built by a factory with a local import so adding one never forces every caller to import every SDK. Switching vendors is one environment variable; adding one is a single new file.
Vendor errors are wrapped in project-owned exception types, with a missing API key separated out from the rest because it is a deployment problem rather than a user's fault and maps to a different status code. Status codes are chosen so the frontend can tell the cases apart without parsing message text: too large, unusable PDF, unusable model reply, no credentials configured. Nothing expected returns a 500.
Document ingestion
An upload is validated twice. The browser checks type, emptiness and size; the backend re-checks the real byte count and verifies the file actually begins with a PDF signature, so a renamed file cannot get through on its content type alone.
Encrypted files get an empty-password decrypt attempt, which covers the common printing-restricted case, and are rejected otherwise rather than guessed at. A file whose extraction yields almost no text is a scanned image with no text layer; it is rejected with an explanation instead of returning an empty result.
Extracted text is cleaned conservatively: ligatures normalised, words rejoined across hyphenated line breaks, runs of spaces collapsed but never across newlines, because paragraph structure is a real signal about where sections begin. Nothing in the cleaning removes content.
Chunking is deliberately conditional. A paper that fits is sent whole, because cross-section reasoning is exactly what gap analysis depends on and chunking would destroy it. Only a genuinely oversized document takes the map step, and its boundaries are chosen at paragraph breaks where possible, falling back to sentence ends and only then to a hard offset.
Testing
186 offline tests, run with pytest. The suite never touches the network and never spends a token: the model provider is replaced by an offline fake through FastAPI's dependency-override mechanism, which is the practical reason the endpoint takes its provider as a dependency rather than constructing one.
Coverage spans the analysis pipeline, both LLM providers, the library schemas, the repository implementation against a mock, the embedding request construction, and the backend foundation. Linting is ruff; a mypy configuration is present.
Technical Decisions
The model tier was chosen against a platform constraint rather than a benchmark. A Vercel function has a 300-second ceiling, and three sequential schema-constrained calls have to complete inside it. A deeper-reasoning model is one environment variable away with no code change.
pypdf was chosen over faster alternatives on licensing. The fastest option ships native binaries and is AGPL, which is incompatible with an MIT repository intended to be read publicly. Only one function touches the library, so swapping it later is a one-function change.
The application is stateless by choice, and that choice is what makes the absence of authentication and rate limiting defensible rather than negligent: there is nothing stored to protect.
Outcome
A working, deployed application that does what it says and states what it does not do. The analysis path is complete and in use; the persistence and retrieval layers are written and tested but not connected, and both the interface and the documentation say so rather than implying otherwise.
What's live, and what isn't
“It exists in the repository” and “it runs in production” are different claims. This is the difference, stated rather than left to be assumed.
- ImplementedPDF upload, validation and extractionLive. Double validation, signature check, encryption and scanned-file rejection.
- ImplementedSummary, gap analysis, literature reviewLive. Three schema-constrained model calls, each validated on return.
- ImplementedProvider abstraction (Gemini, Anthropic)Both providers implemented behind one interface. Gemini is the configured default.
- ImplementedConditional chunking for long papersWhole-document by default; map-reduce only above a configured character threshold.
- Built, not liveResearch library and cross-paper reviewPages, API routes, repository, schemas and prompt are written and tested. Without a connected database every route answers 503 rather than pretending the library is empty.
- Not connectedEmbedding providerThe provider interface and request construction are written and unit-tested offline. The HTTP call is deliberately not implemented, so no embedding is ever produced.
- Not connectedRetrieval / vector searchNot implemented. The analysis path performs no retrieval: it sends the document, not retrieved passages. This is not a RAG system.
- Not connectedDatabase (Supabase / pgvector)Migrations, a storage-independent repository and a Supabase implementation exist and are tested against a mock. No project is connected and nothing is persisted.
Technical stack
Backend
Frontend
AI
Documents
Quality & platform
Limitations
- Nothing is saved. Reloading the tab discards the analysis.
- No OCR, so scanned papers with no text layer are rejected rather than processed.
- The literature review covers only the prior work one paper discusses. It does not search a corpus.
- No authentication and no rate limiting. Acceptable only while the application stores nothing.
- Papers are identified by filename: bibliographic metadata extraction is not implemented.