v2.6.0: Direct methods on You, Answer API, keyless search, Speakeasy removal - #27
v2.6.0: Direct methods on You, Answer API, keyless search, Speakeasy removal#27tyler5673 wants to merge 35 commits into
Conversation
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>
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
|
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. |
|
|
||
| ### 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. |
There was a problem hiding this comment.
[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().
| """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" | ||
|
|
||
|
|
||
| from .sdkconfiguration import SDKConfiguration |
There was a problem hiding this comment.
[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>
|
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>
|
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 |
| ```bash | ||
| python -m venv .venv | ||
| source .venv/bin/activate | ||
| pip install -e ".[dev]" |
There was a problem hiding this comment.
[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).
| """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" | ||
|
|
||
|
|
||
| from .sdkconfiguration import SDKConfiguration |
There was a problem hiding this comment.
[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.
| - **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`) |
There was a problem hiding this comment.
[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>
|
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>
|
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. |
| @@ -209,6 +223,543 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): | |||
| await self.sdk_configuration.async_client.aclose() | |||
There was a problem hiding this comment.
[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.
| # --------------------------------------------------------------------------- | ||
| # Search helpers (keyless-capable /v1/agents/search) | ||
| # --------------------------------------------------------------------------- | ||
| class TestLiveSearchHelpers: |
There was a problem hiding this comment.
[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.
| ### Available Operations | ||
|
|
||
| * [answer](#answer) - Returns a synthesized answer with citations from web search results | ||
| * [create_run](#create_run) - Run an Agent |
There was a problem hiding this comment.
[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.
| | `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. | |
There was a problem hiding this comment.
[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>
|
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 |
| citations: List[AnswerCitation] = [] | ||
| r"""The sources cited in the answer, in citation order.""" | ||
|
|
||
| results: AnswerResults = AnswerResults() |
There was a problem hiding this comment.
[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.
| # Migration Guide | ||
|
|
||
| ## 2.4.0 → 2.5.0 (Latest) | ||
| ## 2.5.0 → 2.6.0 (major version) |
There was a problem hiding this comment.
[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>
|
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]}") |
There was a problem hiding this comment.
[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.
| 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>
|
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. |
| | 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. | |
There was a problem hiding this comment.
[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.
| | `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. | |
| |-------|------|----------|-------------| | ||
| | `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. | |
There was a problem hiding this comment.
[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.
| | `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. | |
| | `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. | |
There was a problem hiding this comment.
[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.
| | `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. | |
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio | ||
| pip install -e . | ||
| pip install mypy pylint pyright pytest pytest-asyncio |
There was a problem hiding this comment.
[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.
| 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>
|
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>
|
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>
|
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>
|
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 |
| @@ -41,3 +41,6 @@ jobs: | |||
|
|
|||
| - name: Run tests | |||
| run: pytest tests/ -v --tb=short -x | |||
There was a problem hiding this comment.
[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).
| run: pytest tests/ -v --tb=short -x | |
| run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py |
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>
|
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>
|
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>
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
|
Droid finished @tyler5673's task —— View job The PR is cohesive and well-scoped (direct |
| Union[models.FreshnessValue, models.FreshnessValueTypedDict] | ||
| ] = None, | ||
| offset: Optional[int] = None, | ||
| country: Optional[models.Country] = None, |
There was a problem hiding this comment.
[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. |
There was a problem hiding this comment.
[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.
Summary
Version 2.6.0 — major version with breaking changes. Direct methods on
Youreplace sub-SDKs, Answer API added,search_helpersmerged intoyou.search()(keyless-capable), Speakeasy artifacts removed, dev dependencies updated.Breaking Changes
Sub-SDKs and
search_helpersmodule removed. Methods are now direct onYou: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 byyou.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 replacingFreeTierLimitError. Structured data:error,message,upgrade_url,limit,used,period,reset_at.detail(FastAPI array) anderrors(JSON:API array) fields added. Backward compatible.Other Changes
ydc-index.io→api.you.comfor all search/contents endpointsYDCUserAgentOverrideHook(no-op),__gen_version____user_agent__derived from resolved__version__at runtime_hooks/registration.py,models/answerop.py,models/searchpostop.py, unused importssearch()server_url fix: Now respects customserver_url(was hardcoded toSEARCH_POST_OP_SERVERS[0])answer()type annotations:country/languagealigned to enum types with.upper()normalizationpylint/pyrightinstalls, addedmypystepDependency Updates (dev only)
1.15.0→>=2.3.03.2.3→>=4.0.0>=8.0.0→>=9.0.0>=0.24.0→>=1.0.0Test Coverage
Tickets
/v1/agents/search)you.answer())Migration
See MIGRATION.md for the full 2.5.0 → 2.6.0 migration guide.