Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ NORMOL_MODEL=deepseek-chat
HIERARCHY_LLM_MODEL=qwen3.6-flash
IMAGE_MODEL=qwen3.5-flash
IMAGE_MODEL_MAX=qwen3.5-flash
RETRIEVAL_DECOMPOSITION_ENABLED=false
RETRIEVAL_PLANNER_MODEL=
RETRIEVAL_PLANNER_THINKING_BUDGET=4000
RETRIEVAL_DECOMPOSITION_MAX_STEPS=5
RETRIEVAL_WALLET_TOTAL_BUDGET=200000
RETRIEVAL_WALLET_PER_RETRIEVE_STEP_BUDGET=40000
RETRIEVAL_WALLET_PER_SYNTHESIZE_STEP_BUDGET=6000
RETRIEVAL_WORKFLOW_PARALLEL_MAX=3

# File handling defaults
SUPPORTED_EXTENSIONS=.doc,.docx,.pdf,.txt,.xls,.xlsx,.csv,.pptx,.jpg,.jpeg,.png,.md
Expand Down
36 changes: 35 additions & 1 deletion apps/api/app/api/v1/routes/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class RetrievalQueryRequest(BaseModel):
internal_recall_k: int | None = Field(
None, ge=1, description="Override per-channel recall count"
)
enable_decomposition: bool | None = Field(
None,
description="Deprecated: agentic mode now always uses workflow decomposition. This field is ignored.",
deprecated=True,
)

@field_validator("channels")
@classmethod
Expand All @@ -63,7 +68,35 @@ def validate_channels(cls, v: list[str]) -> list[str]:
return v


@router.post("/query")
class WorkflowStepResponse(BaseModel):
step_id: str
sub_query: str
step_kind: Literal["retrieve", "synthesize"]
depends_on: list[str]
output_role: str
status: Literal["done", "skipped", "error", "budget_stop"]
answer_text: str
evidence_text: str | None = None
referenced_chunks: list[dict] = Field(default_factory=list)
budget_snapshot: dict | None = None
child_run_id: str | None = None


class RetrievalQueryResponse(BaseModel):
namespace: str
query: str
router_used: str
answer_text: str | None = None
referenced_chunks: list[dict] = Field(default_factory=list)
results: list[dict] = Field(default_factory=list)
plan: dict | None = None
steps: list[WorkflowStepResponse] | None = None
final_strategy_used: str | None = None
wallet_snapshot: dict | None = None
planner_snapshot: dict | None = None


@router.post("/query", response_model=RetrievalQueryResponse)
async def query_retrieval(
payload: RetrievalQueryRequest,
current_user: CurrentUser = Depends(with_current_user),
Expand All @@ -85,4 +118,5 @@ async def query_retrieval(
rerank=payload.rerank,
threshold=payload.threshold,
internal_recall_k=payload.internal_recall_k,
enable_decomposition=payload.enable_decomposition,
)
7 changes: 7 additions & 0 deletions apps/api/tests/contract/test_retrieval_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ async def test_should_return_empty_results_for_an_empty_query(
"query": "",
"router_used": "empty_query_filtered",
"results": [],
"answer_text": None,
"final_strategy_used": None,
"plan": None,
"planner_snapshot": None,
"referenced_chunks": [],
"steps": None,
"wallet_snapshot": None,
}


Expand Down
8 changes: 8 additions & 0 deletions apps/worker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ NORMOL_MODEL=deepseek-chat
HIERARCHY_LLM_MODEL=deepseek-chat
IMAGE_MODEL=qwen3.5-flash
IMAGE_MODEL_MAX=qwen3.5-flash
RETRIEVAL_DECOMPOSITION_ENABLED=false
RETRIEVAL_PLANNER_MODEL=
RETRIEVAL_PLANNER_THINKING_BUDGET=4000
RETRIEVAL_DECOMPOSITION_MAX_STEPS=5
RETRIEVAL_WALLET_TOTAL_BUDGET=200000
RETRIEVAL_WALLET_PER_RETRIEVE_STEP_BUDGET=40000
RETRIEVAL_WALLET_PER_SYNTHESIZE_STEP_BUDGET=6000
RETRIEVAL_WORKFLOW_PARALLEL_MAX=3

# Required for specific features: billing and analytics
BILLING_ENABLED=false
Expand Down
32 changes: 32 additions & 0 deletions packages/shared-python/shared/core/config/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ class AIConfig(BaseModel):
default="qwen3.5-flash",
description="Higher-capability image model for OCR and ask-image Q&A",
)
RETRIEVAL_DECOMPOSITION_ENABLED: bool = Field(
default=False,
description="Enable query-decomposition workflow before agentic retrieval.",
)
RETRIEVAL_PLANNER_MODEL: str = Field(
default="",
description="Reasoning-capable model used by the workflow query planner.",
)
RETRIEVAL_PLANNER_THINKING_BUDGET: int = Field(
default=4000,
description="Token budget for the query planner thinking call.",
)
RETRIEVAL_DECOMPOSITION_MAX_STEPS: int = Field(
default=5,
description="Maximum number of planned workflow steps.",
)
RETRIEVAL_WALLET_TOTAL_BUDGET: int = Field(
default=200000,
description="Total workflow token wallet for decomposed retrieval.",
)
RETRIEVAL_WALLET_PER_RETRIEVE_STEP_BUDGET: int = Field(
default=40000,
description="Default token budget issued to each retrieve step.",
)
RETRIEVAL_WALLET_PER_SYNTHESIZE_STEP_BUDGET: int = Field(
default=6000,
description="Default token budget issued to each synthesize step.",
)
RETRIEVAL_WORKFLOW_PARALLEL_MAX: int = Field(
default=3,
description="Maximum concurrent workflow steps in the same DAG batch.",
)

# Runtime LLM controls.
LLM_MOCK_ENABLED: bool = Field(
Expand Down
3 changes: 3 additions & 0 deletions packages/shared-python/shared/models/database/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ class RetrievalRun(Base):
result_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
final_doc_ids: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True)
result_provenance: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
parent_run_id: Mapped[Optional[str]] = mapped_column(String(36), nullable=True, index=True)
workflow_step_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
workflow_plan: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
token_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
Expand Down
3 changes: 2 additions & 1 deletion packages/shared-python/shared/services/retrieval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
)
from .graph_service import DocumentGraphService, GraphQueryService, GraphScope
from .hit_stats_service import record_retrieval_hits
from .llm_adapter import create_retrieval_llm_fn
from .llm_adapter import create_retrieval_llm_fn, create_retrieval_planner_fn

__all__ = [
"create_retrieval_llm_fn",
"create_retrieval_planner_fn",
"run_retrieval_query",
"merge_channels_rrf",
"DocumentGraphService",
Expand Down
Loading
Loading