Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3eaffc6
Merge pull request #101 from Ontos-AI/staging
suguanYang May 19, 2026
599d1dd
Merge pull request #106 from Ontos-AI/staging
suguanYang May 21, 2026
8673f96
Merge pull request #111 from Ontos-AI/staging
suguanYang May 27, 2026
611f51f
Merge pull request #124 from Ontos-AI/staging
suguanYang Jun 2, 2026
4ec45fa
fix: route oversized pdf shards through s3
suguanYang Jun 4, 2026
97e5512
Merge pull request #129 from Ontos-AI/feat/suguan/pdf-split-s3-mineru
suguanYang Jun 4, 2026
ba24a2e
Revert "Route oversized PDF shards through S3 for MinerU"
suguanYang Jun 4, 2026
628a36b
Merge pull request #130 from Ontos-AI/revert-129-feat/suguan/pdf-spli…
suguanYang Jun 4, 2026
5329914
ci: move production release flow to tags
suguanYang Jun 8, 2026
975380f
docs: detail contribution release workflow
suguanYang Jun 8, 2026
bb1ff65
ci: skip deployment image builds on pull requests
suguanYang Jun 8, 2026
8872b57
ci: trigger production deploy from releases
suguanYang Jun 8, 2026
378f36c
docs: hide internal environment branch from public flow
suguanYang Jun 8, 2026
59e7cdc
docs: remove release flow from public docs
suguanYang Jun 8, 2026
125940e
Merge pull request #137 from Ontos-AI/ci/wangbinqi/tag-based-release-…
suguanYang Jun 9, 2026
e86ca03
chore: sync staging into main (#138)
EricNGOntos Jun 9, 2026
1e2d0d7
docs: add long PDF parsing news (#140)
EricNGOntos Jun 9, 2026
d1201c6
feat: unify PDF document profile routing (#141)
EricNGOntos Jun 10, 2026
e6ff72d
feat: page memory parse track, TOC refactor, and usage_task cost trac…
EricNGOntos Jun 11, 2026
e6b9f1d
Add anonymous self-hosted telemetry client (#142)
suguanYang Jun 11, 2026
bfedbe5
Use PostHog SDK for self-hosted telemetry
suguanYang Jun 11, 2026
e6055bd
Add self-hosted aggregate telemetry events
suguanYang Jun 12, 2026
c8db23e
Fix telemetry protocol method bodies
suguanYang Jun 12, 2026
4f8af7b
Set telemetry aggregate default interval to five minutes
suguanYang Jun 12, 2026
18834fd
Fix self-hosted aggregate telemetry review findings
suguanYang Jun 12, 2026
4f3fa6f
fix: update page billing rate
suguanYang Jun 12, 2026
e7e00b0
Merge pull request #146 from Ontos-AI/fix/suguan/update-pricing-copy
suguanYang Jun 12, 2026
d005afe
Merge pull request #145 from Ontos-AI/fix/wangbinqi/posthog-python-sdk
suguanYang Jun 13, 2026
81328b6
Add official library demo catalog
suguanYang Jun 16, 2026
acf60a4
Promote parsed official library docs
suguanYang Jun 16, 2026
f7e71ed
Fix official library catalog performance and paths
suguanYang Jun 16, 2026
3dfc2f3
Fix demo catalog test import isolation
suguanYang Jun 16, 2026
e2a1e9d
Merge pull request #153 from Ontos-AI/feat/wangbinqi/official-library…
suguanYang Jun 16, 2026
607df45
chore: sync main into staging
suguanYang Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 21 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ flowchart TB

This is the typed `ParseOutput` entry for all file types. The parser flow:

1. **Profiles** the document via `profiling.doc_profiler.profile_document()` to detect
file type, page count, and special categories (e.g. `atlas`).
1. **Profiles** the document via `profiling.doc_profiler.profile_document()`. PDF
profiling uses `document_agent` as the single PyMuPDF feature source, then runs
VLM coarse classification with two fields: open semantic `category` (for example,
`Financial Prospectus`) and routing-only `routing_category`
(`atlas/scanned/slides/generic`). Oversized non-atlas PDFs additionally run the
structural anatomy stage once at the entry point and pass the resulting shard
plan to PDF parsing.
2. **Routes** to the appropriate parser based on file extension.
3. **Post-processes**: cleans up unreferenced images, compresses PNG→JPG.
4. Returns typed parse output with task-local artifact paths.
Expand All @@ -134,7 +139,7 @@ This is the typed `ParseOutput` entry for all file types. The parser flow:

| Extension | Parser Module | Strategy |
|:---|:---|:---|
| `.pdf` | `formats.pdf.parser.parse_pdfs` | MinerU API → Markdown parser → `structure.layout_parser.pred_titles` |
| `.pdf` | `formats.pdf.parser.parse_pdfs` | DOC_PROFILE category dispatch: `atlas` → atlas parser; oversized with entry anatomy → shard MinerU; otherwise MinerU API → Markdown parser → `structure.layout_parser.pred_titles` |
| `.docx` | `formats.docx.parser.parse_docx` + `convert_doc2dics` | OXML iteration → heading detection → hierarchical tree |
| `.doc` | `conversion.legacy_converter.doc_to_docx` → `.docx` pipeline | LibreOffice headless conversion first |
| `.pptx` | `formats.pptx.parser.parse_pptx` | iLoveAPI PPTX→PDF → MinerU pipeline |
Expand Down Expand Up @@ -187,12 +192,19 @@ Key logic in `parse_docx()`:
- **Table handling**: `table2html()` converts python-docx Table to HTML with
accurate `rowspan`/`colspan` via direct OXML inspection.

### PDF Parsing: MinerU Pipeline
### PDF Parsing: DOC_PROFILE + MinerU Pipeline

```mermaid
flowchart LR
PDF[formats.pdf.parser] --> MinerU[MinerU Cloud API]
MinerU --> MDFile[Markdown + layout.json]
PDF[profiling.doc_profiler.profile_document] --> Probe[document_agent probe_page_features]
Probe --> Coarse[VLM coarse category]
Coarse -->|atlas| Atlas[formats.atlas.parser]
Coarse -->|oversized generic/scanned/slides| Anatomy[document_agent structural anatomy + shard_plan]
Coarse -->|standard| MinerU[MinerU Cloud API]
Anatomy --> Shards[Shard MinerU pipeline]
Shards --> MDFile[Markdown + layout.json]
Atlas --> Chunks[Atlas page chunks]
MinerU --> MDFile
MDFile --> MDParser[formats.markdown.parser.parse_md]
MDParser --> EvalHeadings[eval_md_headings + layout.json]
EvalHeadings --> PredTitles[structure.layout_parser.pred_titles]
Expand All @@ -205,9 +217,9 @@ flowchart LR
|:---|:---|:---|
| Text/table summarization | `NORMOL_MODEL` | `deepseek-chat` |
| Heading hierarchy recognition | `HIERARCHY_LLM_MODEL` | Falls back to `NORMOL_MODEL` |
| Image description (VLM) | `IMAGE_MODEL` | `qwen3.5-flash` |
| Image OCR / Q&A | `IMAGE_MODEL_MAX` | `qwen3.5-flash` |
| Atlas classification | VLM via `formats.atlas.classifier` | `IMAGE_MODEL` |
| Image description (VLM) | `IMAGE_MODEL` | `qwen3.6-flash` |
| Image OCR / Q&A | `IMAGE_MODEL_MAX` | `qwen3.6-flash` |
| PDF coarse classification | `IMAGE_MODEL` | `qwen3.6-flash` |

---

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ It ingests unstructured documents and produces persistent, navigable memory: par

## 📢 News

- **June 1, 2026**: 📚 **Knowhere now supports ultra-long PDFs and atlas-style documents.** The parsing pipeline can process long-form PDFs with hundreds of pages (for example, 300, 500, or more) and route technical atlases or drawing collections through a dedicated layout-aware parser.
- **May 7, 2026**: 🚀 **Knowhere is now Open Source!** We have open-sourced our entire stack for document ingestion, parsing, and agentic RAG. You can now self-host the full platform using [knowhere-self-hosted](https://github.com/Ontos-AI/knowhere-self-hosted). Check out our [Contribution Guide](CONTRIBUTING.md) to get involved!

## How it Works
Expand Down Expand Up @@ -77,7 +78,7 @@ A: Knowhere uses MinerU as its default parser because it performs best in our te

**Q: What LLM / VLM dependencies does Knowhere have?**

A: By default, DeepSeek (`deepseek-chat`) handles text and table summarization, and Qwen-VL (`qwen3.5-flash`) handles image OCR and descriptions. Knowhere is model-agnostic. Swap in OpenAI, DashScope, Zhipu, or Volcengine via environment variables.
A: By default, DeepSeek (`deepseek-chat`) handles text and table summarization, and Qwen-VL (`qwen3.6-flash`) handles image OCR and descriptions. Knowhere is model-agnostic. Swap in OpenAI, DashScope, Zhipu, or Volcengine via environment variables.

**Q: How is Agentic Retrieval different from traditional RAG?**

Expand Down
16 changes: 14 additions & 2 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ ARK_API_KEY=
# ARK_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions
# NORMOL_MODEL=deepseek-chat
# HIERARCHY_LLM_MODEL=
# IMAGE_MODEL=qwen3.5-flash
# IMAGE_MODEL_MAX=qwen3.5-flash
# IMAGE_MODEL=qwen3.6-flash
# IMAGE_MODEL_MAX=qwen3.6-flash

# Optional retrieval overrides have code defaults. Retrieval is evidence-only:
# evidence_text is the primary output and answer_text is always empty. Set
Expand All @@ -95,7 +95,19 @@ MAX_FILE_SIZE=314572800
MAX_PDF_PAGE_LIMIT=200
OVERSIZED_PDF_SHARD_ENABLED=true
OVERSIZED_PDF_SOFT_LIMIT=1500
PDF_PAGE_TOC_ENABLED=true
RETRIEVAL_PAGE_MEMORY_ENABLED=false
MINERU_SHARD_CONCURRENCY=3
PARSE_AGENT_PLAN_BUDGET=50000
PARSE_AGENT_VISUAL_BUDGET=80000
PARSE_AGENT_TOC_CONFIRM_MIN_BUDGET=8000
PARSE_AGENT_TOC_CONFIRM_CAP=24000
PARSE_AGENT_COARSE_PLANNER_MIN_BUDGET=12000
PARSE_AGENT_COARSE_PLANNER_CAP=36000
PARSE_AGENT_STRUCTURAL_REACT_MIN_BUDGET=24000
PARSE_AGENT_STRUCTURAL_REACT_CAP=64000
PARSE_AGENT_PAGE_TAGGING_MIN_BUDGET=0
PARSE_AGENT_PAGE_TAGGING_CAP=0

# Required for specific features: webhooks and callbacks
WEBHOOK_MASTER_KEY=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add doc profile to document page plan

Revision ID: f9a0b1c2d3e4
Revises: f8a9b0c1d2e3
Create Date: 2026-06-11 09:50:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "f9a0b1c2d3e4"
down_revision: Union[str, Sequence[str], None] = "f8a9b0c1d2e3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"document_page_plan",
sa.Column("doc_profile", sa.JSON(), nullable=True),
)


def downgrade() -> None:
op.drop_column("document_page_plan", "doc_profile")
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""add parse track to documents

Revision ID: f9b0c1d2e3f4
Revises: f9a0b1c2d3e4
Create Date: 2026-06-11 10:05:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "f9b0c1d2e3f4"
down_revision: Union[str, Sequence[str], None] = "f9a0b1c2d3e4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"documents",
sa.Column(
"parse_track",
sa.String(length=32),
nullable=False,
server_default="chunk",
),
)
op.alter_column("documents", "parse_track", server_default=None)


def downgrade() -> None:
op.drop_column("documents", "parse_track")
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Add retrieval step timestamp index.

Revision ID: f9c0d1e2f3a4
Revises: f9b0c1d2e3f4
Create Date: 2026-06-12 08:35:00.000000
"""

from __future__ import annotations

from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = "f9c0d1e2f3a4"
down_revision: Union[str, Sequence[str], None] = "f9b0c1d2e3f4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_index(
"idx_retrieval_steps_created",
"retrieval_steps",
["created_at"],
unique=False,
)


def downgrade() -> None:
op.drop_index("idx_retrieval_steps_created", table_name="retrieval_steps")
2 changes: 1 addition & 1 deletion apps/api/app/api/v1/routes/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
router = APIRouter(tags=["Documents"])

_document_service = DocumentService()
DocumentChunkType = Literal["text", "image", "table"]
DocumentChunkType = Literal["text", "image", "table", "page"]


async def _archive_document_response(
Expand Down
37 changes: 37 additions & 0 deletions apps/api/app/core/middleware/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Anonymous aggregate telemetry middleware."""

from __future__ import annotations

import time

from starlette.types import ASGIApp, Message, Receive, Scope, Send

from shared.services.telemetry.api_metrics import ApiRequestTelemetryMetrics


class ApiTelemetryMiddleware:
"""Record bounded API request metrics without request payloads or raw paths."""

def __init__(self, app: ASGIApp, *, metrics: ApiRequestTelemetryMetrics) -> None:
self.app = app
self.metrics = metrics

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return

status_code = 500
started_at = time.perf_counter()

async def send_wrapper(message: Message) -> None:
nonlocal status_code
if message["type"] == "http.response.start":
status_code = int(message.get("status", 500))
await send(message)

try:
await self.app(scope, receive, send_wrapper)
finally:
elapsed_ms = (time.perf_counter() - started_at) * 1000
self.metrics.record(status_code=status_code, latency_ms=elapsed_ms)
Loading
Loading