Skip to content

v2.6.0: Direct methods on You, Answer API, keyless search, Speakeasy removal - #27

Closed
tyler5673 wants to merge 35 commits into
mainfrom
dx-694-python-sdk-route-search-to-v1agentssearch-keyless-capable
Closed

v2.6.0: Direct methods on You, Answer API, keyless search, Speakeasy removal#27
tyler5673 wants to merge 35 commits into
mainfrom
dx-694-python-sdk-route-search-to-v1agentssearch-keyless-capable

Conversation

@tyler5673

@tyler5673 tyler5673 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Version 2.6.0 — major version with breaking changes. Direct methods on You replace sub-SDKs, Answer API added, search_helpers merged into you.search() (keyless-capable), Speakeasy artifacts removed, dev dependencies updated.

Breaking Changes

Sub-SDKs and search_helpers module removed. Methods are now direct on You:

Old (removed) New
you.agents.runs.create(request=...) you.agents(request=...)
you.agents.runs.create_async(request=...) you.agents_async(request=...)
you.search.unified(query=...) you.search(query=...) (POST /v1/agents/search)
you.search_post(query=...) you.search(query=...)
you.search_post_async(query=...) you.search_async(query=...)
you.contents.generate(urls=...) you.contents(urls=...)
you.contents.generate_async(urls=...) you.contents_async(urls=...)
search_helpers.search(client, query=...) you.search(query=...)
search_helpers.search_async(client, query=...) you.search_async(query=...)

GET /v1/search (you.search.unified()) is removed. POST /v1/search (you.search_post()) is removed. Both are replaced by you.search() which targets POST /v1/agents/search (keyless-capable proxy).

New Features

  • you.answer() / you.answer_async() — POST /v1/answer. Synthesized markdown answers with inline citations. Requires API key.
  • you.search() is now keyless-capable — targets POST /v1/agents/search. No API key needed for free tier (100 queries/day, count ≤ 50, no livecrawl). With a key, proxy forwards to full search.
  • PaymentRequiredResponseError (402) — first-class error class replacing FreeTierLimitError. Structured data: error, message, upgrade_url, limit, used, period, reset_at.
  • 422/500 error models expandeddetail (FastAPI array) and errors (JSON:API array) fields added. Backward compatible.

Other Changes

  • Host: ydc-index.ioapi.you.com for all search/contents endpoints
  • Speakeasy removal: All "DO NOT EDIT" disclaimers (101 files), YDCUserAgentOverrideHook (no-op), __gen_version__
  • __user_agent__ derived from resolved __version__ at runtime
  • Dead code removal: _hooks/registration.py, models/answerop.py, models/searchpostop.py, unused imports
  • search() server_url fix: Now respects custom server_url (was hardcoded to SEARCH_POST_OP_SERVERS[0])
  • answer() type annotations: country/language aligned to enum types with .upper() normalization
  • CI: Removed unused pylint/pyright installs, added mypy step

Dependency Updates (dev only)

  • mypy: 1.15.0>=2.3.0
  • pylint: 3.2.3>=4.0.0
  • pytest: >=8.0.0>=9.0.0
  • pytest-asyncio: >=0.24.0>=1.0.0
  • Runtime deps (httpx, httpcore, pydantic) unchanged — already at latest stable

Test Coverage

  • 54 unit tests (MockTransport) — all pass
  • 3 mock server keyless tests (Go mock server, no API key) — all pass
  • 117 mock server tests (Go) — all pass
  • 45 performance tests — all pass
  • 5 live keyless tests (real API, no API key) — all pass
  • mypy 2.3.0 clean (104 files)

Tickets

  • DX-694: SDK keyless search route (/v1/agents/search)
  • DX-308: Answer API (you.answer())

Migration

See MIGRATION.md for the full 2.5.0 → 2.6.0 migration guide.

tyler5673 and others added 16 commits August 3, 2026 17:24
Hand-maintained search_helpers.py (mirrors research_helpers.py pattern)
that POSTs to /v1/agents/search instead of /v1/search. The agents-search
endpoint is a proxy that:
- With an API key -> forwards to /v1/search unrestricted (full features)
- Without a key -> free tier (count <= 50, no livecrawl, IP rate-limited;
  returns 402 on any limit)

- FreeTierLimitError exception for 402 responses
- search() / search_async() standalone helpers accepting string enum params
- 14 unit tests covering keyed/keyless, 402/401/422/500/4XX branches

DX-694

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… error (DX-694)

Answer API:
- New `Answer` sub-SDK: `you.answer.create()` / `create_async()` → POST /v1/answer
- Request model `AnswerRequestBody` reuses existing Country/Language/FreshnessValue enums
- Response models: `AnswerResponse` (answer + citations + results.web),
  `AnswerCitation` (source + excerpts), `AnswerSearchResult` (url + title + snippets + page_age)
- Wired into `You` via `_sub_sdk_map` (lazy instantiation, same as Search/Agents/Contents)
- 13 unit tests (success, 402, 401, 403, 422, 500, async, param serialization)

Keyless search host:
- Changed SEARCH_OP_SERVERS, SEARCH_POST_OP_SERVERS, CONTENTS_OP_SERVERS
  from ydc-index.io → api.you.com (matches MCP server + docs)
- search_helpers.py already targeted api.you.com via client._get_url()

First-class 402 error:
- New `PaymentRequiredResponseError` + `PaymentRequiredResponseErrorData`
  matching the UpgradeRequiredResponse schema (error, message, upgrade_url,
  limit, used, period, reset_at)
- Replaces standalone `FreeTierLimitError` in search_helpers.py
- Reused by both search and answer 402 handlers
- Registered in errors/__init__.py (__all__ + _dynamic_imports)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Normalize lowercase language strings (e.g. "en" → "EN") before pydantic
  validation in search() and search_async() (P1)
- Use self._get_url(None, None) for default base_url in Answer.create() and
  create_async() so You(server_url=...) is honored (P1)
- Fix search() docstring: FreeTierLimitError → PaymentRequiredResponseError (P3)
- Add test_lowercase_language_is_normalized

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Unit tests added:
- Answer: exclude/boost domains serialization, server_url override honored,
  402 with optional usage fields (limit/used/period/reset_at), page_age
  deserialization, multi-citation response
- Search helpers: exclude/boost domains accepted, lowercase language normalized

Live tests added (test_live.py):
- TestLiveAnswer: basic answer, freshness, country, boost_domains, async
- TestLiveSearchHelpers: keyed search, filters, domain filters, async
- All 9 live tests pass against real API with YDC_API_KEY

32 unit tests pass, 9 live tests pass, mypy clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- test_keyless_search_helper: no API key, hits /v1/agents/search free tier
- test_keyless_search_helper_with_filters: keyless + country/freshness/safesearch
- test_async_keyless_search_helper: async keyless
- All 3 pass against real api.you.com proxy with no auth

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…r shapes

Answer API language/country normalization:
- answer.create() and create_async() now accept Optional[str] for country
  and language (was Optional[models.Country/Language] enum)
- Normalizes to uppercase before passing to AnswerRequestBody so callers
  can pass "en", "us" etc. without pydantic validation errors
- Matches search_helpers.py behavior

422 error data model (UnprocessableEntityResponseErrorData):
- Added `detail: Optional[List[dict]]` for FastAPI validation errors
  ({detail: [{type, loc, msg, input, ctx}]})
- Added `errors: Optional[List[dict]]` for JSON:API format
  ({errors: [{status, code, title, detail}]})
- Existing `error: Optional[str]` preserved for search spec format
  ({error: "..."}) — backward compatible
- All three 422 body shapes now deserialize without crashing

500 error data model (InternalServerErrorResponseData):
- Added `errors: Optional[List[dict]]` for JSON:API format
  ({errors: [{status, code, title}]})
- Existing `detail: Optional[str]` preserved — backward compatible

Tests:
- test_422_json_api_format, test_422_search_spec_format,
  test_500_with_json_api_errors: verify all error shapes deserialize
- test_lowercase_language_and_country_normalized: verify "en"/"us" → "EN"/"US"
- 35 unit tests pass, 5 live tests pass, mypy clean

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…less search

Removed "Code generated by Speakeasy — DO NOT EDIT" from 101 source files.
The SDK is no longer generated; all files are hand-maintained.

Removed:
- Speakeasy header docstrings from all source files
- "Built by Speakeasy" badge from README
- __gen_version__ / SPEAKEASY_GENERATOR_VERSION (unused after removing generator)
- speakeasy-sdk prefix from user-agent string

Updated docs:
- README: Added Answer API to Summary and Available Resources sections
- README: Added keyless search section with code example
- README: Updated Search API description to mention keyless capability
- CHANGELOG: Added [Unreleased] section documenting all changes in this PR

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…error changes

New docs:
- docs/sdks/answer/README.md: Answer sub-SDK with create() method, params, errors
- docs/errors/paymentrequiredresponseerror.md: 402 error with 7 fields
- docs/models/answerresponse.md, answercitation.md, answersearchresult.md,
  answerrequestbody.md: model reference docs

Updated docs:
- docs/errors/unprocessableentityresponseerror.md: added detail + errors fields
  for FastAPI validation and JSON:API 422 shapes
- docs/errors/internalservererrorresponse.md: added errors field for JSON:API
  500 shape
- docs/sdks/you/README.md: added Answer API to summary
- CONTRIBUTING.md: replaced "generated code, no PRs accepted" with hand-maintained
  PR guide and dev setup instructions
- MIGRATION.md: added 2.5.0 → Unreleased section covering Answer API, keyless
  search, host change, FreeTierLimitError→PaymentRequiredResponseError, 422/500
  error model expansion, and Speakeasy removal

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…swerResults fields

- Remove "SDK Created by Speakeasy" footer and placeholder comment from README
- Add AnswerResults fields section to answerresponse.md (web field was
  described inline but not in a proper table)

Found during SDK docs drift check — all other doc files match source exactly.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The SDK is no longer generated by Speakeasy, so the "Re-apply this file
after Speakeasy regen" instruction is misleading. Updated the module
docstring to reflect hand-maintained status.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ython-sdk/

The Speakeasy removal changed __user_agent__ but left _DEFAULT_UA_PREFIX
in the hook pointing at "speakeasy-sdk/". This caused the hook to treat
the new default UA as custom and pass it through without rewriting,
producing "youdotcom-python-sdk 2.5.0" (space) instead of the expected
"youdotcom-python-sdk/2.5.0" (slash).

- _version.py: __user_agent__ = "youdotcom-python-sdk/2.5.0"
- registration.py: _DEFAULT_UA_PREFIX = "youdotcom-python-sdk/"
- test_user_agent_hook.py: updated test to use new prefix

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The hook existed to rewrite Speakeasy's default UA (speakeasy-sdk/python ...)
to youdotcom-python-sdk/{version}. Now that __user_agent__ is already
youdotcom-python-sdk/2.5.0, BaseSDK._build_request sets it directly on
every request — the hook was a no-op that added per-request overhead.

Integrations that need a custom UA still just set
client.sdk_configuration.user_agent — BaseSDK picks it up.

- Gut registration.py: init_hooks is now a no-op
- Delete test_user_agent_hook.py
- Update tests/README.md and CHANGELOG

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…dings

Three issues found by dual ponytail + honest review:

[P1] search_helpers.py: country param not upper-cased before model
validation. answer.py normalizes both country and language, but
search_helpers only normalized language. A caller passing country='us'
would hit a pydantic ValidationError. Fixed with .upper() in both
search() and search_async(). Added test_lowercase_country_is_normalized.

[P1] test_live.py: module-level pytestmark skipped ALL tests when no
API key was set, including the keyless search tests that exist
specifically to verify the no-key free-tier path. Moved skip to
per-class @requires_api_key decorator; TestLiveSearchHelpers runs
without an API key.

[P3] test_live.py: has_any_contents variable computed in
test_search_with_livecrawl_all but never asserted — dead code. Removed.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Fixed:
- [P1] country normalization: already fixed in prior commit, confirmed
- [P2] __user_agent__ now derived from resolved __version__ via f-string
  after the importlib.metadata lookup, so it tracks the installed version
- [P2] MIGRATION.md Answer API snippet: added missing import os
- [P2] README.md: replaced 'generated programmatically, changes will be
  overwritten' with hand-maintained language matching CONTRIBUTING.md
- [P2] test_live.py: async keyless test no longer leaks a user-supplied
  AsyncClient — let the SDK create and close its own via async with

Pushed back:
- [P2] 'Mark domain lists as Optional in answerrequestbody.md': the
  existing searchrequestbody.md docs use the same List[*str*] notation
  (not Optional[List[*str*]]) for the same Optional[List[str]] fields.
  Our docs match the established convention; the ➖
  marker already communicates optionality.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Unit tests (test_search_helpers.py):
- test_default_user_agent_is_set: verifies the request header is
  youdotcom-python-sdk/{__version__} via MockTransport capture
- test_custom_user_agent_passes_through: verifies that overriding
  sdk_configuration.user_agent puts the custom value on the wire

Live test (test_live.py):
- test_custom_user_agent_keyless: verifies a custom UA doesn't break
  the keyless search path against the real API

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Fixed:
- [P1] MIGRATION.md: 402 snippet now self-contained with all imports
  (You, PaymentRequiredResponseError, search)
- [P1 security] README.md: added warning that debug logs include full
  headers/bodies which may contain API keys
- [P1] tests/README.md: updated to reflect that keyless search tests
  (TestLiveSearchHelpers) run without an API key by design

Pushed back:
- [P1] Close httpx clients in test helpers: these are MockTransport
  clients with no real connections. No ResourceWarnings emitted. Test
  hygiene only — doesn't cause test failures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@tyler5673
tyler5673 marked this pull request as ready for review August 4, 2026 23:05
@factory-droid

factory-droid Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


The PR is solid and well-tested (Answer SDK, keyless search helpers, expanded error models). Main follow-ups are tightening docs to match actual routing behavior and reducing the risk of leaking API keys via debug logging.

Comment thread CHANGELOG.md Outdated

### Changed

- **Search/Contents host**: `SEARCH_OP_SERVERS`, `SEARCH_POST_OP_SERVERS`, and `CONTENTS_OP_SERVERS` changed from `https://ydc-index.io` to `https://api.you.com` to align with the MCP server and published docs. The keyless search proxy at `api.you.com/v1/agents/search` is now the default for all search operations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fix changelog claim about default search endpoint

The changelog says the keyless proxy at api.you.com/v1/agents/search “is now the default for all search operations”, but the generated Search SDK methods still target /v1/search (GET/POST). This is likely to mislead users; please scope the statement to search_helpers.search() / search_async().

Comment thread src/youdotcom/basesdk.py
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""


from .sdkconfiguration import SDKConfiguration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] [security] Redact sensitive data in debug logs

BaseSDK.do_request() logs full request headers and bodies at debug level, which can include X-API-Key and other sensitive data. Even with the new README warning, it’s easy for debug logging to be enabled in real deployments, so redacting known-secret headers (and potentially large bodies) before logging would materially reduce the risk of credential leakage via logs.

answer was a sub-SDK with a single operation (create), making the call
path you.answer.create(query=...). As a direct method on You, it's now
you.answer(query=...) — consistent with you.research(), you.search_post(),
you.finance_research().

- Deleted src/youdotcom/answer.py (Answer class)
- Moved answer()/answer_async() onto You class in sdk.py
- Removed Answer from _sub_sdk_map and TYPE_CHECKING imports
- Updated all tests: you.answer.create() → you.answer()
- Updated docs/sdks/answer/README.md: documents you.answer() as direct method
- Updated docs/sdks/you/README.md: added answer to operations list
- Updated README, CHANGELOG, MIGRATION to reflect you.answer()

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

The keyless proxy at /v1/agents/search is used by search_helpers, not
by the generated search.unified/search_post methods which target
/v1/search. Clarified the distinction.

Pushed back on [P1 security] redact debug logs: pre-existing code in
basesdk.py not introduced by this PR. README warning already added.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Solid update overall. Remaining follow-ups are to fix CONTRIBUTING dev install docs, avoid eager request/response body materialization in debug logging, and update the README server_url example to https://api.you.com.

Comment thread CONTRIBUTING.md Outdated
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fix dev dependency install instructions

CONTRIBUTING.md recommends pip install -e ".[dev]", but this repo defines dev tools under [dependency-groups].dev (not project.optional-dependencies), so pip will not install pytest/mypy/pylint via that extra. This leaves contributors without the documented tooling unless they happen to install it separately; either add a real dev extra in pyproject.toml or update the docs to the supported install flow (for example uv sync --dev plus editable install).

Comment thread src/youdotcom/basesdk.py
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""


from .sdkconfiguration import SDKConfiguration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid eager request/response body materialization for debug logs

BaseSDK.do_request() always calls logger.debug(...) with get_body_content(req) and (for non-streaming) http_res.text; even when the default NoOpLogger is in use, those arguments are still evaluated, adding overhead and potentially materializing large payloads on every request. Consider guarding the body/response logging behind a real enabled check (for example, only compute/log body when debug_logger is not the no-op logger), while keeping exception logging intact.

Comment thread README.md
- **Research API**: In-depth, multi-step research with citations and sources
- **Finance Research API**: Finance-focused multi-step research with citations and sources
- **Search API**: Get search results from web and news sources
- **Search API**: Get search results from web and news sources (keyless-capable via `/v1/agents/search`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Update README examples to the new api.you.com host

This PR switches the Search/Contents servers from https://ydc-index.io to https://api.you.com, but the README still contains examples that hardcode server_url="https://ydc-index.io" (for example, the per-operation override snippet). Updating those snippets will prevent users from copying an obsolete host and getting unexpected routing failures.

…K accessors

Added direct methods on You that replace the single-operation sub-SDK
pattern:
- you.create_run() / you.create_run_async() → replaces you.agents.runs.create()
- you.search_unified() / you.search_unified_async() → replaces you.search.unified()
- you.generate_contents() / you.generate_contents_async() → replaces you.contents.generate()

The direct methods delegate to the existing sub-SDK instances internally,
so there's one implementation. The sub-SDK accessors (you.agents, you.search,
you.contents) still work but emit DeprecationWarning and will be removed
in a future major version.

Updated: README, CHANGELOG, MIGRATION (with migration table), docs/sdks/you/
README.md, PR description.

11 new tests: 7 delegation tests (verify params pass through, endpoint
correct, async works) + 4 deprecation tests (warning emitted, sub-SDK
still functional after warning).

49 unit tests total, all pass. mypy clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

…feedback

- [P1] CONTRIBUTING.md: replaced pip install -e .[dev] with correct
  install (pip install -e . + pip install dev tools, or uv sync --dev)
  since pyproject.toml uses [dependency-groups] not [optional-dependencies]
- [P3] README.md: updated stale ydc-index.io example to api.you.com

Pushed back:
- [P2] Eager body materialization in debug logs: pre-existing code in
  basesdk.py, not introduced by this PR

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Validated the remaining review items: the client context managers still leave one owned httpx client unclosed, keyless live tests can run during default test runs, and a couple of docs issues (dead anchors, incorrect Optional typing) remain.

Comment thread src/youdotcom/sdk.py
@@ -209,6 +223,543 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.sdk_configuration.async_client.aclose()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Ensure both HTTP clients are closed for sync/async context managers

You.__init__ creates both a sync httpx.Client and an httpx.AsyncClient when callers don’t supply them, but __exit__ only closes the sync client and __aexit__ only closes the async client. This leaves one owned client unclosed for common usage (with You() as you: leaks the async client, async with You() as you: leaks the sync client), which can retain open connections and trigger resource warnings; consider closing/unsetting both clients in both exit paths, or lazily creating the async client only when needed.

Comment thread tests/test_live.py Outdated
# ---------------------------------------------------------------------------
# Search helpers (keyless-capable /v1/agents/search)
# ---------------------------------------------------------------------------
class TestLiveSearchHelpers:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Prevent keyless live tests from running in default unit test runs

TestLiveSearchHelpers now runs without an API key and makes real network calls, but the repo’s test runner (scripts/run_tests.sh) executes pytest tests/ -v without ignoring tests/test_live.py. As a result, a standard unit test invocation can start failing or hanging in CI and developer environments without outbound network access; gate keyless live tests behind an explicit opt-in (env var/pytest mark) or keep all of test_live.py skipped by default.

Comment thread docs/sdks/you/README.md Outdated
### Available Operations

* [answer](#answer) - Returns a synthesized answer with citations from web search results
* [create_run](#create_run) - Run an Agent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fix broken anchors for new direct methods in You SDK docs

The “Available Operations” list adds answer, create_run, search_unified, and generate_contents links (for example #create_run), but there are no corresponding sections in this doc, so those links (and the README links that point here) are dead. Add the missing sections/anchors or change the links to point at the actual pages so users can navigate to the new API surface.

Comment thread docs/models/answerrequestbody.md Outdated
| `freshness` | [Optional[models.FreshnessValue]](../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results. One of `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD`. |
| `country` | [Optional[models.Country]](../models/country.md) | :heavy_minus_sign: | A supported country code that determines the geographical focus of the web results. |
| `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | A supported BCP 47 language tag that determines the language of the web results. |
| `include_domains` | List[*str*] | :heavy_minus_sign: | Domains to exclusively include. Cannot combine with `exclude_domains` or `boost_domains`. Max 500. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Correct AnswerRequestBody docs for optional domain lists

include_domains, exclude_domains, and boost_domains are documented as List[str], but the SDK model treats them as optional and omits them when unset. Updating the docs to Optional[List[str]] avoids misleading users into thinking they must always provide these fields or that empty lists are semantically required.

Version: 2.5.0 → 2.6.0

Dev dependency updates (runtime deps unchanged — already at latest):
- mypy: 1.15.0 → >=2.3.0
- pylint: 3.2.3 → >=4.0.0
- pytest: >=8.0.0 → >=9.0.0
- pytest-asyncio: >=0.24.0 → >=1.0.0

Runtime dependencies verified current:
- httpx 0.28.1 (latest stable, 1.0 in dev preview)
- httpcore 1.0.9 (latest)
- pydantic 2.13.4 (latest, floor >=2.11.2 adequate)

Changed pins to floors (>=) for dev deps to avoid breaking
contributor environments on minor releases.

54 unit tests pass, 117 mock server tests pass, mypy 2.3.0 clean.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Two doc and model hygiene follow-ups remain: avoid mutable defaults in the new Answer response models by using Field(default_factory=...), and align the migration guide’s “major version” wording with the actual 2.6.0 SemVer versioning (or bump to 3.0.0 if intended).

Comment thread src/youdotcom/models/answerresponse.py Outdated
citations: List[AnswerCitation] = []
r"""The sources cited in the answer, in citation order."""

results: AnswerResults = AnswerResults()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Prefer Field(default_factory=...) for Answer model defaults

These new Answer models use =[] and AnswerResults() as defaults (AnswerResults.web, AnswerResponse.citations, AnswerCitation.excerpts, AnswerSearchResult.snippets, AnswerResponse.results). Switching to pydantic.Field(default_factory=list) (and Field(default_factory=AnswerResults) for results) avoids mutable-default footguns and keeps the model definitions consistent with common pydantic style.

Comment thread MIGRATION.md
# Migration Guide

## 2.4.0 → 2.5.0 (Latest)
## 2.5.0 → 2.6.0 (major version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Migration guide: “major version” wording vs SemVer 2.6.0

The 2.5.0 → 2.6.0 section calls this a “major version release,” but the package version is 2.6.0 and the changelog explicitly states Semantic Versioning; consider rephrasing this as “breaking changes” (or bumping to 3.0.0 if a semver-major is intended) to avoid confusing upgrade guidance.

- CHANGELOG: [Unreleased] to [2.6.0] - 2026-08-04
- uv.lock: regenerated (youdotcom 2.5.0 to 2.6.0, mypy/pytest/pylint updated)
- CI workflow: fix dev dependency install for [dependency-groups]
- PR description: updated with version, review fixes, dependency updates

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Overall the PR looks solid and consistent across SDK surfaces, with broad doc/test updates to support the new direct methods. The main remaining issue is a docs example that can raise an IndexError on valid responses with empty citation excerpts.

# Handle response
print(res.answer)
for citation in res.citations:
print(f" [{citation.source}] {citation.excerpts[0]}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Guard against empty citation excerpts in docs example

The Answer API can omit excerpts or return an empty list (the model defaults excerpts to []), so indexing citation.excerpts[0] in this example can raise IndexError; use a conditional fallback before printing.

Suggested change
print(f" [{citation.source}] {citation.excerpts[0]}")
excerpt = citation.excerpts[0] if citation.excerpts else ""
print(f" [{citation.source}] {excerpt}")

P1: Answer model List fields changed to Optional to match SearchResponse
pattern. AnswerResults.web, AnswerResponse.citations, AnswerResponse.results,
AnswerCitation.excerpts, AnswerSearchResult.snippets all now default to
None instead of empty list. Prevents ValidationError when API returns null.

P1: Deleted dead searchpostop.py (SEARCH_POST_OP_SERVERS no longer used
after search() switched to _get_url()). Removed all references from
models/__init__.py.

P3: Fixed PaymentRequiredResponseErrorData docstring (referenced
non-existent UpgradeRequiredResponse class).

P3: Removed dead Speakeasy test-header reads from Go mock handler.

54 unit tests pass, 117 mock server tests pass, mypy clean (105 files).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Found four small, high-signal follow-ups: the Answer API model docs should mark several optional fields as Optional, and CI installs mypy/pylint/pyright without running them.

Comment thread docs/models/answercitation.md Outdated
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `source` | *str* | :heavy_check_mark: | The URL of the cited source. |
| `excerpts` | List[*str*] | :heavy_minus_sign: | Verbatim excerpts from the cited source that support the answer. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fix AnswerCitation.excerpts type in docs

AnswerCitation.excerpts is Optional[List[str]] in code (it can be missing/null), but the docs currently show it as List[str], which is misleading for users and static type checkers.

Suggested change
| `excerpts` | List[*str*] | :heavy_minus_sign: | Verbatim excerpts from the cited source that support the answer. |
| `excerpts` | Optional[List[*str*]] | :heavy_minus_sign: | Verbatim excerpts from the cited source that support the answer. |

Comment thread docs/models/answersearchresult.md Outdated
|-------|------|----------|-------------|
| `url` | *str* | :heavy_check_mark: | The URL of the source webpage. |
| `title` | *str* | :heavy_check_mark: | The title of the source webpage. |
| `snippets` | List[*str*] | :heavy_minus_sign: | Text snippets from the search result that preview its content. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fix AnswerSearchResult.snippets type in docs

AnswerSearchResult.snippets is Optional[List[str]] in code (it can be missing/null), but the docs currently show it as List[str], which can lead to incorrect client assumptions.

Suggested change
| `snippets` | List[*str*] | :heavy_minus_sign: | Text snippets from the search result that preview its content. |
| `snippets` | Optional[List[*str*]] | :heavy_minus_sign: | Text snippets from the search result that preview its content. |

Comment thread docs/models/answerresponse.md Outdated
Comment on lines +11 to +19
| `web` | List[[models.AnswerSearchResult](../models/answersearchresult.md)] | :heavy_minus_sign: | All web search results considered during answer synthesis. |

## AnswerResponse

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `answer` | *str* | :heavy_check_mark: | The synthesized response with numbered inline citations that reference items in the `citations` array. |
| `citations` | List[[models.AnswerCitation](../models/answercitation.md)] | :heavy_minus_sign: | The sources cited in the answer, in citation order. |
| `results` | [models.AnswerResults](#answerresults) | :heavy_minus_sign: | Search results grouped by result type. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Mark AnswerResponse optional fields as Optional in docs

In code, AnswerResults.web, AnswerResponse.citations, and AnswerResponse.results are all optional, but the docs currently present them as non-optional types, which can cause confusion and incorrect type expectations.

Suggested change
| `web` | List[[models.AnswerSearchResult](../models/answersearchresult.md)] | :heavy_minus_sign: | All web search results considered during answer synthesis. |
## AnswerResponse
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `answer` | *str* | :heavy_check_mark: | The synthesized response with numbered inline citations that reference items in the `citations` array. |
| `citations` | List[[models.AnswerCitation](../models/answercitation.md)] | :heavy_minus_sign: | The sources cited in the answer, in citation order. |
| `results` | [models.AnswerResults](#answerresults) | :heavy_minus_sign: | Search results grouped by result type. |
| `web` | Optional[List[[models.AnswerSearchResult](../models/answersearchresult.md)]] | :heavy_minus_sign: | All web search results considered during answer synthesis. |
## AnswerResponse
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `answer` | *str* | :heavy_check_mark: | The synthesized response with numbered inline citations that reference items in the `citations` array. |
| `citations` | Optional[List[[models.AnswerCitation](../models/answercitation.md)]] | :heavy_minus_sign: | The sources cited in the answer, in citation order. |
| `results` | [Optional[models.AnswerResults]](#answerresults) | :heavy_minus_sign: | Search results grouped by result type. |

Comment thread .github/workflows/test.yml Outdated
pip install -e ".[dev]"
pip install pytest pytest-asyncio
pip install -e .
pip install mypy pylint pyright pytest pytest-asyncio

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Avoid installing unused lint/type tools in CI

This workflow only runs pytest, but it also installs mypy, pylint, and pyright, which adds CI time and can introduce avoidable breakage when those tools release new versions; either add steps to run them, or keep installs limited to what this job executes.

Suggested change
pip install mypy pylint pyright pytest pytest-asyncio
pip install pytest pytest-asyncio

…ethod

The search_helpers module was a standalone function that called POST
/v1/agents/search (the keyless-capable proxy). you.search() called POST
/v1/search. These were the same operation with different endpoints.

Now you.search() targets POST /v1/agents/search directly. With no API key,
runs in the free tier (100 queries/day, count <= 50, no livecrawl). With a
key, the proxy forwards to the full search endpoint.

Changes:
- search()/search_async() now POST to /v1/agents/search (was /v1/search)
- Added 402 PaymentRequiredResponseError handling to search()
- Added .upper() normalization for country/language (accept plain strings)
- Changed param types from enums to str (callers don't need enum imports)
- language default changed from Language.EN to None (SearchRequestBody
  defaults to EN)
- Deleted src/youdotcom/search_helpers.py (merged into you.search())
- Renamed test_search_helpers.py to test_search_keyless.py
- Updated all tests, docs, mock server route, CHANGELOG, MIGRATION

54 unit tests + 117 mock server tests + 45 performance tests pass.
mypy clean (104 files).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

- docs/sdks/search/README.md: POST /v1/search → POST /v1/agents/search
- docs/sdks/you/README.md: add PaymentRequiredResponseError (402) to
  search error table

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

Three test layers now cover the keyless search path (no API key):

1. Unit (MockTransport): TestSearchSuccess + TestSearchErrors in
   test_search_keyless.py — sync/async keyless with api_key=None,
   string enum params, .upper() normalization, 402/401/403/422/500
   error handling.

2. Mock server (Go): TestSearchKeylessMockServer in
   test_search_keyless.py — 3 tests hitting the Go mock server with
   no API key. Mock server handler changed from required to optional
   X-API-Key header (SecurityHeader false → true) since
   /v1/agents/search is keyless-capable.

3. Live: TestLiveSearchKeyless in test_live.py — 5 tests against the
   real API (keyed, keyless, keyless+filters, async keyless, custom UA).
   Fixed: test_keyed_search now decorated with @requires_api_key so it
   skips when no key is set instead of silently becoming keyless.

All tests pass: 54 unit + 3 mock keyless + 117 mock server + 45 perf
+ 5 live keyless. mypy clean (104 files).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

- docs/models/answerresponse.md: web, citations, results now marked
  Optional to match the code (changed to Optional[List[...]] = None
  in a previous commit to prevent crash on null API response).
- docs/models/answercitation.md: excerpts marked Optional.
- docs/models/answersearchresult.md: snippets marked Optional.
- .github/workflows/test.yml: removed unused pylint/pyright installs,
  added mypy step to actually run type checking in CI.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Most items in this batch were already raised in existing PR comments. The remaining high-impact follow-up is to keep CI offline-safe by excluding tests/test_live.py, since it now contains keyless tests that perform real network calls by default.

Comment thread .github/workflows/test.yml Outdated
@@ -41,3 +41,6 @@ jobs:

- name: Run tests
run: pytest tests/ -v --tb=short -x

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Exclude live API tests from CI runs

The CI job runs pytest tests/ without ignoring tests/test_live.py, and this PR added keyless live tests that make real network calls even when no API key is set, which will make CI flaky/offline-failing and can burn rate limits; align CI with CONTRIBUTING by ignoring the live test module (or add an explicit opt-in marker and exclude it in CI).

Suggested change
run: pytest tests/ -v --tb=short -x
run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py

@tyler5673 tyler5673 changed the title feat: keyless search host + first-class 402 error (DX-694) + Answer API (DX-308) v2.6.0: Direct methods on You, Answer API, keyless search, Speakeasy removal Aug 5, 2026
CI runs pytest tests/ which includes test_live.py. The keyless tests
in TestLiveSearchKeyless make real network calls to the You.com API
without an API key — not appropriate for CI. Added --ignore=tests/test_live.py
to the CI test step.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

The API can return contents.markdown='' (empty string) for some URLs.
The old assertion used truthiness (or) which treats '' as falsy. Changed
to explicit None checks so empty-string content is accepted.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

…confidence

P0: docs/sdks/you/README.md agents example used models.AgentType.EXPRESS
which does not exist (removed in 2.0.0). ExpressAgentRunsRequest defaults
agent='express' internally, so the parameter is not needed. Removed it.

P1: tests/__init__.py still had Speakeasy 'DO NOT EDIT' header. Removed.

P1: test_search.py used pytest.raises((SpecificError, YouDefaultError))
which passes even if only YouDefaultError is raised (the 4XX fallback).
This defeats the tests' stated purpose of locking the error-class contract.
Changed to assert the specific error class only. Removed unused YouDefaultError
import.

P1: CI workflow installed mypy/pytest/pytest-asyncio without version pins.
Pinned to match pyproject.toml floors: mypy>=2.3.0, pytest>=9.0.0,
pytest-asyncio>=1.0.0.

P2: tests/README.md referenced non-existent examples/search.py,
examples/contents.py, examples/agents.py. Updated to reference
examples/api-example-calls.py.

54 unit tests pass, mypy clean (104 files).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@tyler5673 tyler5673 closed this Aug 5, 2026
@tyler5673 tyler5673 reopened this Aug 5, 2026
@tyler5673 tyler5673 closed this Aug 5, 2026
@factory-droid

factory-droid Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


The PR is cohesive and well-scoped (direct You methods, Answer API, keyless search routing, and expanded error models). The main remaining issues are small but user-facing, a typing mismatch in answer()’s locale parameters and Answer docs that should more clearly state the API key requirement.

Comment thread src/youdotcom/sdk.py
Union[models.FreshnessValue, models.FreshnessValueTypedDict]
] = None,
offset: Optional[int] = None,
country: Optional[models.Country] = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Align Answer country/language types with accepted strings

You.answer() is typed as country: Optional[models.Country] / language: Optional[models.Language], but the implementation still treats them as strings (isinstance(..., str) with .upper() normalization) and the docs present them as Optional[str]; this mismatch breaks static typing for callers passing plain strings and is inconsistent with You.search() which is typed as Optional[str]. Consider changing these parameters to Optional[str] (or Optional[Union[str, models.Country]] / Optional[Union[str, models.Language]]) to reflect actual supported inputs.


## Overview

The Answer API returns a synthesized natural-language answer with citations and the web results used to generate it. Send a `query` with optional freshness, locale, and domain controls.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Document that Answer requires an API key

This page doesn’t explicitly call out that you.answer() requires an API key (unlike you.search(), which is keyless-capable), so users can easily copy/paste the snippet without realizing why they get auth failures. Add a short “Requires API key” note near the overview (and optionally near the example) to prevent confusion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant