Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
22fa82f
refactor: deepen service and retrieval architecture
suguanYang May 15, 2026
edcda23
refactor: deepen apps/api workflow seams
suguanYang May 15, 2026
492f0db
refactor: split stripe billing workflows
suguanYang May 15, 2026
2881263
refactor: split api key workflows
suguanYang May 15, 2026
917fb75
refactor: extract stripe refund reconciliation
suguanYang May 15, 2026
f1fef67
refactor: split document ingestion workflows
suguanYang May 16, 2026
69143f8
refactor: split job admission policies
suguanYang May 16, 2026
7c47d11
refactor: extract stripe credits settlement
suguanYang May 16, 2026
25556d4
refactor: package document ingestion workflow
suguanYang May 16, 2026
85ba461
refactor: package job read workflows
suguanYang May 16, 2026
4d970c1
refactor: remove GitHub flow test file
suguanYang May 16, 2026
1d8f88f
refactor: split worker ingestion and parser orchestration
suguanYang May 16, 2026
459b5c8
refactor: consolidate job storage helpers
suguanYang May 16, 2026
53811a7
refactor: align sync state machine payloads
suguanYang May 16, 2026
a4be52b
refactor: centralize job result delivery
suguanYang May 16, 2026
8c2985c
refactor: centralize retrieval asset projection
suguanYang May 16, 2026
857351a
refactor: deepen job file storage interface
suguanYang May 16, 2026
b15bf50
refactor: remove api state machine facade
suguanYang May 16, 2026
080821c
refactor deepen workflow modules
suguanYang May 17, 2026
d6f737b
refactor deepen retrieval and ingestion modules
suguanYang May 17, 2026
2e29103
refactor deepen job lifecycle and upload handoff
suguanYang May 17, 2026
f8c16e9
refactor deepen workflow and demo projections
suguanYang May 17, 2026
b3e51da
refactor deepen agentic and zip modules
suguanYang May 17, 2026
fa8f553
refactor deepen webhook delivery modules
suguanYang May 17, 2026
f05e0e7
refactor split retrieval hydration modules
suguanYang May 17, 2026
e00679a
refactor split retrieval graph modules
suguanYang May 17, 2026
0c73c9c
refactor extract agentic selection hydration
suguanYang May 17, 2026
d740fb1
refactor extract legacy retrieval route
suguanYang May 17, 2026
dad7e46
refactor deepen qstash webhook publisher
suguanYang May 17, 2026
defaff8
refactor extract retrieval lexical ranker
suguanYang May 17, 2026
4641a35
refactor extract agentic section counts
suguanYang May 17, 2026
ab803cf
refactor extract agentic section prompt projection
suguanYang May 17, 2026
3dbfa96
refactor move agentic asset availability
suguanYang May 17, 2026
06cf7d6
refactor split agentic discovery selection
suguanYang May 17, 2026
76c0316
refactor share agentic asset scope loading
suguanYang May 17, 2026
0a6e55c
refactor(worker): deepen parser architecture
suguanYang May 17, 2026
d146c64
refactor(worker): deepen excel parser contract
suguanYang May 17, 2026
5dbf048
refactor(worker): deepen markdown table parser modules
suguanYang May 17, 2026
e4462eb
refactor(api): deepen demo source materialization
suguanYang May 17, 2026
a6c99d7
refactor: remove dead code
suguanYang May 17, 2026
d6de8c5
refactor: deepen workflow architecture
suguanYang May 18, 2026
1e9c148
refactor: deepen terminal workflow seams
suguanYang May 18, 2026
26aa103
refactor: deepen workflow policy seams
suguanYang May 18, 2026
bbd2fe0
fix: resolve refactor review blockers
suguanYang May 18, 2026
6fdf0da
fix: satisfy demo source projection typing
suguanYang May 18, 2026
788965f
fix: use explicit protocol stubs
suguanYang May 18, 2026
83c62f1
test: remove unit-style contract tests
suguanYang May 18, 2026
9dbd5af
test: keep contract suite at behavior boundaries
suguanYang May 18, 2026
30bbbb1
test: fix contract test lint
suguanYang May 18, 2026
7006127
chore: remove empty service package modules
suguanYang May 18, 2026
b6591a7
refactor: organize shared utilities by domain
suguanYang May 18, 2026
b7e344f
refactor: put shared quota base in neutral module
suguanYang May 18, 2026
3252cb8
Revert "refactor: put shared quota base in neutral module"
suguanYang May 18, 2026
1496638
Revert "refactor: organize shared utilities by domain"
suguanYang May 18, 2026
bf7d0e9
refactor: organize shared utility ownership seams
suguanYang May 18, 2026
8a98eb9
refactor: remove internal compatibility wrappers
suguanYang May 18, 2026
95a8fde
refactor: group demo services by feature
suguanYang May 18, 2026
16dda96
refactor: remove empty app package markers
suguanYang May 18, 2026
7806117
refactor: remove init-only modules
suguanYang May 18, 2026
bb334ff
refactor: remove unused image client bootstrap
suguanYang May 18, 2026
4249d6c
refactor: move current user auth out of core
suguanYang May 18, 2026
74e77fe
refactor: isolate job admission request adapters
suguanYang May 18, 2026
e585654
refactor: return qstash callback outcomes from workflow
suguanYang May 18, 2026
74d1a2e
refactor: group document lifecycle service
suguanYang May 18, 2026
afc7f5a
refactor: group shared job lifecycle services
suguanYang May 18, 2026
b8ee509
docs: refresh architecture context map
suguanYang May 18, 2026
189b71c
refactor: group webhook callback services
suguanYang May 18, 2026
410734b
refactor: structure worker document parser modules
suguanYang May 18, 2026
607b7ea
refactor: group retrieval internals by ownership
suguanYang May 18, 2026
9d1d635
refactor: structure agentic retrieval modules
suguanYang May 18, 2026
d1cc76f
refactor: split current user dependency from job admission
suguanYang May 18, 2026
cf8e029
refactor: rename document ingestion worker dispatch
suguanYang May 18, 2026
3b7b4f5
refactor: replace parser kb dir with namespace
suguanYang May 18, 2026
5835ac0
refactor: decouple parser paths from namespace
suguanYang May 18, 2026
bcbabdc
refactor: deepen document processing modules
suguanYang May 18, 2026
addb331
refactor: deepen lifecycle and packaging modules
suguanYang May 18, 2026
6d4f57b
fix: address advanced security review comments
suguanYang May 18, 2026
96655cf
fix: clear remaining codeql alerts
suguanYang May 18, 2026
4cbf388
fix: restrict agentic vlm inputs to images
suguanYang May 19, 2026
a0bdc5d
fix: add mineru upload mode flag
suguanYang May 19, 2026
4b3917a
fix: drop legacy mineru upload flag
suguanYang May 19, 2026
2a5720a
test: cover mineru mode through parser contract
suguanYang May 19, 2026
72b4d01
test: remove mineru upload mode contract
suguanYang May 19, 2026
7e87f04
fix: support python 3.14 dependency resolution
suguanYang May 19, 2026
0f74acc
Resolve Issue #96 (#97)
EricNGOntos May 19, 2026
2234234
fix: configure pdf page limit
suguanYang May 19, 2026
06c0ac2
chore: sync main history into staging
suguanYang May 19, 2026
55c59fb
Merge pull request #102 from Ontos-AI/sync/wangbinqi/main-into-stagin…
suguanYang May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ ARK_API_KEY=
# File handling defaults
SUPPORTED_EXTENSIONS=.doc,.docx,.pdf,.txt,.xls,.xlsx,.csv,.pptx,.jpg,.jpeg,.png,.md
MAX_FILE_SIZE=104857600
MAX_PDF_PAGE_LIMIT=600

# Required for specific features: webhooks and callbacks
WEBHOOK_MASTER_KEY=
Expand All @@ -115,7 +116,9 @@ ILOVEAPI_SECRET_KEY=
# MINERU_TOKEN_RPM_LIMIT=300
# MINERU_TOKEN_DAILY_LIMIT=10000
# MINERU_TOKEN_COOLDOWN_SECONDS=60
# FORCE_MINERU_UPLOAD_ENABLED=false
# False prefers S3 URL mode when the source object is reusable. Set true only
# when MinerU cannot fetch your storage URLs and direct upload is required.
# MINERU_UPLOAD_MODE_ENABLED=false
# ILOVEAPI_BASE_URL=https://api.ilovepdf.com/v1
# ILOVEAPI_TIMEOUT=120

Expand Down
7 changes: 7 additions & 0 deletions apps/api/app/api/v1/routes/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
router = APIRouter(tags=["Retrieval"])


def _is_none(value: object) -> bool:
return value is None


class ExcludeSection(BaseModel):
document_id: str
section_path: str
Expand Down Expand Up @@ -83,6 +87,9 @@ class RetrievalQueryResponse(BaseModel):
answer_text: str | None = None
referenced_chunks: list[dict] = Field(default_factory=list)
results: list[dict] = Field(default_factory=list)
evidence_text: str | None = Field(default=None, exclude_if=_is_none)
stop_reason: str | None = Field(default=None, exclude_if=_is_none)
failure_reason: str | None = Field(default=None, exclude_if=_is_none)


@router.post("/query", response_model=RetrievalQueryResponse)
Expand Down
4 changes: 2 additions & 2 deletions apps/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "knowhere-api-app"
version = "0.1.0"
description = "Knowhere API service"
requires-python = ">=3.11,<3.14"
requires-python = ">=3.11"
dependencies = [
"knowhere-shared",
"fastapi==0.135.1",
Expand All @@ -13,7 +13,7 @@ dependencies = [
"redis==5.3.1",
"celery==5.4.0",
"stripe==13.0.1",
"pydantic==2.11.7",
"pydantic==2.13.4",
"PyJWT==2.12.0",
"qstash==3.2.0",
"httpx==0.28.1",
Expand Down
129 changes: 126 additions & 3 deletions apps/api/tests/contract/test_retrieval_contract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections.abc import Callable
from collections.abc import Callable, Coroutine, Sequence
from contextlib import AbstractAsyncContextManager
from datetime import datetime, timezone
from typing import cast
from typing import Any, cast
from uuid import uuid4

import pytest
Expand All @@ -14,6 +14,9 @@
from shared.services.retrieval.workflow.run_request import WorkflowRunRequest
from shared.services.retrieval.workflow.types import PlannedStep, QueryPlan, WorkflowResult

LLMFnInput = str | Sequence[dict[str, Any]]
LLMFn = Callable[[LLMFnInput], Coroutine[Any, Any, str]]


async def _seed_retrieval_document(
*,
Expand All @@ -23,6 +26,9 @@ async def _seed_retrieval_document(
section_path: str,
content: str,
chunk_id: str | None = None,
chunk_type: str = "text",
file_path: str | None = None,
chunk_metadata: dict[str, Any] | None = None,
) -> dict[str, str]:
document_id = f"doc_{uuid4().hex[:12]}"
job_id = f"job_{uuid4().hex[:12]}"
Expand Down Expand Up @@ -80,9 +86,11 @@ async def _seed_retrieval_document(
document_id=document_id,
job_result_id=job_result_id,
section_id=section_id,
chunk_type="text",
chunk_type=chunk_type,
content=content,
section_path=section_path,
file_path=file_path,
chunk_metadata=chunk_metadata,
)

return {
Expand Down Expand Up @@ -600,6 +608,121 @@ async def test_agentic_retrieval_should_reference_discovery_content_when_navigat
}


@pytest.mark.asyncio
async def test_agentic_retrieval_should_not_send_table_artifacts_to_vlm(
developer_api_client_factory: Callable[
[], AbstractAsyncContextManager[AsyncClient]
],
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.setenv("LLM_MOCK_ENABLED", "true")
vlm_calls: list[LLMFnInput] = []

async def fake_vlm(prompt: LLMFnInput) -> str:
vlm_calls.append(prompt)
return '{"status":"DONE","answer":"unexpected table VLM answer"}'

def fake_create_retrieval_vlm_fn(**_kwargs: object) -> LLMFn:
return fake_vlm

class FakeResultStorage:
def generate_artifact_url(
self,
*,
job_id: str,
artifact_ref: str,
expires_in: int = 3600,
) -> str | None:
del expires_in
return f"https://assets.example.com/{job_id}/{artifact_ref}"

def normalize_artifact_ref(self, artifact_ref: str | None) -> str | None:
if not artifact_ref:
return None
normalized = artifact_ref.strip().replace("\\", "/").lstrip("/")
if not normalized:
return None
root_dir = normalized.split("/", 1)[0]
if root_dir not in {"images", "tables"}:
return None
return normalized

def fake_get_result_storage() -> FakeResultStorage:
return FakeResultStorage()

async with developer_api_client_factory() as api_client:
monkeypatch.setattr(
"shared.services.retrieval.llm_adapter.create_retrieval_vlm_fn",
fake_create_retrieval_vlm_fn,
)
monkeypatch.setattr(
"shared.services.retrieval.hydration.assets.get_result_storage",
fake_get_result_storage,
)
table_document = await _seed_retrieval_document(
user_id="local-dev-user",
namespace="contract-agentic-table-vlm-filter",
source_file_name="table-report.md",
section_path="Realdata Results Summary / Main Metrics",
content=(
"<table><tr><th>budget</th><th>metric</th><th>value</th></tr>"
"<tr><td>1000</td><td>Flat inspect_evidence_score_mean</td>"
"<td>0.5674</td></tr></table>"
),
chunk_type="table",
file_path="tables/table-0-main-metrics.html",
)
await _seed_retrieval_document(
user_id="local-dev-user",
namespace="contract-agentic-table-vlm-filter",
source_file_name="filler-table-report.md",
section_path="Appendix / Filler Metrics",
content=(
"<table><tr><th>metric</th><th>value</th></tr>"
"<tr><td>unrelated filler metric</td><td>999</td></tr></table>"
),
chunk_type="table",
file_path="tables/table-1-filler-metrics.html",
)

response = await api_client.post(
"/api/v1/retrieval/query",
json={
"namespace": "contract-agentic-table-vlm-filter",
"query": "budget 1000 Flat inspect_evidence_score_mean",
"top_k": 1,
"data_type": 4,
"use_agentic": True,
},
)

assert response.status_code == 200

response_json = cast(dict[str, object], response.json())
referenced_chunks = cast(list[dict[str, object]], response_json["referenced_chunks"])
results = cast(list[dict[str, object]], response_json["results"])

assert response_json["router_used"] == "workflow_single_step"
assert vlm_calls == []
matching_references = [
reference
for reference in referenced_chunks
if reference["chunk_id"] == table_document["chunk_id"]
]
assert len(matching_references) == 1
assert matching_references[0]["document_id"] == table_document["document_id"]
assert matching_references[0]["chunk_type"] == "table"
assert matching_references[0]["section_path"] == table_document["section_path"]
assert matching_references[0]["file_path"] == "tables/table-0-main-metrics.html"
assert matching_references[0]["job_id"] == table_document["job_id"]
assert str(matching_references[0]["asset_url"]).startswith(
"https://assets.example.com/"
)
assert len(results) == 1
assert results[0]["chunk_type"] == "table"
assert _result_source(results[0])["document_id"] == table_document["document_id"]


@pytest.mark.asyncio
async def test_agentic_retrieval_should_not_hydrate_references_outside_request_scope(
developer_api_client_factory: Callable[
Expand Down
6 changes: 4 additions & 2 deletions apps/worker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,17 @@ ILOVEAPI_SECRET_KEY=
# MINERU_TOKEN_RPM_LIMIT=300
# MINERU_TOKEN_DAILY_LIMIT=10000
# MINERU_TOKEN_COOLDOWN_SECONDS=60
# FORCE_MINERU_UPLOAD_ENABLED=false
# False prefers S3 URL mode when the source object is reusable. Set true only
# when MinerU cannot fetch your storage URLs and direct upload is required.
# MINERU_UPLOAD_MODE_ENABLED=false
# ILOVEAPI_BASE_URL=https://api.ilovepdf.com/v1
# ILOVEAPI_TIMEOUT=120

# File handling defaults
SUPPORTED_EXTENSIONS=.doc,.docx,.pdf,.txt,.xls,.xlsx,.csv,.pptx,.jpg,.jpeg,.png,.md
MAX_FILE_SIZE=104857600
MAX_PDF_PAGE_LIMIT=600

# Legacy parser compatibility fields.
# ALL_DF_COLS=content,path,type,length,keywords,summary,know_id,tokens,connectto,addtime,page_nums
# SPLIT_CHAR=/

Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
from shared.core.config import settings
from shared.core.exceptions.domain_exceptions import ValidationException

PDF_PAGE_LIMIT = 600


@dataclass(frozen=True)
class ParseSession:
Expand Down Expand Up @@ -111,16 +109,17 @@ def build_parse_session(parse_input: ParseInput) -> ParseSession:
f"ℹ️ VLM rejected atlas for {parse_input.filename}, routing as generic"
)

if profile.file_type == "pdf" and profile.page_count > PDF_PAGE_LIMIT:
pdf_page_limit = settings.MAX_PDF_PAGE_LIMIT
if profile.file_type == "pdf" and profile.page_count > pdf_page_limit:
raise ValidationException(
user_message=(
f"Document too large: {profile.page_count} pages exceeds the {PDF_PAGE_LIMIT}-page limit. "
f"Document too large: {profile.page_count} pages exceeds the {pdf_page_limit}-page limit. "
"Please split the document and upload in smaller batches."
),
violations=[
{
"field": "page_count",
"description": f"PDF has {profile.page_count} pages, limit is {PDF_PAGE_LIMIT}",
"description": f"PDF has {profile.page_count} pages, limit is {pdf_page_limit}",
}
],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@


def _should_use_mineru_s3_url_mode(s3_key: Optional[str]) -> bool:
if settings.FORCE_MINERU_UPLOAD_ENABLED:
return False

return settings.ENVIRONMENT != "development" and s3_key is not None
return not settings.MINERU_UPLOAD_MODE_ENABLED and s3_key is not None


def _log_mineru_url_mode_storage_fallback(
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "knowhere-worker-app"
version = "0.1.0"
description = "Knowhere Worker service"
requires-python = ">=3.11,<3.14"
requires-python = ">=3.11"
dependencies = [
"knowhere-shared",
"numpy==2.2.6",
Expand Down
16 changes: 7 additions & 9 deletions apps/worker/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ argon2-cffi-bindings==25.1.0
# via argon2-cffi
asgiref==3.11.1
# via opentelemetry-instrumentation-asgi
async-timeout==5.0.1 ; python_full_version < '3.12'
# via
# asyncpg
# redis
asyncpg==0.29.0
async-timeout==5.0.1 ; python_full_version < '3.11.3'
# via redis
asyncpg==0.31.0
# via knowhere-shared
attrs==26.1.0
# via aiohttp
Expand Down Expand Up @@ -333,24 +331,24 @@ psycogreen==1.0.2
# via
# knowhere-shared
# knowhere-worker-app
psycopg2-binary==2.9.9
psycopg2-binary==2.9.12
# via knowhere-shared
pwdlib==0.3.0
# via fastapi-users
pycparser==3.0 ; implementation_name != 'PyPy'
# via cffi
pycryptodome==3.23.0
# via oss2
pydantic==2.11.7
pydantic==2.13.4
# via
# fastapi
# knowhere-shared
# openai
# pptx2md
# pydantic-settings
pydantic-core==2.33.2
pydantic-core==2.46.4
# via pydantic
pydantic-settings==2.10.1
pydantic-settings==2.14.1
# via knowhere-shared
pygments==2.20.0
# via
Expand Down
Loading
Loading