feat: add knowledge param and knowledge result models to search (DX-835) - #65
Merged
Merged
Conversation
Adds the `knowledge` request parameter on POST /v1/search and the models for the results that come back with it, grounded against the published OpenAPI spec and the public docs. Request: `knowledge="core"` on search(), search_async() and the deprecated you.search.unified* shims. Normalized to lowercase like the other enum-typed params; an invalid value raises ValidationError locally rather than reaching the network, mirroring the server's 422. Every parameter on these methods is keyword-only, so inserting `knowledge` mid-signature is non-breaking -- hence a minor bump to 3.5.0. Response: Results.knowledge is an optional List[KnowledgeResult]. Each result carries type, title and attribution, plus description and an optional as_of date for type: answer results. KnowledgeAttribution carries name and an optional source_description. `type` is modeled as a plain str rather than an enum so an unrecognized kind parses instead of raising, since a new kind may populate a different set of fields. The API omits the section entirely when nothing relevant is found, so the parsed field is None rather than []. Results.web, .news and .knowledge are all Optional and the published schema requires none of them, so nothing here assumes a particular combination arrives together. Two response models were narrower than the specs they parse and were silently dropping documented fields; both are fixed: AnswerSearchResult + description, thumbnail_url FinanceResearchSource + snippets Tooling. scripts/check_drift.py now recurses nested response schemas instead of comparing top-level properties only, so drift inside a nested object is visible. The walk uses a recursion stack rather than a global visited cache, so a model reused at two schema paths is compared at both. Two suppression tables (KNOWN_RESPONSE_GAPS, KNOWN_SHARED_MODEL_EXTRAS) record known asymmetries and report themselves stale once the side they excuse catches up. tests/test_check_drift.py is the first test coverage that script has had: 22 cases over recursion, cycle safety and all three staleness paths. scripts/audit_wire.py walks the raw JSON from a live call next to the parsed model and reports any key no field consumed -- the gap that spec-vs-model comparison structurally cannot see. It needs YDC_API_KEY, so it is a pre-release check rather than a CI gate. Docs. Corrected 11 pre-existing Optional type-cell mismatches across docs/models/, added the missing `warnings` row to researchresponse.md, gave results.md prose and a runnable example, made the README's five Configuration snippets standalone, and refreshed tests/README.md, which listed 11 of the 21 test files with wrong counts. Every knowledge snippet guards the optional `results` before iterating. Tests. 21 unit tests for the knowledge surface, live coverage across search/search_async and the deprecated shims, a perf case and normalization cases. Also strengthened three Contents live tests that could pass without asserting the format they requested, and one search filter test whose only substantive assertion sat behind an `if`. Offline gate 450 tests, mypy clean over 87 files, pylint 10.00/10 on the CI errors-only gate, drift --strict clean, docs audits clean, build OK.
Contributor
|
Droid finished @tyler5673's task —— View job The PR is thorough and consistent across SDK plumbing, models, tests, and docs. The only issue I found is a small correctness gap in the new wire-audit script where list traversal can silently skip mismatches. |
…ing them (DX-835) _walk() zipped the raw and parsed lists, which truncates to the shorter one. If the parsed list were ever shorter than the wire list, the trailing raw items and every key inside them were skipped silently and the audit reported "all clear" having never looked at part of the response -- the exact failure mode the tool exists to catch. Reproduced before fixing: a raw list of three items against a parsed list of two reported zero drops, with the third item's undeclared field invisible. Now a length mismatch is appended to the dropped list with both counts, so it surfaces as an unexplained drop. Verified no false positives: equal-length lists and empty-on-both-sides lists report nothing, and the full seven-endpoint audit over 241 wire keys still exits clean.
Contributor
|
Droid finished @tyler5673's task —— View job Strong, consistent PR across SDK plumbing, models, tests, docs, and drift tooling. The only actionable gap I found is in the new wire-audit script: the search audits currently walk only |
…ts (DX-835) The three search calls passed root="results", so _audit walked spy.last["results"] against parsed.results and never looked at the rest of SearchResponse. Every other endpoint in the tool is walked from the response root; search was the odd one out. That left metadata unaudited and, more importantly, any future top-level field on SearchResponse invisible. Demonstrated: with a brand-new top-level key injected into a raw response, root="results" reports zero drops while root=None reports it. Switched all three search calls to root=None and moved the original_thumbnail_url entry in KNOWN_WIRE_EXTRAS to the root-relative path that results. Coverage rose from 241 to 253 wire keys walked, with the same known extras still matching and no unexplained drops.
Contributor
|
Droid finished @tyler5673's task —— View job LGTM — I did not find any additional high-confidence, actionable issues beyond items already discussed in existing PR comments. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Supersedes #62, #63 and #64 — each closed to keep the review readable. The work
is unchanged and carries here as a single squashed commit, so per-commit SHA
references from those PRs no longer apply.
Adds the
knowledgerequest parameter onPOST /v1/searchand the knowledgeresult models that come back with it.
Request —
knowledge="core"onsearch()/search_async()and thedeprecated
you.search.unified*shims. Normalized to lowercase like the otherenum-typed params. An invalid value raises
ValidationErrorlocally before anynetwork call, mirroring the server's
422(same fail-fast contract asextraction). Every parameter on these methods is keyword-only, so insertingknowledgemid-signature is non-breaking — hence a minor bump to 3.5.0.Response —
Results.knowledgeis an optionalList[KnowledgeResult]. Eachresult carries
type,title, andattribution, plusdescriptionand anoptional
as_ofdate fortype: answerresults.KnowledgeAttributioncarriesnameand an optionalsource_description.Two contract details drove the model design:
typeis a plainstr, not an enum. The spec says to ignore an unrecognizedvalue rather than fail on it, since a new kind may populate a different set of
fields. An enum here would turn a future server-side addition into a client
crash.
results.knowledgewhen nothing relevant is found ratherthan returning
[], so the parsed field isNone. Docs and examples iteratewith
or [].Contract grounding
Grounded only against official public surfaces:
https://you.com/docs/openapi/web-search.json— the only spec of the sevenpublished specs that mentions
knowledgeat allhttps://docs.you.com/api-reference/search/v1-search.mdhttps://docs.you.com/administration/billing.mdAlso verified against the live prod API: enum shape, the omitted-key behavior,
as_ofgenuinely optional, invalid value →422, and thatcountdoes notcap knowledge (a
count=1call still returned 2 knowledge results — knowledgehas its own documented limit of 25).
The implementation is scoped to exactly what the public spec defines: no value
beyond the one published there is accepted or modeled.
Also in this PR: drift-checker blind spot
scripts/check_drift.pycompared only top-level response properties, so anew nested field like
results.knowledgewas invisible — the checker reportedthe request-param drift but not the response drift. This is why the original
warning named only the parameter.
The response check now recurses through nested object schemas and arrays. It
falls back to the previous flat comparison when a response resolves to a
oneOfunion rather than a single object schema, so union responses are not mistaken
for empty schemas.
Recursion immediately surfaced two pre-existing gaps unrelated to Knowledge.
Both are now closed by adding the fields rather than suppressed:
answerresults.webdescription,thumbnail_urlAnswerSearchResultfinance-researchoutput.sourcessnippetsFinanceResearchSourceThe answer one was live data loss. Prod returns
descriptionon 20/20 webresults and
thumbnail_urlon 16/20, and both were discarded at parse time.WebResulton the search endpoint already declared them, soAnswerSearchResultwas simply the narrower of two siblings describing the same wire shape.
The finance one was not losing data yet — two live calls at
deepandexhaustivereturned 10 sources carrying onlytitleandurl— but the specdefines
snippetsand the siblingSourcemodel on the Research API alreadydeclared it with an identical description. It was added anyway, because the
suppression holding it back can only notice the SDK catching up, never the
API starting to honour its own spec, so that gap would have stayed silent from
the API side indefinitely.
KNOWN_RESPONSE_GAPSis therefore empty, anddrift --verbosereports nodrift with an empty table — which is the real proof the gaps closed rather than
got hidden. Both suppression tables stay self-invalidating: an entry is reported
stale once the side it excuses catches up, and that path is negative-tested so it
is still live for the next gap.
Negative-tested the fix three ways: dropping
knowledgefromResultsiscaught; a stale suppression entry is reported; and a simulated spec enum gain is
caught by the enum check.
Two follow-ups hardened that recursion:
visitedwas a global cache, not a cycle breaker. It returned earlywhenever the same pydantic class reappeared, so a model used at two response
paths backed by different schemas was compared only at the first and real
drift on later branches went unreported. Reproduced before fixing, with a root
model whose two fields share a type but whose spec schemas differ by one field:
only the first path was reported.
visitedis now discarded on exit(
try/finally), so it breaks cycles without suppressing sibling branches.Checked against self-referential (
Node.child -> Node) and mutual(
A.b -> B.a -> A) schemas: both still terminate, and drift inside the cycleis still reported.
SDK declares that the spec schema at that path does not:
results.news.contentscarrieshighlightsin the SDK but not in the spec.results.web[].contentsresolves toWebContentsPost, which defineshighlights, whileresults.news[].contentsresolves to the narrowerContentsschema (html,markdown), and the SDK shares oneContentsmodelacross both. Settled against prod rather than by inspection: with
extraction_mode="highlights"news items carry nocontentsat all, and withthe deprecated
livecrawl="all"they carryhtmlonly, sohighlightsneverarrives on the news path and the spec is the accurate side. Narrowing the model
would be a breaking change for no behavioral gain, so
KNOWN_SHARED_MODEL_EXTRASrecords the gap — mirroringKNOWN_RESPONSE_GAPSand self-invalidating the same way.
Negative-tested eight ways in total:
knowledgefromResultsis caught;KNOWN_RESPONSE_GAPSentry is reported (re-checked after the tablewent empty, by injecting an entry for a field the SDK now defines);
KNOWN_SHARED_MODEL_EXTRASentry is reported (injectedhtml, whichthe spec does define at that path);
known & model_fieldsintersection got wrong;skipped;
Node.child -> Node) terminates and still reportsdrift inside the cycle;
A.b -> B.a -> A) terminates and still reports drift insidethe cycle.
scripts/check_drift.pyhad no committed test coverage at all, which is why twobugs in one function — the
visitedcache and the staleness intersection —surfaced through review rather than through a failing test, and why every
reproduction above was hand-written and then thrown away.
tests/test_check_drift.py(22 tests) now pins those rules permanently:response recursion including a sibling branch that reuses a model, cycle safety
for self-referential and mutual schemas, all three staleness cases for both
suppression tables, and the
_resolve_schema/_nested_modelhelpers. Bothregressions were confirmed to actually fail when their fix is reverted and pass
when it is restored, so they are regression tests rather than mere coverage.
Also in this PR: a live wire audit
check_drift.pycompares the published specs against the models. That is blindto a field the API returns but no spec declares — the model drops it at parse
time and every static check still agrees with itself, reporting no drift. That
is precisely how
AnswerSearchResultcame to losedescriptionandthumbnail_url: the spec does declare both, but the fact that settled it (prodactually sending them, on 20/20 and 16/20 results) only came from reading the
wire.
scripts/audit_wire.pycloses the gap. It wraps the transport, keeps the rawJSON, and walks it next to the parsed model, reporting any key no field
consumed. Across all seven endpoints that is 228 wire keys, none unexplained.
It needs
YDC_API_KEY, so it is a pre-release check rather than a CI gate;--strictexits 1 on an unexplained drop, negative-tested by emptying the knownlist and confirming it fails.
Two keys are recorded in
KNOWN_WIRE_EXTRASas observed-but-undeclared ratherthan modeled — Documented skips below explains why each is a decision rather than
an oversight.
Verification
pytest tests/(CI gate, live + perf excluded)pytest tests/test_live.py(full suite against prod)description20/20 andthumbnail_url16/20 answer web results now parse; previously all droppedcheck_drift.pywith an emptyKNOWN_RESPONSE_GAPSmypy src/youdotcom/ scripts/pylint src/youdotcom/ --disable=all --enable=E(CI gate)python scripts/check_drift.py --verbose/--strictgo build ./...clean;gofmtclean on the edited handleruv build --sdist --wheelexamples/functionTypecells vs. real annotations (typing.get_type_hints)docs/models/field table, all four AGENTS.md traps: 0 drift## Valuespages vs. code membersknowledgetests/README.mdvs.pytest --collect-onlyscripts/audit_wire.pyagainst prodresearchresponse.md) is fixedscripts/check_drift.pyregression suitedocs/,README.md,USAGE.mdvs. the copy-paste-runnable andtimeout_msrulesResponse composition is not assumed
Results.web,.newsand.knowledgeare allOptional, andresultsitselfis
Optional. Which sections a given call returns depends on the query and theparameters, and the published schema marks none of them required, so the SDK does
not assume any particular combination arrives together.
That is a deliberate design choice, and it is why nothing in this PR couples
knowledgeto the other sections:webornewsaccompanyknowledge results.
results.knowledge, never onresults.web, so they neither encode nor depend on a particular composition.if res.results:andor []—because either can legitimately be absent.
docs/models/results.mdstates the optionality explicitly and shows the guardpattern. It was previously a bare field table with no prose, unlike its sibling
searchresponse.md. The example on that page was executed verbatim against thelive API and runs clean.
Live testing during this PR saw the section mix vary across queries run with
identical parameters, including queries that returned no sections at all. That is
the behavior the optionality exists to absorb, and it is why composition is
treated as a runtime property rather than a documented guarantee: if it changes
server-side, no SDK change is required.
Two related things deliberately not done:
ordinary, correct responses, and it would assert the SDK knows the server's
intent.
sections you get when
knowledgeis set" into the docs would be wrong as soonas that changes, and nothing would catch the staleness —
check_drift.pycompares field names, enums, endpoints and servers, never descriptions.
The
Knowledgeenum docstring stays verbatim againstcomponents.schemas.Knowledge.description, per the convention that modeldocstrings mirror the published spec. The SDK-authored surfaces — the parameter
tables, both field docstrings,
_search_impl's:param knowledge:, anddocs/models/knowledge.md— describe where results land without assertinganything about the other sections.
Also worth noting: not every query yields knowledge results. The API omits
results.knowledgeentirely when nothing relevant is found, so the parsed fieldis
Nonerather than an empty list. The live tests use queries re-verifiedagainst the live API.
Surface sweep
Models, exports, request plumbing, shims, tests, docs, examples, changelog, and
version — following the AGENTS.md checklist:
Knowledge,KnowledgeResult,KnowledgeAttribution, eachwith TypedDict + Pydantic docstrings in lockstep and
optional_fieldsinserialize_model. Exported in all three places inmodels/__init__.py._build_search_request,_search_impl,search_async, andall three
SearchShimmethods.results.md,searchrequestbody.md,docs/sdks/search/README.md, anddocs/sdks/you/README.md(search section only, not the answer section).Type cells mirror the annotations including
Optional[...].[knowledge]snippet block,examples/api-example-calls.pygains a functionplus its
FUNCTIONSmenu entry.tests/test_knowledge.py, 6 live tests, 1 perfcase, and
knowledgeadded totests/test_param_normalization.pyalongsideits normalized siblings.
pyproject.toml,_version.py, anduv.lock.Documented skips
Per AGENTS.md, each skipped surface and why:
MIGRATION.mdsection — additive and non-breaking, nothing to migrate.src/youdotcom/models/searchop.py(SearchRequest) gets noknowledgefield.The published spec (
https://you.com/docs/openapi/web-search.json) defines exactlyone operation,
POST /v1/search, whose body isSearchRequestBody— there is no GEToperation and no
SearchRequestschema to sync against.SearchRequestis aquery-param model (
QueryParamMetadata(style="form", explode=True)) that the SDKnever puts on a request path (it builds
SearchRequestBody), and its fields alreadydiffer by design:
include_domainsis a comma-separatedOptional[str]here versusOptional[List[str]]on the body. Addingknowledgewould advertise a capabilitywith no public contract basis. It likewise has no
extraction, dating to 3.1.0.docs/models/searchrequest.mdis accurate against the model as it stands; its onewrong type cell (
livecrawl_formats) is fixed here.mention knowledge, so adding a price claim would be unsupported.
WebResult.original_thumbnail_urlnot modeled — prod returns it on webresults and no published spec declares it, so
check_drift.pycannot see itfrom either side. Modeling a field observed only on the wire would make the
SDK authoritative for behavior the contract does not promise. Recorded in
KNOWN_WIRE_EXTRAS; it reads like a spec omission worth reporting upstream.FinanceResearchResponse.warningsnot modeled — prod sendswarnings: []at the top level and the sibling
research.jsondeclares it (ResearchResponsemodels it), but
finance-research.jsondeclares onlyoutput. I added thefield first;
check_drift.py --strictthen failed on exactly the standingmismatch that predicted, so it was reverted. The rule that falls out matches
snippets: add the field when that endpoint's spec declares it, record itwhen only prod does.
tests/test_research.pypins the current state so closingthe gap later is a deliberate act rather than a silent one.
One former skip was closed instead of kept: the two pre-existing nested response
gaps (
AnswerSearchResult.description/.thumbnail_urlandFinanceResearchSource.snippets) are fixed here rather than tracked viaKNOWN_RESPONSE_GAPS, which is now empty.Two further surfaces were brought up to date in the same pass:
tests/README.md— listed 11 of the 20 test files and carried wrong counts(Contents 12 vs 13, Answer 23 vs 25). Now lists all 21 with groups for extraction,
knowledge,
page_age, stream events and the cross-cutting suites, and counts takenfrom
pytest --collect-only. The groups sum to 450, exactly the CI-gate collection.Optionaltype-cell drift acrossdocs/models/— a mechanical scancomparing every
Typecell against its real annotation found 11 rows across 8 pageswith the same
Optional[X]↔Xmismatch this PR fixed inresults.md:contentsrequest,sourcecontrol,source,searchrequest,researchdetail,financeresearchdetail,researchrequest,researchtaskstreameventdata. Allpre-dated this branch. Each cell fit its existing padded column, so only those 11
lines changed and table alignment is preserved. The repo now audits clean.
Review notes / open questions
requestsrather than this SDK, and the search guide's optional-parametertable does not list
knowledge. Both are documentation-site changes ratherthan SDK changes, so they are handled separately from this PR.