feat(sdk): add per-call peer chat timeout (#1098)
Forward optional timeout overrides through sync and async Peer.chat while retaining client-wide defaults. Refs #734
This commit is contained in:
parent
a5fa8c3962
commit
55a0519bd2
|
|
@ -276,6 +276,9 @@ response = alice.chat("What do I know about Bob?", target="bob")
|
|||
response = alice.chat("What happened in session-1?", session="session-1")
|
||||
response = alice.chat("Summarize what matters most to me.", reasoning_level="high")
|
||||
|
||||
# Override the timeout for one non-streaming dialectic request
|
||||
response = alice.chat("Give me a quick summary.", timeout=5.0)
|
||||
|
||||
# Add content to a session with a peer
|
||||
session = honcho.session("session-1")
|
||||
session.add_messages([
|
||||
|
|
@ -378,6 +381,13 @@ const bobConclusions = await alice.conclusionsOf("bob").list(); // Conclusions
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
For Python, `peer.chat(timeout=...)` and `await peer.aio.chat(timeout=...)`
|
||||
accept a timeout in seconds for each HTTP attempt made by one non-streaming
|
||||
request. Omit it or pass `None` to use the client-wide timeout configured on
|
||||
`Honcho`. Retries still follow the client's `max_retries` setting and can extend
|
||||
total elapsed time; use `max_retries=0` when a host shutdown budget permits only
|
||||
one attempt.
|
||||
|
||||
### Peer Context
|
||||
|
||||
The `context()` method on peers retrieves both the working representation and peer card in a single API call:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Optional per-call `timeout` on synchronous and asynchronous `Peer.chat()`. It overrides the timeout for each HTTP attempt; when omitted or set to `None`, the client-wide timeout configured on `Honcho` remains in effect.
|
||||
|
||||
## [2.4.0] - 2026-08-25
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -777,6 +777,7 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: type[TResponseFormat],
|
||||
timeout: float | None = None,
|
||||
) -> TResponseFormat | None: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -791,6 +792,7 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: dict[str, Any] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> str | None: ...
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
|
|
@ -805,12 +807,17 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: type[BaseModel] | dict[str, Any] | None = None,
|
||||
timeout: float | None = Field(
|
||||
None, gt=0, description="Timeout in seconds for this chat request"
|
||||
),
|
||||
) -> BaseModel | str | None:
|
||||
"""Query the peer's representation asynchronously.
|
||||
|
||||
See Peer.chat for parameter details. When response_format is a Pydantic
|
||||
model class, the answer is parsed into an instance of it; when it is a
|
||||
JSON Schema dict, the answer is a JSON string.
|
||||
JSON Schema dict, the answer is a JSON string. When timeout is omitted,
|
||||
the Honcho client's configured timeout is used; retries can extend total
|
||||
elapsed time.
|
||||
"""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
target_id = resolve_id(target)
|
||||
|
|
@ -835,6 +842,7 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
data = await self._peer._honcho._async_http_client.post(
|
||||
routes.peer_chat(self._peer.workspace_id, self._peer.id),
|
||||
body=body,
|
||||
timeout=timeout,
|
||||
)
|
||||
content = data.get("content")
|
||||
if not content:
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: type[TResponseFormat],
|
||||
timeout: float | None = None,
|
||||
) -> TResponseFormat | None: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -260,6 +261,7 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: dict[str, Any] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> str | None: ...
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
|
|
@ -274,6 +276,9 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
|
||||
| None = None,
|
||||
response_format: type[BaseModel] | dict[str, Any] | None = None,
|
||||
timeout: float | None = Field(
|
||||
None, gt=0, description="Timeout in seconds for this chat request"
|
||||
),
|
||||
) -> BaseModel | str | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
|
@ -310,6 +315,9 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
model class to get a parsed instance back, or a raw
|
||||
JSON Schema dict (root type "object") to get the
|
||||
answer as a JSON string.
|
||||
timeout: Optional timeout in seconds for each HTTP attempt made by
|
||||
this request. When omitted, the Honcho client's configured
|
||||
timeout is used. Retries can extend total elapsed time.
|
||||
|
||||
Returns:
|
||||
Response string containing the answer (a JSON string when a schema
|
||||
|
|
@ -342,6 +350,7 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
data = self._honcho._http.post(
|
||||
routes.peer_chat(self.workspace_id, self.id),
|
||||
body=body,
|
||||
timeout=timeout,
|
||||
)
|
||||
content = data.get("content")
|
||||
if not content:
|
||||
|
|
|
|||
|
|
@ -277,6 +277,40 @@ async def test_peer_chat_non_streaming(
|
|||
assert response is None or isinstance(response, str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("timeout", [None, 2.5])
|
||||
async def test_peer_chat_forwards_per_call_timeout(
|
||||
client_fixture: tuple[Honcho, str],
|
||||
timeout: float | None,
|
||||
) -> None:
|
||||
honcho_client, client_type = client_fixture
|
||||
timeout_label = "default" if timeout is None else "override"
|
||||
|
||||
if client_type == "async":
|
||||
peer = await honcho_client.aio.peer(id=f"test-timeout-{timeout_label}-async")
|
||||
|
||||
async def mock_post(*args: object, **kwargs: object) -> dict[str, str]: # pyright: ignore[reportUnusedParameter]
|
||||
return {"content": "ok"}
|
||||
|
||||
with patch.object(
|
||||
peer._honcho._async_http_client, # pyright: ignore[reportPrivateUsage]
|
||||
"post",
|
||||
side_effect=mock_post,
|
||||
) as mock:
|
||||
result = await peer.aio.chat("What do I like?", timeout=timeout)
|
||||
else:
|
||||
peer = honcho_client.peer(id=f"test-timeout-{timeout_label}-sync")
|
||||
with patch.object(
|
||||
peer._honcho._http, # pyright: ignore[reportPrivateUsage]
|
||||
"post",
|
||||
return_value={"content": "ok"},
|
||||
) as mock:
|
||||
result = peer.chat("What do I like?", timeout=timeout)
|
||||
|
||||
assert result == "ok"
|
||||
assert mock.call_args.kwargs["timeout"] == timeout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_representation_no_params(
|
||||
client_fixture: tuple[Honcho, str],
|
||||
|
|
|
|||
Loading…
Reference in New Issue