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
3 changes: 2 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ ARK_API_KEY=
# ASSET_MODEL=

# Optional retrieval overrides have code defaults. Retrieval is evidence-only:
# evidence_text is the primary output and answer_text is always empty.
# evidence is the primary output; evidence_text is deprecated and always empty;
# answer_text is always empty.
# use_agentic on the request turns agent retrieval on or off:
# false → no agent; classic map-unit BM25
# omit/true → agent on; AGENT_EXPLORE_HARNESS selects the mode
Expand Down
14 changes: 11 additions & 3 deletions apps/api/app/api/v1/routes/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,25 @@ class RetrievalQueryResponse(BaseModel):
router_used: str
evidence: list[dict] = Field(
default_factory=list,
description="Composed evidence parts (text and inline images) for downstream agents.",
description=(
"Grouped evidence blocks. Each group starts with "
"[E1] [§ source_file_name / parent_path]. Text and table HTML "
"stay as text parts; images stay as image parts at their "
"placeholder positions."
),
)
evidence_text: str = Field(
default="",
description="Text projection of evidence. Tables stay as HTML; images are data URLs.",
description=(
"DEPRECATED. Always empty. Knowhere no longer projects evidence "
"into a string. Use evidence."
),
)
answer_text: str = Field(
default="",
description=(
"DEPRECATED. Always empty; KNOWHERE no longer generates answers. "
"Use evidence_text and synthesize answers downstream."
"Use evidence and synthesize answers downstream."
),
)
referenced_chunks: list[dict] = Field(default_factory=list)
Expand Down
2 changes: 1 addition & 1 deletion apps/api/app/mcp/dynamic_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
there is no public entry point to register a tool from an already-built JSON
Schema dict, which is what every ``agent_tools.ToolSpec`` carries. Since our
schema is the one already shipped to ``agent_explore`` and meant to be
verbatim-identical across harnesses (see ``CORPUS_SCHEMA.md``), we construct
verbatim-identical across harnesses (see ``CORPUS_OVERVIEW.md``), we construct
``Tool`` objects directly instead of round-tripping through a synthetic
Python function signature, and insert them into the tool manager's registry
dict — the same dict ``ToolManager.__init__`` accepts a ``tools=`` list for,
Expand Down
71 changes: 42 additions & 29 deletions apps/api/app/mcp/retrieval_server.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
from __future__ import annotations

import json
from typing import Annotated, Any, AsyncContextManager, Callable

from app.services.auth.current_user_authentication_service import (
get_current_user_authentication_service,
)
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from mcp.types import ImageContent, TextContent
from pydantic import Field
from sqlalchemy.ext.asyncio import AsyncSession

from app.mcp.dynamic_tools import register_corpus_tools
from shared.core.database import get_db_context
from shared.models.schemas.retrieval_namespace import normalize_retrieval_namespace
from shared.services.retrieval.agent_tools import load_corpus_schema_text
from shared.services.retrieval.agent_tools import load_corpus_overview_text
from shared.services.retrieval.app_service import run_retrieval_query
from shared.services.retrieval.settings import DEFAULT_TOP_K

Expand Down Expand Up @@ -53,26 +55,37 @@ def resolve_mcp_namespace(*, ctx: Context | None) -> str:
return normalize_retrieval_namespace(namespace)


def to_mcp_query_response(response: dict[str, Any]) -> dict[str, Any]:
"""Project the public retrieval response to the MCP agent contract.
def to_mcp_query_response(response: dict[str, Any]) -> list[TextContent | ImageContent]:
"""Project the public retrieval response to MCP content blocks.

MCP returns the same Knowhere package the HTTP query emits:
- evidence: composed parts (text/HTML and inline images)
- evidence_text: text projection of those parts
- results: raw path chunks for debug
- referenced_chunks: structured chunk references for citation / follow-up
- decision_trace: navigation decisions including terminal stop/failure
Each evidence part becomes ``TextContent`` or ``ImageContent``. A final
text block is JSON for query, router_used, failure_reason,
referenced_chunks, decision_trace, and results. That JSON does not
repeat evidence or evidence_text.
"""
return {
"query": response.get("query"),
"router_used": response.get("router_used"),
"failure_reason": response.get("failure_reason"),
"evidence": response.get("evidence") or [],
"evidence_text": response.get("evidence_text") or "",
"results": response.get("results") or [],
"referenced_chunks": response.get("referenced_chunks") or [],
"decision_trace": response.get("decision_trace") or [],
}
blocks: list[TextContent | ImageContent] = [
ImageContent(type="image", data=part["data"], mimeType=part["media_type"])
if part["type"] == "image"
else TextContent(type="text", text=part["text"])
for part in response.get("evidence") or []
]
blocks.append(
TextContent(
type="text",
text=json.dumps(
{
"query": response.get("query"),
"router_used": response.get("router_used"),
"failure_reason": response.get("failure_reason"),
"referenced_chunks": response.get("referenced_chunks") or [],
"decision_trace": response.get("decision_trace") or [],
"results": response.get("results") or [],
},
ensure_ascii=False,
),
)
)
return blocks


async def resolve_mcp_user_id(*, ctx: Context | None, db: AsyncSession) -> str:
Expand All @@ -95,8 +108,8 @@ def create_retrieval_mcp_server(
instructions=(
"retrieval.query is a one-shot legacy search tool retained for "
"backward compatibility (see its own tool description below). "
"Prefer the corpus.* tools for exploration; the schema below "
"describes what they operate on.\n\n" + load_corpus_schema_text()
"Prefer the corpus.* tools for exploration; the overview below "
"describes the corpus they operate on.\n\n" + load_corpus_overview_text()
),
streamable_http_path=streamable_http_path,
stateless_http=True,
Expand All @@ -108,14 +121,14 @@ def create_retrieval_mcp_server(
name="retrieval.query",
description=(
"Search published documents. Compose already happened in Knowhere. "
"Returns evidence (composed parts for LLM consumption), "
"results (raw path chunks for debug), "
"evidence_text (text projection of evidence), "
"referenced_chunks (cited chunk metadata for follow-up queries), "
"and decision_trace (navigation decisions including stop/failure reasons). "
"Include navigation intent directly in your query text — the "
"engine will automatically locate the right documents and sections."
"Returns grouped evidence as text and image content blocks, then "
"a JSON text block with query, router_used, failure_reason, "
"referenced_chunks, decision_trace, and results (raw path chunks "
"for debug). Include navigation intent directly in your query "
"text — the engine will automatically locate the right documents "
"and sections."
),
structured_output=False,
)
async def query_documents(
query: Annotated[
Expand Down Expand Up @@ -149,7 +162,7 @@ async def query_documents(
)),
] = [],
ctx: Context | None = None,
) -> dict:
) -> list[TextContent | ImageContent]:
# TODO(intent-step): When the Intent Understanding step is
# implemented, it will parse `query` here to extract structured
# hints (document_hint, scope_hint, content_type_hint) and set
Expand Down
Loading
Loading