Source code for pydocs_mcp.application.docs_search

"""DocsSearch — thin wrapper around chunk_pipeline.run (spec §5.1)."""

from __future__ import annotations

from dataclasses import dataclass
from typing import cast

from pydocs_mcp.models import ChunkList, SearchQuery, SearchResponse
from pydocs_mcp.retrieval.pipeline import CodeRetrieverPipeline


[docs] @dataclass(frozen=True, slots=True) class DocsSearch: """Runs the chunk retrieval pipeline and wraps its state as a SearchResponse. Deliberately thin: all ranking/filtering logic lives in the pipeline stages. This class only threads query → pipeline → response. Reads ``state.result`` first (composite output from ``token_budget_formatter`` — what ``chunk_search.yaml`` produces) and falls back to ``state.candidates`` (ranked top-K from ``chunk_search_ranked.yaml``-style presets that omit the formatter). Avoids the silent-empty-results footgun if a deployment overlays the ranked preset onto the MCP server. NOTE (spec S21): this class and :class:`ApiSearch` are intentionally near-duplicate thin wrappers. Keeping them as two separate classes instead of one parameterized ``PipelineSearchService`` is deliberate — the duplication is grep-friendly (a developer looking for "where do chunk searches happen" finds exactly :class:`DocsSearch`) and avoids over-parameterization. If a third near-identical service appears in the future, that's the trigger to parameterize into a shared base class — not now. """ chunk_pipeline: CodeRetrieverPipeline async def search(self, query: SearchQuery) -> SearchResponse: state = await self.chunk_pipeline.run(query) result = state.result if result is None: # Ranked preset (no token_budget_formatter) — surface # state.candidates instead of returning silent empty. result = state.candidates if state.candidates is not None else ChunkList(items=()) return SearchResponse( result=result, query=state.query, duration_ms=state.duration_ms, # Ranked rows ride along so items[] (contract §3.2) come from the # SAME pipeline run the composite body was rendered from. candidates=state.candidates, )
[docs] async def ranked(self, query: SearchQuery) -> ChunkList: """Return the RANKED candidate chunks (pre composite collapse). Multi-repo union needs item-level candidates — each chunk's per-item ``relevance`` score and ``package`` / ``qualified_name`` metadata — to merge and dedup across databases, which the single composite ``search`` output cannot provide. Reads ``state.candidates``; falls back to ``state.result`` for a ranked preset that skips the formatter. """ state = await self.chunk_pipeline.run(query) # A chunk pipeline's candidates / result are always ChunkList; the state # field is a union across pipeline kinds, so narrow explicitly. candidates = state.candidates if state.candidates is not None else state.result return cast("ChunkList", candidates) if candidates is not None else ChunkList(items=())