Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
58dd486
feat: Python SDK 2.5.0 — Research Background Mode
tyler5673 Jul 20, 2026
09b252a
refactor: ponytail shrinks + P2 fixes for research_helpers
tyler5673 Jul 21, 2026
aa6d154
docs: fix 2.5.0 doc inaccuracies (terminal events, research_and_wait …
tyler5673 Jul 22, 2026
cc43e45
fix: sync research_and_wait now enforces total wall-clock deadline (m…
tyler5673 Jul 22, 2026
e000a04
test: fix Go mock comment (four->six terminal events) + add ok-but-ge…
tyler5673 Jul 22, 2026
6ed04e8
feat: add frontier research effort tier + streaming example
tyler5673 Jul 23, 2026
b89039a
feat: auto-adjust research_and_wait timeout for frontier + surface po…
tyler5673 Jul 23, 2026
edb2aeb
feat: add FinanceResearchEffort.LITE + fix PR review findings
tyler5673 Jul 23, 2026
c03c183
test: add live tests for frontier research + finance lite effort
tyler5673 Jul 23, 2026
19464c0
fix: bound async SSE per-read timeout to timeout_s (match sync variant)
tyler5673 Jul 23, 2026
039fee1
fix: distinguish early-break from exhausted re-poll in _resolve_from_…
tyler5673 Jul 23, 2026
714d9b8
fix: align sync/async stream timeout_ms handling + add async repoll t…
tyler5673 Jul 23, 2026
7f7a0bd
fix: stream-open failure falls back to polling + cap per-read timeout…
tyler5673 Jul 23, 2026
ee28a4c
fix: sync research_and_wait falls back to polling on mid-stream errors
tyler5673 Jul 23, 2026
330028d
fix: narrow fallback to httpx.TransportError + decouple SSE per-read …
tyler5673 Jul 23, 2026
c1ef2e1
fix: close stream before fallback poll in mid-stream TransportError h…
tyler5673 Jul 23, 2026
9a363a1
docs: fix sync docstring to drop stale timeout_ms parenthetical
tyler5673 Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,53 @@ All notable changes to the You.com Python SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.0] - 2026-07-20

### Added

- **`frontier` research effort tier**: New `ResearchEffort.FRONTIER` enum value for the highest-quality, longest-running research tasks. Frontier runs over longer durations with improved quality and accuracy. It only works with the task-based API (`background=true`); sending `frontier` without `background=true` returns a `422`. Use it with the background-mode helpers:

```python
from youdotcom import You
from youdotcom.models import ResearchEffort
from youdotcom.research_helpers import research_and_wait

you = You()
detail = research_and_wait(
you,
input="Evaluate the measurable global-health impact of the Gates Foundation",
research_effort=ResearchEffort.FRONTIER,
# timeout_s auto-adjusts to 14400 (4 hours) for frontier when omitted
)
print(detail.result.model_dump()["output"]["content"])
```

- **`lite` finance research effort tier**: New `FinanceResearchEffort.LITE` enum value for the Finance Research API. Returns answers quickly for straightforward financial questions. The default remains `deep`. No migration required; existing `DEEP` and `EXHAUSTIVE` values are unchanged.

- **Research Background Mode**: The `you.research()` method now accepts an optional `background=True` parameter. When enabled, the API queues the research task and returns a `TaskResponse` (with `task_id`, `type`, `status`, `stream_url`, `created_at`) immediately instead of waiting for the inline `ResearchResponse`. Use the new methods to poll or stream the task to completion.

- **`you.get_research_task(task_id=...)`**: Poll the status of a background research task via `GET /v1/research/{task_id}`. Returns a `TaskDetail` with `status`, `result` (populated when completed), `error` (populated when failed), and timing fields.

- **`you.stream_research_task(task_id=..., from_id=0)`**: Stream real-time updates for a background research task via `GET /v1/research/{task_id}/stream` (Server-Sent Events). Returns an `EventStream` of `ResearchTaskStreamEvent` objects. The connection closes automatically when the task reaches a terminal state. Terminal event names: `response.done`, `complete`, `completed` (success); `error`, `failed`, `cancelled` (failure).

- **Convenience helpers** in `youdotcom.research_helpers` (hand-maintained, regen-safe):
- `research_background(you, ...)` / `research_background_async(you, ...)` — submit and return `TaskResponse` directly (no Union narrowing needed).
- `poll_research_task(you, task_id, ...)` / `poll_research_task_async(...)` — poll until terminal status (`completed`, `failed`, `cancelled`). Defaults: `interval_s=2.0`, `timeout_s=600.0` (10 minutes). For `frontier` tasks, pass `timeout_s=14400` (4 hours) explicitly since `poll_research_task` receives a `task_id` and cannot auto-detect the effort tier.
- `research_and_wait(you, ...)` / `research_and_wait_async(...)` — submit with `background=True`, then stream SSE events until a terminal event arrives, and fetch the final `TaskDetail`. If the stream times out or closes without a terminal event, a final `get_research_task` call resolves the status (returns the detail if completed, raises `RuntimeError` for terminal non-completed, or `TimeoutError` if still running). For polling instead of streaming, use `poll_research_task` directly. **`timeout_s` auto-adjusts** based on `research_effort` when omitted: 600s (10 min) for standard/deep/exhaustive, 14400s (4 hours) for `frontier`.
- `stream_research(you, task_id, ...)` / `stream_research_async(...)` — tolerant SSE iterator that surfaces undocumented event types as raw dicts instead of crashing on validation. **Recommended over `you.stream_research_task()` for real research tasks**, since the server emits intermediate workflow events not in the strict `Event` enum.

- **New models**: `TaskResponse`, `TaskResponseStatus`, `TaskDetail`, `TaskDetailStatus`, `TaskDetailInput`, `Result`, `GetResearchTaskRequest`, `StreamResearchTaskRequest`, `ResearchTaskStreamEvent`, `ResearchTaskStreamEventData`, `Event`, `ResearchResult` (Union alias).

- **New error classes**: `GetResearchTaskUnauthorizedError`, `GetResearchTaskForbiddenError`, `GetResearchTaskNotFoundError`, `GetResearchTaskInternalServerError`, `StreamResearchTaskUnauthorizedError`, `StreamResearchTaskForbiddenError`, `StreamResearchTaskNotFoundError`, `StreamResearchTaskInternalServerError` (and their `*Data` variants).

### Changed

- **`you.research()` return type** is now `Union[ResearchResponse, TaskResponse]` (exposed as the `ResearchResult` alias). When `background=False` (the default), the return is `ResearchResponse` as before. When `background=True`, the return is `TaskResponse`. Use `isinstance(res, TaskResponse)` to narrow, or use the `research_helpers` convenience functions.

### Notes

- All 2.4.0 features (finance_research, source_control, output_schema, boost_domains, max_age, env-var precedence, user_agent hook) are unchanged.

## [2.4.0] - 2026-07-14

### Added
Expand Down
166 changes: 165 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,170 @@
# Migration Guide

## 2.3.0 → 2.4.0 (Latest)
## 2.4.0 → 2.5.0 (Latest)

### New `frontier` Research Effort Tier

A new `ResearchEffort.FRONTIER` enum value has been added for the highest-quality, longest-running research tasks. Frontier runs over longer durations (up to 4 hours) with improved quality and accuracy.

**Key constraint:** `frontier` only works with the task-based API (`background=true`). Sending `frontier` without `background=true` returns a `422`.

```python
from youdotcom import You
from youdotcom.models import ResearchEffort
from youdotcom.research_helpers import research_and_wait

you = You()

# Submit a frontier task with background mode and wait for completion
detail = research_and_wait(
you,
input="Evaluate the measurable global-health impact of the Gates Foundation",
research_effort=ResearchEffort.FRONTIER,
timeout_s=14400, # frontier tasks can run up to 4 hours
)
print(detail.result.model_dump()["output"]["content"])
```

You can also use the lower-level helpers for manual polling or streaming:

```python
from youdotcom.research_helpers import research_background, poll_research_task

task = research_background(
you,
input="Evaluate the measurable global-health impact of the Gates Foundation",
research_effort=ResearchEffort.FRONTIER,
)
detail = poll_research_task(you, task_id=task.task_id, timeout_s=14400)
```

No migration is required for existing code. The `frontier` tier is purely additive; existing `LITE`, `STANDARD`, `DEEP`, and `EXHAUSTIVE` values are unchanged.

### New `lite` Finance Research Effort Tier

A new `FinanceResearchEffort.LITE` enum value has been added for quick, straightforward financial questions. The default remains `deep`.

```python
from youdotcom import You
from youdotcom.models import FinanceResearchEffort

you = You()
res = you.finance_research(
input="What was Apple's revenue in FY2024?",
research_effort=FinanceResearchEffort.LITE,
)
```

No migration is required. Existing `DEEP` and `EXHAUSTIVE` values are unchanged.

### New Background Mode for Research

The `you.research()` method now accepts `background=True` to queue long-running research tasks asynchronously. The return type changes from `ResearchResponse` to `Union[ResearchResponse, TaskResponse]` (exposed as the `ResearchResult` alias).

**Synchronous mode (default, unchanged):**

```python
from youdotcom.models import ResearchResponse
res = you.research(input="...", research_effort=ResearchEffort.DEEP)
# res is ResearchResponse — same as 2.4.0
assert isinstance(res, ResearchResponse)
```

**Background mode (new):**

```python
from youdotcom.models import TaskResponse, TaskDetail

# Submit and get a task handle
res = you.research(input="...", research_effort=ResearchEffort.DEEP, background=True)
assert isinstance(res, TaskResponse)
print(res.task_id, res.stream_url)

# Poll for completion
detail = you.get_research_task(task_id=res.task_id)
if detail.status.value == "completed":
payload = detail.result.model_dump() # extra="allow" preserves the full ResearchResponse
print(payload["output"]["content"])

# Or stream events with the tolerant helper (recommended)
from youdotcom.research_helpers import stream_research
for event in stream_research(you, task_id=res.task_id):
print(event.event, event.data)
if event.event in ("response.done", "complete", "completed", "error", "failed", "cancelled"):
break
```

**Or use the convenience helpers:**

```python
from youdotcom.research_helpers import (
research_background, poll_research_task, research_and_wait,
stream_research,
)

# Option 1: Submit and wait in one call (simplest)
# Streams SSE events until the task completes, then fetches the result.
# If the stream times out or closes without a terminal event, a final
# get_research_task call resolves the status (returns the detail if
# completed, raises RuntimeError for terminal non-completed, or
# TimeoutError if still running).
detail = research_and_wait(
you, input="...", research_effort=ResearchEffort.DEEP,
)

# Option 2: Submit, then poll manually
task = research_background(you, input="...", research_effort=ResearchEffort.DEEP)
detail = poll_research_task(you, task_id=task.task_id)

# Option 3: Stream SSE events with a tolerant decoder
# (recommended over you.stream_research_task for real tasks, since
# the server emits intermediate event types not in the strict enum)
for event in stream_research(you, task_id=task.task_id):
print(event.event, event.data)
if event.event in ("response.done", "complete", "completed"):
break
```

> **Note on streaming:** The generated `you.stream_research_task()` method uses a strict pydantic decoder that validates event names against a fixed `Event` enum. The server emits intermediate workflow events (e.g. `response.created`, `response.starting`, `response.output_item.added`) that are not in this enum, which causes `ResponseValidationError` on the first intermediate event. The `stream_research()` helper uses a tolerant decoder that surfaces unknown event names as raw dicts instead of crashing. For real research tasks, prefer `stream_research()`.

### Polling and Timeout Guidance

The `poll_research_task` helper polls `GET /v1/research/{task_id}` at a configurable interval until the task reaches a terminal state. The `research_and_wait` helper streams SSE events and enforces a total wall-clock timeout. Both default sensibly for most effort tiers, but `frontier` tasks can run much longer.

**`research_and_wait` auto-adjusts `timeout_s` based on `research_effort`** when you don't pass an explicit value:

| `research_effort` | Auto `timeout_s` | Typical latency |
|-------------------|-------------------|-----------------|
| `lite` | 600s (10 min) | seconds |
| `standard` | 600s (10 min) | 30-120s |
| `deep` | 600s (10 min) | 120-300s |
| `exhaustive` | 600s (10 min) | 300-600s |
| `frontier` | 14400s (4 hours) | 300s - 4 hours |

**`poll_research_task` does not auto-adjust** (it receives a `task_id`, not the effort level). You must set `timeout_s` explicitly for frontier:

```python
from youdotcom.research_helpers import research_background, poll_research_task

task = research_background(
you, input="...", research_effort=ResearchEffort.FRONTIER,
)
# poll_research_task defaults: interval_s=2.0, timeout_s=600.0
# Override for frontier:
detail = poll_research_task(
you, task_id=task.task_id,
interval_s=5.0, # less aggressive for long-running tasks
timeout_s=14400, # 4 hours
)
```

### No Breaking Changes for Existing Code

If you do not use `background=True`, your existing `you.research()` calls are unchanged at runtime. The return is still `ResearchResponse` when `background` is omitted or `False`.

> **Type-level note for statically-typed callers:** The return type of `you.research()` widened from `ResearchResponse` to `Union[ResearchResponse, TaskResponse]`. If your code accesses `res.output` directly (or passes the result where a `ResearchResponse` is expected), `mypy`/`pyright` will flag it because `TaskResponse` has no `output` field. Add an `isinstance(res, ResearchResponse)` narrow or use `cast(ResearchResponse, res)` to satisfy type checkers. This only affects type checking, not runtime behavior.

## 2.3.0 → 2.4.0

This guide covers breaking changes introduced in 2.4.0. If you are upgrading from 1.x or 2.0, also read the [1.x → 2.0](#1x-to-20) section below.

Expand Down
Loading
Loading