paragraph-summary-service generates Gemini-backed one-sentence paragraph summary artifacts for DeepReader retrieval enhancement.
DeepReader keeps the original paragraph records as the citation-grade evidence. This service takes those already-ingested paragraph records, asks a configured LLM provider to produce one sentence per paragraph, then writes a JSONL sidecar artifact that DeepReader can index alongside the original text.
Gemini is the primary real provider path for the project. The deterministic mock provider exists so the API, cache, artifact writer, redaction, batching, tests, CI, and local demos can run without network access or API keys.
Paragraph Summary Service is DeepReader's companion summary execution service, not a RAG/search/dashboard/ingestion system.
DeepReader stable paragraph records
→ Paragraph Summary Service
→ Gemini one-sentence paragraph summaries
→ JSONL sidecar artifact
→ DeepReader indexes original text + summaries
- provider/mock paragraph summary generation execution
- prompt/provider abstraction
- batching (token-aware packing)
- redaction (input text + metadata)
- cache behavior (text-hash-based SQLite cache)
- usage metadata (token counts, estimated cost)
- provider safety (opt-in external calls, safe defaults)
- JSONL/sidecar artifact generation
This service is intentionally narrow. DeepReader remains the canonical owner of:
- document ingestion and source records
- retrieval, search, and RAG pipelines
- QA citations and evidence packets
- frontend dashboard and inspection tooling
- summary indexing and secondary field management
Live provider calls are opt-in. The safe default for reviewers, tests, and CI:
LLM_PROVIDER=mock
ALLOW_EXTERNAL_PROVIDER_CALLS=falseThis keeps the project reviewable without credentials, network access, or quota. See docs/DEMO_WORKFLOW.md for the reviewer-safe offline workflow.
There are two provider paths:
| Provider | Role | Network/API key | Intended use |
|---|---|---|---|
gemini |
Primary real provider | Yes | Actual one-sentence paragraph summary generation |
mock |
Deterministic offline provider | No | Tests, CI, local demos, pipeline validation |
The service is safe-by-default:
LLM_PROVIDER=mock
ALLOW_EXTERNAL_PROVIDER_CALLS=falseThat default does not mean mock is the product goal. It means a reviewer can run the full pipeline locally without credentials, and tests never spend quota.
Production-style Gemini mode must be explicit:
LLM_PROVIDER=gemini
ALLOW_EXTERNAL_PROVIDER_CALLS=true
GEMINI_API_KEY=your_authorised_key_here
GEMINI_MODEL=gemini-3.1-flash-lite
GEMINI_INPUT_TPM_LIMIT=250000
GEMINI_SAFE_INPUT_TOKEN_TARGET=225000
GEMINI_REQUESTS_PER_MINUTE=1
GEMINI_TIMEOUT_SECONDS=90Use only API keys and projects you are authorised to use, and configure token/request limits to match the provider dashboard for your account or project.
DeepReader records ─┐
├─> shared paragraph summary pipeline ─> JSONL sidecar artifact
File upload adapter ┘
The canonical integration endpoint accepts already-ingested DeepReader-style paragraph records:
POST /paragraph-summariesThe standalone demo endpoint accepts .txt and .md uploads, creates provisional records, and uses the same summary pipeline:
POST /paragraph-summaries/from-fileThe file upload endpoint is a convenience adapter, not a replacement for DeepReader ingestion. DeepReader should remain the source of truth for canonical record IDs, source references, hashes, and citations.
Artifacts are written under SUMMARY_ARTIFACT_DIR, defaulting to:
output/paragraph_summaries/<document_id>.paragraph_summaries.jsonl
Each JSONL line contains record IDs, source references, text hashes, summary text, provider/model metadata, template version, cache status, usage, status, and provenance. Raw paragraph text is not written to artifacts by default.
Example line:
{
"artifact_id": "artifact_doc_001_6204e91faa92_1782289345276",
"document_id": "doc_001",
"record_id": "doc_001/page_0001/para_0001",
"source_ref": "page 1, paragraph 1",
"input_text_hash": "sha256:abc123...",
"summary": "The paragraph explains how stable paragraph records improve retrieval while preserving citation evidence.",
"summary_style": "paragraph_sentence",
"template_version": "paragraph_sentence_batch_v1",
"provider": "gemini",
"model": "gemini-3.1-flash-lite",
"cache_hit": false,
"status": "completed",
"runtime_mode": "mock",
"provenance": "deepreader"
}python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e ".[dev]"
uvicorn paragraph_summary_service.api.app:create_app --factory --reloadIn another terminal:
curl -X POST http://localhost:8000/paragraph-summaries \
-H "Content-Type: application/json" \
-d @examples/paragraph_request.jsonUpload demo file:
curl -F "file=@examples/sample.md" \
-F "provider=mock" \
-F "template_version=paragraph_sentence_batch_v1" \
http://localhost:8000/paragraph-summaries/from-fileDownload the generated artifact:
curl http://localhost:8000/artifacts/<artifact_id>The default local path uses the deterministic mock provider, so the service can be exercised without network access, quota usage, or API keys.
Start the API server:
.venv/bin/uvicorn paragraph_summary_service.api.app:create_app --factory --reloadCheck service health:
curl -sS http://localhost:8000/health | jqExample response:
{
"status": "ok",
"service": "Paragraph Summary Service",
"version": "0.1.1",
"provider_default": "mock",
"external_provider_calls_enabled": false
}Generate paragraph summary artifacts from canonical DeepReader-shaped records:
curl -sS -X POST http://localhost:8000/paragraph-summaries \
-H "Content-Type: application/json" \
-d @examples/paragraph_request.json | jqFirst run example:
{
"artifact_id": "artifact_doc_001_6204e91faa92_1782289345276",
"document_id": "doc_001",
"output_path": "output/paragraph_summaries/doc_001.paragraph_summaries.jsonl",
"records_received": 2,
"records_completed": 2,
"records_failed": 0,
"records_skipped": 0,
"cache_hits": 0,
"provider": "mock",
"model": "mock-deterministic-v1",
"template_version": "paragraph_sentence_batch_v1",
"runtime_mode": "mock",
"created_at": "2026-07-02T11:15:00Z",
"usage": {
"input_tokens": 66,
"output_tokens": 42,
"total_tokens": 108,
"estimated_cost_usd": 0.0
},
"batches_processed": 1
}Inspect the generated JSONL sidecar artifact:
cat output/paragraph_summaries/doc_001.paragraph_summaries.jsonl | jq -c .Example JSONL line:
{
"artifact_id": "artifact_doc_001_6204e91faa92_1782289345276",
"cache_hit": false,
"document_id": "doc_001",
"error_code": null,
"input_text_hash": "sha256:a9dc3322bf37fa60292b5a71d4e675de3b80a9e2a12b7cb05f3509b666ed4546",
"metadata": {
"page": 1,
"section": "Safety Model"
},
"model": "mock-deterministic-v1",
"provenance": "deepreader",
"provider": "mock",
"record_id": "doc_001/page_0001/para_0001",
"runtime_mode": "mock",
"source_ref": "page 1, paragraph 1",
"status": "completed",
"summary": "The service must not log raw source text.",
"summary_style": "paragraph_sentence",
"template_version": "paragraph_sentence_batch_v1",
"usage": {
"estimated_cost_usd": 0.0,
"input_tokens": 33,
"output_tokens": 21,
"total_tokens": 54
}
}Run the same request again to demonstrate cache reuse:
curl -sS -X POST http://localhost:8000/paragraph-summaries \
-H "Content-Type: application/json" \
-d @examples/paragraph_request.json | jqCache-hit response excerpt:
{
"records_received": 2,
"records_completed": 2,
"records_failed": 0,
"cache_hits": 2,
"provider": "mock",
"model": "mock-deterministic-v1",
"template_version": "paragraph_sentence_batch_v1",
"batches_processed": 0
}The artifact contains stable IDs, hashes, summaries, provenance, provider/model details, status, usage, and redacted metadata. It does not include raw paragraph text.
Create .env from .env.example, then enable Gemini intentionally:
cp .env.example .envLLM_PROVIDER=gemini
ALLOW_EXTERNAL_PROVIDER_CALLS=true
GEMINI_API_KEY=your_authorised_key_here
GEMINI_MODEL=gemini-3.1-flash-lite
GEMINI_INPUT_TPM_LIMIT=250000
GEMINI_SAFE_INPUT_TOKEN_TARGET=225000
GEMINI_REQUESTS_PER_MINUTE=1Then call the same /paragraph-summaries endpoint with provider: "gemini" or omit the provider field if your request schema is configured to default to the environment provider in a future version.
The current implementation packs records by estimated input tokens and targets GEMINI_SAFE_INPUT_TOKEN_TARGET, leaving margin below GEMINI_INPUT_TPM_LIMIT for prompt overhead, tokenizer mismatch, and response tokens.
{
"document_id": "doc_001",
"records": [
{
"record_id": "doc_001/page_0001/para_0001",
"source_ref": "page 1, paragraph 1",
"text": "The original paragraph text...",
"checksum": "sha256:abc123",
"metadata": {"page": 1, "section": "Introduction"},
"provenance": "deepreader"
}
],
"provider": "gemini",
"model": "gemini-3.1-flash-lite",
"template_version": "paragraph_sentence_batch_v1"
}For offline local testing, use provider: "mock" and model: "mock-deterministic-v1".
{
"artifact_id": "artifact_doc_001_...",
"document_id": "doc_001",
"output_path": "output/paragraph_summaries/doc_001.paragraph_summaries.jsonl",
"records_received": 1,
"records_completed": 1,
"records_failed": 0,
"records_skipped": 0,
"cache_hits": 0,
"provider": "gemini",
"model": "gemini-3.1-flash-lite",
"template_version": "paragraph_sentence_batch_v1",
"runtime_mode": "live",
"created_at": "2026-07-02T11:15:00Z",
"usage": {
"input_tokens": 12,
"output_tokens": 8,
"total_tokens": 20,
"estimated_cost_usd": 0.0
},
"batches_processed": 1
}Logs may include artifact IDs, document IDs, provider names, model names, template versions, text hashes, record counts, cache hit counts, duration, and status.
Logs must not include raw paragraph text, full prompts, API keys, credentials, or unredacted sensitive metadata.
External LLM calls require both:
LLM_PROVIDER=gemini
ALLOW_EXTERNAL_PROVIDER_CALLS=trueand a configured GEMINI_API_KEY. This prevents accidental quota use during development, tests, and CI.
python3 -m pytest
python3 -m ruff check src testsThe tests are fully offline and use the deterministic mock provider.
docker compose up --buildSee docs/architecture.md, docs/gemini-provider.md, and docs/deepreader-integration.md for design notes.
This project is licensed under the MIT License. See LICENSE.