SDK Changes (#223)
* fix: Add top level transactions for tracing in deriver * fix: Add Delete methods to sdks for sessions * feat: Add session deletion and session level deriver status methods * fix: Streaming support to python sdk * fix: Checkpoint Streaming working in SDKs with workarounds for stainless * feat: fix pagination, add message metadata update, and get peer card methods * feat: parity in typescript and python sdks * fix: Address Coderabbit * fix: Address Coderabbit * chore: add basedpyright rules to sdk * fix: Code Rabbit & Changelog * fix: updated docs with api-reference
This commit is contained in:
parent
3df044e1b7
commit
73c5eb1f5a
|
|
@ -185,3 +185,4 @@ config.toml
|
|||
|
||||
CRUSH.md
|
||||
.crush/
|
||||
AGENTS.md
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ 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/).
|
||||
|
||||
## [2.3.3] - 2025-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Dialectic Streaming to follow SSE conventions
|
||||
- Sentry tracing in the deriver
|
||||
|
||||
## [2.3.2] - 2025-09-25
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
---
|
||||
|
||||

|
||||

|
||||
[](https://pypi.org/project/honcho-ai/)
|
||||
[](https://npmjs.org/package/@honcho-ai/sdk)
|
||||
[](https://discord.gg/plasticlabs)
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
## Version Compatibility
|
||||
|
||||
### Honcho API v2.3.2 (Current)
|
||||
### Honcho API v2.3.3 (Current)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v1.4.0
|
||||
**Compatible Version:** v1.4.1
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@1.4.0
|
||||
npm install @honcho-ai/sdk@1.4.1
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v1.4.0
|
||||
**Compatible Version:** v1.4.1
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==1.4.0
|
||||
pip install honcho-ai==1.4.1
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -35,7 +35,9 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v2.3.2 (Current) | v1.4.0 | v1.4.0 |
|
||||
| v2.3.3 (Current) | v1.4.1 | v1.4.1 |
|
||||
| v2.3.2 | v1.4.0 | v1.4.0 |
|
||||
| v2.3.1 | v1.4.0 | v1.4.0 |
|
||||
| v2.3.0 | v1.4.0 | v1.4.0 |
|
||||
| v2.2.0 | v1.3.0 | v1.3.0 |
|
||||
| v2.1.1 | v1.2.1 | v1.2.2 |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,14 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
### Honcho API and SDK Changelogs
|
||||
<Tabs>
|
||||
<Tab title="Honcho API">
|
||||
<Update label="v2.3.2 (Current)">
|
||||
<Update label="v2.3.3 (Current)">
|
||||
### Fixed
|
||||
|
||||
- Dialectic Streaming to follow SSE conventions
|
||||
- Sentry tracing in the deriver
|
||||
</Update>
|
||||
|
||||
<Update label="v2.3.2">
|
||||
### Added
|
||||
|
||||
- Get peer cards endpoint (`GET /v2/peers/{peer_id}/peer-card`) for retrieving targeted peer context information
|
||||
|
|
|
|||
|
|
@ -9,21 +9,14 @@
|
|||
},
|
||||
"favicon": "/favicon.svg",
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude"
|
||||
]
|
||||
"options": ["copy", "view", "chatgpt", "claude"]
|
||||
},
|
||||
"navigation": {
|
||||
"versions": [
|
||||
{
|
||||
"version": "v2.3.2",
|
||||
"version": "v2.3.3",
|
||||
"api": {
|
||||
"openapi": [
|
||||
"openapi.documented.yml"
|
||||
]
|
||||
"openapi": ["openapi.documented.yml"]
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
|
|
@ -61,17 +54,11 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"v2/guides/overview",
|
||||
"v2/guides/mcp"
|
||||
]
|
||||
"pages": ["v2/guides/overview", "v2/guides/mcp"]
|
||||
},
|
||||
{
|
||||
"group": "Application Interfaces",
|
||||
"pages": [
|
||||
"v2/guides/discord",
|
||||
"v2/guides/telegram"
|
||||
]
|
||||
"pages": ["v2/guides/discord", "v2/guides/telegram"]
|
||||
},
|
||||
{
|
||||
"group": "Design Patterns",
|
||||
|
|
@ -106,9 +93,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": [
|
||||
"v2/api-reference/introduction"
|
||||
]
|
||||
"pages": ["v2/api-reference/introduction"]
|
||||
},
|
||||
{
|
||||
"group": "workspaces",
|
||||
|
|
@ -129,7 +114,8 @@
|
|||
"v2/api-reference/endpoint/peers/get-sessions-for-peer",
|
||||
"v2/api-reference/endpoint/peers/chat",
|
||||
"v2/api-reference/endpoint/peers/get-working-representation",
|
||||
"v2/api-reference/endpoint/peers/search-peer"
|
||||
"v2/api-reference/endpoint/peers/search-peer",
|
||||
"v2/api-reference/endpoint/peers/get-peer-card"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -163,9 +149,7 @@
|
|||
},
|
||||
{
|
||||
"group": "keys",
|
||||
"pages": [
|
||||
"v2/api-reference/endpoint/keys/create-key"
|
||||
]
|
||||
"pages": ["v2/api-reference/endpoint/keys/create-key"]
|
||||
},
|
||||
{
|
||||
"group": "webhooks",
|
||||
|
|
@ -195,9 +179,7 @@
|
|||
{
|
||||
"version": "v1.1.0",
|
||||
"api": {
|
||||
"openapi": [
|
||||
"openapi.json"
|
||||
]
|
||||
"openapi": ["openapi.json"]
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
|
|
@ -227,23 +209,15 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"v1/guides/overview",
|
||||
"v1/guides/streaming-response"
|
||||
]
|
||||
"pages": ["v1/guides/overview", "v1/guides/streaming-response"]
|
||||
},
|
||||
{
|
||||
"group": "Application Interfaces",
|
||||
"pages": [
|
||||
"v1/guides/discord",
|
||||
"v1/guides/honcho-mcp"
|
||||
]
|
||||
"pages": ["v1/guides/discord", "v1/guides/honcho-mcp"]
|
||||
},
|
||||
{
|
||||
"group": "Personal Memory",
|
||||
"pages": [
|
||||
"v1/guides/dialectic-endpoint"
|
||||
]
|
||||
"pages": ["v1/guides/dialectic-endpoint"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -252,9 +226,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": [
|
||||
"v1/api-reference/introduction"
|
||||
]
|
||||
"pages": ["v1/api-reference/introduction"]
|
||||
},
|
||||
{
|
||||
"group": "apps",
|
||||
|
|
@ -302,9 +274,7 @@
|
|||
},
|
||||
{
|
||||
"group": "keys",
|
||||
"pages": [
|
||||
"v1/api-reference/endpoint/keys/create-key"
|
||||
]
|
||||
"pages": ["v1/api-reference/endpoint/keys/create-key"]
|
||||
},
|
||||
{
|
||||
"group": "metamessages",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/peers/{peer_id}/card
|
||||
---
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.3.2"
|
||||
version = "2.3.3"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
@ -79,7 +79,7 @@ extend-immutable-calls = ["fastapi.Depends"]
|
|||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
addopts = "--strict-markers --cov=src/ --cov-report=term-missing"
|
||||
addopts = "--strict-markers --cov=src/ --cov=sdks/python/src/honcho --cov-report=term-missing"
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
description = "Official DX Optimized Python SDK for Honcho"
|
||||
dynamic = ["readme"]
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -8,9 +8,10 @@ authors = [
|
|||
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
|
||||
]
|
||||
dependencies = [
|
||||
"honcho-core>=1.4.0",
|
||||
"honcho-core>=1.4.1-alpha.0",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
"typing-extensions>=4.12.0; python_version < \"3.12\"",
|
||||
]
|
||||
requires-python = ">= 3.8"
|
||||
classifiers = [
|
||||
|
|
@ -48,6 +49,16 @@ readme = {file = ["README.md"], content-type = "text/markdown"}
|
|||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.basedpyright]
|
||||
# BasedPyright currently seems like the best type checker option, much faster
|
||||
# https://docs.basedpyright.com/latest/configuration/config-files/#sample-pyprojecttoml-file
|
||||
include = ["src"]
|
||||
reportUnusedCallResult = false
|
||||
reportCallInDefaultInitializer = false
|
||||
reportAny = false
|
||||
reportExplicitAny = false
|
||||
reportImportCycles = false
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple/"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from .pagination import SyncPage
|
|||
from .peer import Peer
|
||||
from .session import Session
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .types import DialecticStreamResponse
|
||||
|
||||
__version__ = "1.4.0"
|
||||
__author__ = "Plastic Labs"
|
||||
|
|
@ -62,4 +63,5 @@ __all__ = [
|
|||
"SessionSummaries",
|
||||
"Summary",
|
||||
"SyncPage",
|
||||
"DialecticStreamResponse",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import httpx
|
|||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.peer import Peer as PeerCore
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
|
|
@ -201,7 +203,7 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
async def get_peers(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncPeer]:
|
||||
) -> AsyncPage[PeerCore, AsyncPeer]:
|
||||
"""
|
||||
Get all peers in the current workspace.
|
||||
|
||||
|
|
@ -265,7 +267,7 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
async def get_sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncSession]:
|
||||
) -> AsyncPage[SessionCore, AsyncSession]:
|
||||
"""
|
||||
Get all sessions in the current workspace.
|
||||
|
||||
|
|
@ -466,6 +468,53 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def update_message(
|
||||
self,
|
||||
message: Message | str = Field(
|
||||
..., description="The Message object or message ID to update"
|
||||
),
|
||||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
Update the metadata of a message.
|
||||
|
||||
Makes an API call to update the metadata of a specific message within a session.
|
||||
|
||||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
||||
Raises:
|
||||
ValidationError: If message is a string ID but session_id is not provided
|
||||
"""
|
||||
if isinstance(message, Message):
|
||||
message_id = message.id
|
||||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
|
||||
return await self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=resolved_session_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the AsyncHoncho client.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
from collections.abc import AsyncIterator, Callable
|
||||
from typing import TypeVar
|
||||
|
||||
from honcho_core.pagination import AsyncPage as AsyncPageCore
|
||||
from pydantic import Field, validate_call
|
||||
from typing_extensions import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
U = TypeVar("U", default=T)
|
||||
|
||||
|
||||
class AsyncPage(AsyncPageCore[U]):
|
||||
class AsyncPage(Generic[T, U]):
|
||||
"""
|
||||
Async paginated result wrapper that transforms objects from type T to type U.
|
||||
|
||||
|
|
@ -16,16 +15,13 @@ class AsyncPage(AsyncPageCore[U]):
|
|||
pagination functionality from the underlying core AsyncPage.
|
||||
"""
|
||||
|
||||
@validate_call
|
||||
_original_page: AsyncPageCore[T]
|
||||
_transform_func: Callable[[T], U] | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
original_page: AsyncPageCore[T] = Field(
|
||||
..., description="The original AsyncPage to wrap"
|
||||
),
|
||||
transform_func: Callable[[T], U] | None = Field(
|
||||
None,
|
||||
description="Optional function to transform objects from type T to type U",
|
||||
),
|
||||
original_page: AsyncPageCore[T],
|
||||
transform_func: Callable[[T], U] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the transformed async page.
|
||||
|
|
@ -35,57 +31,73 @@ class AsyncPage(AsyncPageCore[U]):
|
|||
transform_func: Optional function to transform objects from type T to type U.
|
||||
If None, objects are passed through unchanged.
|
||||
"""
|
||||
super().__init__(items=original_page.items) # pyright: ignore
|
||||
self._original_page = original_page # pyright: ignore
|
||||
self._transform_func = transform_func # pyright: ignore
|
||||
self._original_page = original_page
|
||||
self._transform_func = transform_func
|
||||
|
||||
@property
|
||||
def items(self) -> list[U]: # pyright: ignore
|
||||
"""Get all optionally transformed items as a list."""
|
||||
if self._transform_func is not None:
|
||||
return [self._transform_func(item) for item in self._original_page.items]
|
||||
return self._original_page.items # pyright: ignore
|
||||
|
||||
async def __aiter__(self) -> AsyncIterator[U]:
|
||||
"""Async iterate over optionally transformed objects."""
|
||||
async def __aiter__(self) -> AsyncIterator[U] | AsyncIterator[T]:
|
||||
"""Async iterate over all transformed items across all pages."""
|
||||
async for item in self._original_page:
|
||||
if self._transform_func is not None:
|
||||
yield self._transform_func(item)
|
||||
else:
|
||||
yield item # type: ignore # pyright: ignore
|
||||
yield item
|
||||
|
||||
async def __agetitem__(self, index: int) -> U:
|
||||
"""Get an optionally transformed object by index."""
|
||||
item = await self._original_page.__agetitem__(index) # type: ignore # pyright: ignore
|
||||
def __getitem__(self, index: int) -> U | T:
|
||||
"""Get a transformed item by index on the current page."""
|
||||
items = self._original_page.items or []
|
||||
item = items[index]
|
||||
if self._transform_func is not None:
|
||||
return self._transform_func(item) # pyright: ignore
|
||||
return item # type: ignore # pyright: ignore
|
||||
return self._transform_func(item)
|
||||
return item
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the length of the page."""
|
||||
return len(self._original_page) # type: ignore # pyright: ignore
|
||||
"""Get the number of items on the current page."""
|
||||
items = self._original_page.items or []
|
||||
return len(items)
|
||||
|
||||
@property
|
||||
async def data(self) -> list[U]:
|
||||
"""Get all optionally transformed data as a list."""
|
||||
data = await self._original_page.data # type: ignore # pyright: ignore
|
||||
def items(self) -> list[U] | list[T]:
|
||||
"""Get all transformed items on the current page."""
|
||||
items = self._original_page.items or []
|
||||
if self._transform_func is not None:
|
||||
return [self._transform_func(item) for item in data] # pyright: ignore
|
||||
return data # type: ignore # pyright: ignore
|
||||
return [self._transform_func(item) for item in items]
|
||||
return items
|
||||
|
||||
@property
|
||||
def object(self) -> str:
|
||||
"""Get the object type."""
|
||||
return self._original_page.object # type: ignore # pyright: ignore
|
||||
def total(self) -> int | None:
|
||||
"""Get the total number of items across all pages."""
|
||||
return self._original_page.total
|
||||
|
||||
@property
|
||||
def has_next_page(self) -> bool: # pyright: ignore
|
||||
def page(self) -> int | None:
|
||||
"""Get the current page number."""
|
||||
return self._original_page.page
|
||||
|
||||
@property
|
||||
def size(self) -> int | None:
|
||||
"""Get the page size."""
|
||||
return self._original_page.size
|
||||
|
||||
@property
|
||||
def pages(self) -> int | None:
|
||||
"""Get the total number of pages."""
|
||||
return self._original_page.pages
|
||||
|
||||
def has_next_page(self) -> bool:
|
||||
"""Check if there's a next page."""
|
||||
return self._original_page.has_next_page # type: ignore # pyright: ignore
|
||||
return self._original_page.has_next_page()
|
||||
|
||||
async def next_page(self) -> "AsyncPage[U] | None":
|
||||
"""Get the next page with optional transformation applied."""
|
||||
next_page = await self._original_page.next_page() # type: ignore # pyright: ignore
|
||||
if next_page is None:
|
||||
async def get_next_page(self) -> "AsyncPage[T, U] | None":
|
||||
"""
|
||||
Fetch the next page of results.
|
||||
|
||||
Returns None if there are no more pages.
|
||||
"""
|
||||
if not hasattr(self._original_page, "get_next_page"):
|
||||
return None
|
||||
return AsyncPage(next_page, self._transform_func) # pyright: ignore
|
||||
|
||||
next_original_page = await self._original_page.get_next_page()
|
||||
if not next_original_page:
|
||||
return None
|
||||
|
||||
return AsyncPage(next_original_page, self._transform_func)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types.workspaces import PeerCardResponse
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..types import DialecticStreamResponse
|
||||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -90,12 +94,12 @@ class AsyncPeer(BaseModel):
|
|||
"""
|
||||
peer = cls(peer_id, workspace_id, client)
|
||||
|
||||
if config or metadata:
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
|
||||
return peer
|
||||
|
|
@ -107,7 +111,7 @@ class AsyncPeer(BaseModel):
|
|||
stream: bool = False,
|
||||
target: str | AsyncPeer | None = None,
|
||||
session_id: str | None = None,
|
||||
) -> str | None:
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
||||
|
|
@ -125,9 +129,40 @@ class AsyncPeer(BaseModel):
|
|||
If provided, only information from that session is considered
|
||||
|
||||
Returns:
|
||||
Response string containing the answer to the query, or None if no
|
||||
relevant information is available
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
if stream:
|
||||
|
||||
async def stream_response() -> AsyncGenerator[str]:
|
||||
import json
|
||||
|
||||
# Use core SDK with_streaming_response
|
||||
async with self._client.workspaces.peers.with_streaming_response.chat(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
session_id=session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
async for line in response.iter_lines():
|
||||
if line.startswith("data: "):
|
||||
json_str = line[6:] # Remove "data: " prefix
|
||||
try:
|
||||
chunk_data = json.loads(json_str)
|
||||
if chunk_data.get("done"):
|
||||
break
|
||||
delta_obj = chunk_data.get("delta", {})
|
||||
content = delta_obj.get("content")
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return DialecticStreamResponse(stream_response())
|
||||
|
||||
response = await self._client.workspaces.peers.chat(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
|
|
@ -143,7 +178,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
async def get_sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncSession]:
|
||||
) -> AsyncPage[SessionCore, AsyncSession]:
|
||||
"""
|
||||
Get all sessions this peer is a member of.
|
||||
|
||||
|
|
@ -322,6 +357,40 @@ class AsyncPeer(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
async def card(
|
||||
self,
|
||||
target: str | AsyncPeer | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
||||
Makes an API call to retrieve the peer card, which contains a representation
|
||||
of what this peer knows. If a target is provided, returns this peer's local
|
||||
representation of the target peer.
|
||||
|
||||
Args:
|
||||
target: Optional target peer for local card. If provided, returns this
|
||||
peer's card of the target peer. Can be an AsyncPeer object or peer ID string.
|
||||
|
||||
Returns:
|
||||
A string containing the peer card joined with newlines, or an empty string if none is available
|
||||
"""
|
||||
# Validate target parameter
|
||||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
response: PeerCardResponse = await self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
)
|
||||
|
||||
if response.peer_card is None:
|
||||
return ""
|
||||
|
||||
items: list[str] = response.peer_card
|
||||
return "\n".join(items)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the AsyncPeer.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
|
@ -15,6 +19,8 @@ from .pagination import AsyncPage
|
|||
if TYPE_CHECKING:
|
||||
from .peer import AsyncPeer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionPeerConfig(BaseModel):
|
||||
observe_others: bool | None = Field(
|
||||
|
|
@ -105,12 +111,12 @@ class AsyncSession(BaseModel):
|
|||
"""
|
||||
session = cls(session_id, workspace_id, client)
|
||||
|
||||
if config or metadata:
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
|
||||
return session
|
||||
|
|
@ -296,10 +302,10 @@ class AsyncSession(BaseModel):
|
|||
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=NOT_GIVEN
|
||||
observe_others=omit
|
||||
if config.observe_others is None
|
||||
else config.observe_others,
|
||||
observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
|
||||
observe_me=omit if config.observe_me is None else config.observe_me,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -362,6 +368,17 @@ class AsyncSession(BaseModel):
|
|||
)
|
||||
return AsyncPage(messages_page)
|
||||
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
Delete this session.
|
||||
|
||||
Makes an async API call to delete this session.
|
||||
"""
|
||||
await self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
Get metadata for this session.
|
||||
|
|
@ -436,7 +453,7 @@ class AsyncSession(BaseModel):
|
|||
context = await self._client.workspaces.sessions.get_context(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
tokens=tokens if tokens is not None else NOT_GIVEN,
|
||||
tokens=tokens if tokens is not None else omit,
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
|
|
@ -615,6 +632,100 @@ class AsyncSession(BaseModel):
|
|||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
async def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
"""
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
async def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).",
|
||||
),
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0.
|
||||
This allows you to guarantee that all messages have been processed by the deriver for
|
||||
use with the dialectic endpoint.
|
||||
|
||||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
DeriverStatus when all work units are complete
|
||||
|
||||
Raises:
|
||||
TimeoutError: If timeout is exceeded before work units complete
|
||||
Exception: If get_deriver_status fails repeatedly
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
try:
|
||||
status = await self.get_deriver_status(observer_id, sender_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Check timeout after error
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Error during status check: {e}"
|
||||
) from e
|
||||
continue
|
||||
|
||||
if status.pending_work_units == 0 and status.in_progress_work_units == 0:
|
||||
return status
|
||||
|
||||
# Check timeout before sleeping
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Current status: {status.pending_work_units} pending, "
|
||||
+ f"{status.in_progress_work_units} in progress work units."
|
||||
)
|
||||
|
||||
# Sleep for the expected time to complete all current work units
|
||||
# Assuming each pending and in-progress work unit takes 1 second
|
||||
total_work_units = status.pending_work_units + status.in_progress_work_units
|
||||
sleep_time = max(1, total_work_units)
|
||||
|
||||
# Don't sleep past the timeout
|
||||
remaining_time = timeout - elapsed_time
|
||||
sleep_time = min(sleep_time, remaining_time)
|
||||
if sleep_time <= 0:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Current status: {status.pending_work_units} pending, "
|
||||
+ f"{status.in_progress_work_units} in progress work units."
|
||||
)
|
||||
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the AsyncSession.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from typing import Any, Literal
|
|||
import httpx
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.peer import Peer as PeerCore
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
|
|
@ -185,7 +187,9 @@ class Honcho(BaseModel):
|
|||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
|
||||
def get_peers(self, filters: dict[str, object] | None = None) -> SyncPage[Peer]:
|
||||
def get_peers(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> SyncPage[PeerCore, Peer]:
|
||||
"""
|
||||
Get all peers in the current workspace.
|
||||
|
||||
|
|
@ -247,7 +251,7 @@ class Honcho(BaseModel):
|
|||
|
||||
def get_sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> SyncPage[Session]:
|
||||
) -> SyncPage[SessionCore, Session]:
|
||||
"""
|
||||
Get all sessions in the current workspace.
|
||||
|
||||
|
|
@ -438,6 +442,53 @@ class Honcho(BaseModel):
|
|||
|
||||
time.sleep(sleep_time)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def update_message(
|
||||
self,
|
||||
message: Message | str = Field(
|
||||
..., description="The Message object or message ID to update"
|
||||
),
|
||||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
Update the metadata of a message.
|
||||
|
||||
Makes an API call to update the metadata of a specific message within a session.
|
||||
|
||||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
||||
Raises:
|
||||
ValidationError: If message is a string ID but session_id is not provided
|
||||
"""
|
||||
if isinstance(message, Message):
|
||||
message_id = message.id
|
||||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
|
||||
return self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=resolved_session_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Honcho client.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
from collections.abc import Callable, Iterator
|
||||
from typing import Optional, TypeVar # pyright: ignore
|
||||
from typing import Generic
|
||||
|
||||
from honcho_core.pagination import SyncPage as SyncPageCore
|
||||
from pydantic import Field, validate_call
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
U = TypeVar("U", default=T)
|
||||
|
||||
|
||||
class SyncPage(SyncPageCore[U]):
|
||||
class SyncPage(Generic[T, U]):
|
||||
"""
|
||||
Paginated result wrapper that transforms objects from type T to type U.
|
||||
|
||||
|
|
@ -16,16 +16,13 @@ class SyncPage(SyncPageCore[U]):
|
|||
pagination functionality from the underlying core SyncPage.
|
||||
"""
|
||||
|
||||
@validate_call
|
||||
def __init__( # pyright: ignore
|
||||
_original_page: SyncPageCore[T]
|
||||
_transform_func: Callable[[T], U] | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
original_page: SyncPageCore[T] = Field(
|
||||
..., description="The original SyncPage to wrap"
|
||||
),
|
||||
transform_func: Callable[[T], U] | None = Field(
|
||||
None,
|
||||
description="Optional function to transform objects from type T to type U",
|
||||
),
|
||||
original_page: SyncPageCore[T],
|
||||
transform_func: Callable[[T], U] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the transformed page.
|
||||
|
|
@ -35,48 +32,73 @@ class SyncPage(SyncPageCore[U]):
|
|||
transform_func: Optional function to transform objects from type T to type U.
|
||||
If None, objects are passed through unchanged.
|
||||
"""
|
||||
self._original_page = original_page # pyright: ignore
|
||||
self._transform_func = transform_func # pyright: ignore
|
||||
self._original_page = original_page
|
||||
self._transform_func = transform_func
|
||||
|
||||
def __iter__(self) -> Iterator[U]:
|
||||
"""Iterate over optionally transformed objects."""
|
||||
def __iter__(self) -> Iterator[U] | Iterator[T]:
|
||||
"""Iterate over all transformed items across all pages."""
|
||||
for item in self._original_page:
|
||||
if self._transform_func is not None:
|
||||
yield self._transform_func(item)
|
||||
else:
|
||||
yield item # pyright: ignore
|
||||
yield item
|
||||
|
||||
def __getitem__(self, index: int) -> U:
|
||||
"""Get an optionally transformed object by index."""
|
||||
item = self._original_page[index] # pyright: ignore
|
||||
def __getitem__(self, index: int) -> U | T:
|
||||
"""Get a transformed item by index on the current page."""
|
||||
items = self._original_page.items or []
|
||||
item = items[index]
|
||||
if self._transform_func is not None:
|
||||
return self._transform_func(item) # pyright: ignore
|
||||
return item # pyright: ignore
|
||||
return self._transform_func(item)
|
||||
return item
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the length of the page."""
|
||||
return len(self._original_page) # pyright: ignore
|
||||
"""Get the number of items on the current page."""
|
||||
items = self._original_page.items or []
|
||||
return len(items)
|
||||
|
||||
@property
|
||||
def data(self) -> list[U]:
|
||||
"""Get all optionally transformed data as a list."""
|
||||
def items(self) -> list[U] | list[T]:
|
||||
"""Get all transformed items on the current page."""
|
||||
items = self._original_page.items or []
|
||||
if self._transform_func is not None:
|
||||
return [self._transform_func(item) for item in self._original_page.data] # pyright: ignore
|
||||
return self._original_page.data # pyright: ignore
|
||||
return [self._transform_func(item) for item in items]
|
||||
return items
|
||||
|
||||
@property
|
||||
def object(self) -> str:
|
||||
"""Get the object type."""
|
||||
return self._original_page.object # pyright: ignore
|
||||
def total(self) -> int | None:
|
||||
"""Get the total number of items across all pages."""
|
||||
return self._original_page.total
|
||||
|
||||
@property
|
||||
def has_next_page(self) -> bool: # pyright: ignore
|
||||
def page(self) -> int | None:
|
||||
"""Get the current page number."""
|
||||
return self._original_page.page
|
||||
|
||||
@property
|
||||
def size(self) -> int | None:
|
||||
"""Get the page size."""
|
||||
return self._original_page.size
|
||||
|
||||
@property
|
||||
def pages(self) -> int | None:
|
||||
"""Get the total number of pages."""
|
||||
return self._original_page.pages
|
||||
|
||||
def has_next_page(self) -> bool:
|
||||
"""Check if there's a next page."""
|
||||
return self._original_page.has_next_page # pyright: ignore
|
||||
return self._original_page.has_next_page()
|
||||
|
||||
def next_page(self) -> Optional["SyncPage[U]"]: # pyright: ignore
|
||||
"""Get the next page with optional transformation applied."""
|
||||
next_page = self._original_page.next_page() # pyright: ignore
|
||||
if next_page is None:
|
||||
def get_next_page(self) -> "SyncPage[T, U] | None":
|
||||
"""
|
||||
Fetch the next page of results.
|
||||
|
||||
Returns None if there are no more pages.
|
||||
"""
|
||||
if not hasattr(self._original_page, "get_next_page"):
|
||||
return None
|
||||
return SyncPage(next_page, self._transform_func) # pyright: ignore
|
||||
|
||||
next_original_page = self._original_page.get_next_page()
|
||||
if not next_original_page:
|
||||
return None
|
||||
|
||||
return SyncPage(next_original_page, self._transform_func)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Generator
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types.workspaces import PeerCardResponse
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .types import DialecticStreamResponse
|
||||
from .pagination import SyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -76,12 +80,12 @@ class Peer(BaseModel):
|
|||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
self._client = client
|
||||
|
||||
if config or metadata:
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
|
||||
def chat(
|
||||
|
|
@ -91,7 +95,7 @@ class Peer(BaseModel):
|
|||
stream: bool = False,
|
||||
target: str | Peer | None = None,
|
||||
session_id: str | None = None,
|
||||
) -> str | None:
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
||||
|
|
@ -109,9 +113,40 @@ class Peer(BaseModel):
|
|||
If provided, only information from that session is considered
|
||||
|
||||
Returns:
|
||||
Response string containing the answer to the query, or None if no
|
||||
relevant information is available
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
if stream:
|
||||
|
||||
def stream_response() -> Generator[str, None, None]:
|
||||
import json
|
||||
|
||||
# Use core SDK with_streaming_response
|
||||
with self._client.workspaces.peers.with_streaming_response.chat(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
session_id=session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
for line in response.iter_lines():
|
||||
if line.startswith("data: "):
|
||||
json_str = line[6:] # Remove "data: " prefix
|
||||
try:
|
||||
chunk_data = json.loads(json_str)
|
||||
if chunk_data.get("done"):
|
||||
break
|
||||
delta_obj = chunk_data.get("delta", {})
|
||||
content = delta_obj.get("content")
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return DialecticStreamResponse(stream_response())
|
||||
|
||||
response = self._client.workspaces.peers.chat(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
|
|
@ -126,7 +161,7 @@ class Peer(BaseModel):
|
|||
|
||||
def get_sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> SyncPage[Session]:
|
||||
) -> SyncPage[SessionCore, Session]:
|
||||
"""
|
||||
Get all sessions this peer is a member of.
|
||||
|
||||
|
|
@ -305,6 +340,40 @@ class Peer(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
def card(
|
||||
self,
|
||||
target: str | Peer | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
||||
Makes an API call to retrieve the peer card, which contains a representation
|
||||
of what this peer knows. If a target is provided, returns this peer's local
|
||||
representation of the target peer.
|
||||
|
||||
Args:
|
||||
target: Optional target peer for local card. If provided, returns this
|
||||
peer's card of the target peer. Can be a Peer object or peer ID string.
|
||||
|
||||
Returns:
|
||||
A string containing the peer card joined with newlines, or an empty string if none is available
|
||||
"""
|
||||
# Validate target parameter
|
||||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
response: PeerCardResponse = self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
)
|
||||
if response.peer_card is None:
|
||||
return ""
|
||||
|
||||
items: list[str] = response.peer_card
|
||||
|
||||
return "\n".join(items)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Peer.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
|
@ -15,6 +18,8 @@ from .utils import prepare_file_for_upload
|
|||
if TYPE_CHECKING:
|
||||
from .peer import Peer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionPeerConfig(BaseModel):
|
||||
observe_others: bool | None = Field(
|
||||
|
|
@ -91,12 +96,12 @@ class Session(BaseModel):
|
|||
)
|
||||
self._client = client
|
||||
|
||||
if config or metadata:
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
|
||||
def add_peers(
|
||||
|
|
@ -275,10 +280,10 @@ class Session(BaseModel):
|
|||
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=NOT_GIVEN
|
||||
observe_others=omit
|
||||
if config.observe_others is None
|
||||
else config.observe_others,
|
||||
observe_me=NOT_GIVEN if config.observe_me is None else config.observe_me,
|
||||
observe_me=omit if config.observe_me is None else config.observe_me,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -360,6 +365,17 @@ class Session(BaseModel):
|
|||
or {}
|
||||
)
|
||||
|
||||
def delete(self) -> None:
|
||||
"""
|
||||
Delete this session
|
||||
|
||||
Makes an API call to mark this session as inactive.
|
||||
"""
|
||||
self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
self,
|
||||
|
|
@ -417,7 +433,7 @@ class Session(BaseModel):
|
|||
context = self._client.workspaces.sessions.get_context(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
tokens=tokens if tokens is not None else NOT_GIVEN,
|
||||
tokens=tokens if tokens is not None else omit,
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
|
|
@ -596,6 +612,100 @@ class Session(BaseModel):
|
|||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
"""
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
description="Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).",
|
||||
),
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Poll get_deriver_status until pending_work_units and in_progress_work_units are both 0.
|
||||
This allows you to guarantee that all messages have been processed by the deriver for
|
||||
use with the dialectic endpoint.
|
||||
|
||||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
DeriverStatus when all work units are complete
|
||||
|
||||
Raises:
|
||||
TimeoutError: If timeout is exceeded before work units complete
|
||||
Exception: If get_deriver_status fails repeatedly
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
try:
|
||||
status = self.get_deriver_status(observer_id, sender_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
time.sleep(1)
|
||||
|
||||
# Check timeout after error
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Error during status check: {e}"
|
||||
) from e
|
||||
continue
|
||||
|
||||
if status.pending_work_units == 0 and status.in_progress_work_units == 0:
|
||||
return status
|
||||
|
||||
# Check timeout before sleeping
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time >= timeout:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Current status: {status.pending_work_units} pending, "
|
||||
+ f"{status.in_progress_work_units} in progress work units."
|
||||
)
|
||||
|
||||
# Sleep for the expected time to complete all current work units
|
||||
# Assuming each pending and in-progress work unit takes 1 second
|
||||
total_work_units = status.pending_work_units + status.in_progress_work_units
|
||||
sleep_time = max(1, total_work_units)
|
||||
|
||||
# Don't sleep past the timeout
|
||||
remaining_time = timeout - elapsed_time
|
||||
sleep_time = min(sleep_time, remaining_time)
|
||||
if sleep_time <= 0:
|
||||
raise TimeoutError(
|
||||
f"Polling timeout exceeded after {timeout}s. "
|
||||
+ f"Current status: {status.pending_work_units} pending, "
|
||||
+ f"{status.in_progress_work_units} in progress work units."
|
||||
)
|
||||
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Session.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
"""Shared types for the Honcho SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, AsyncIterator
|
||||
|
||||
|
||||
class DialecticStreamResponse:
|
||||
"""
|
||||
Iterator for streaming dialectic responses with utilities for accessing the final response.
|
||||
|
||||
Similar to OpenAI and Anthropic streaming patterns, this allows you to:
|
||||
- Iterate over chunks as they arrive
|
||||
- Access the final accumulated response after streaming completes
|
||||
|
||||
Works with both sync and async iterators.
|
||||
|
||||
Example (sync):
|
||||
```python
|
||||
stream = peer.chat("Hello", stream=True)
|
||||
|
||||
# Stream chunks
|
||||
for chunk in stream:
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
# Get final response object
|
||||
final = stream.get_final_response()
|
||||
print(f"\\nFull content: {final['content']}")
|
||||
```
|
||||
|
||||
Example (async):
|
||||
```python
|
||||
stream = await peer.chat("Hello", stream=True)
|
||||
|
||||
# Stream chunks
|
||||
async for chunk in stream:
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
# Get final response object
|
||||
final = stream.get_final_response()
|
||||
print(f"\\nFull content: {final['content']}")
|
||||
```
|
||||
"""
|
||||
|
||||
_iterator: Iterator[str] | AsyncIterator[str]
|
||||
_accumulated_content: list[str]
|
||||
_is_complete: bool
|
||||
|
||||
def __init__(self, iterator: Iterator[str] | AsyncIterator[str]):
|
||||
self._iterator = iterator
|
||||
self._accumulated_content = []
|
||||
self._is_complete = False
|
||||
|
||||
# Sync iterator protocol
|
||||
def __iter__(self):
|
||||
if isinstance(self._iterator, Iterator):
|
||||
return self
|
||||
else:
|
||||
raise TypeError("iterator must be an sync iterator, got async iterator")
|
||||
|
||||
def __next__(self) -> str:
|
||||
try:
|
||||
if not isinstance(self._iterator, Iterator):
|
||||
raise TypeError("iterator must be an sync iterator, got async iterator")
|
||||
chunk = next(self._iterator) # type: ignore
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopIteration:
|
||||
self._is_complete = True
|
||||
raise
|
||||
|
||||
# Async iterator protocol
|
||||
def __aiter__(self):
|
||||
if isinstance(self._iterator, AsyncIterator):
|
||||
return self
|
||||
else:
|
||||
raise TypeError("iterator must be an async iterator, got sync iterator")
|
||||
|
||||
async def __anext__(self) -> str:
|
||||
try:
|
||||
if not isinstance(self._iterator, AsyncIterator):
|
||||
raise TypeError("iterator must be an async iterator, got sync iterator")
|
||||
chunk = await self._iterator.__anext__() # type: ignore
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopAsyncIteration:
|
||||
self._is_complete = True
|
||||
raise
|
||||
|
||||
def get_final_response(self) -> dict[str, str]:
|
||||
"""
|
||||
Get the final accumulated response after streaming completes.
|
||||
|
||||
Returns:
|
||||
A dictionary with the full content: {"content": "full accumulated text"}
|
||||
|
||||
Note:
|
||||
This should be called after the stream has been fully consumed.
|
||||
If called before completion, it returns the content accumulated so far.
|
||||
"""
|
||||
return {"content": "".join(self._accumulated_content)}
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if the stream has finished."""
|
||||
return self._is_complete
|
||||
|
|
@ -2,6 +2,7 @@ import { Honcho } from '../src/client';
|
|||
import { Peer } from '../src/peer';
|
||||
import { Session } from '../src/session';
|
||||
import { Page } from '../src/pagination';
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages';
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
|
|
@ -481,4 +482,93 @@ describe('Honcho Client', () => {
|
|||
await expect(honcho.pollDeriverStatus({ timeoutMs: 100 })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMessage', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.workspaces.sessions = {
|
||||
messages: {
|
||||
update: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should update message metadata using Message object', async () => {
|
||||
const mockMessage: Message = {
|
||||
id: 'msg-123',
|
||||
session_id: 'session-456',
|
||||
content: 'Test message',
|
||||
peer_id: 'peer-789',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 10,
|
||||
workspace_id: 'test-workspace',
|
||||
};
|
||||
const metadata = { updated: true, importance: 'high' };
|
||||
const mockUpdatedMessage = { ...mockMessage, metadata };
|
||||
|
||||
mockClient.workspaces.sessions.messages.update.mockResolvedValue(mockUpdatedMessage);
|
||||
|
||||
const result = await honcho.updateMessage(mockMessage, metadata);
|
||||
|
||||
expect(result).toEqual(mockUpdatedMessage);
|
||||
expect(mockClient.workspaces.sessions.messages.update).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'session-456',
|
||||
'msg-123',
|
||||
{ metadata }
|
||||
);
|
||||
});
|
||||
|
||||
it('should update message metadata using message ID and session ID', async () => {
|
||||
const messageId = 'msg-123';
|
||||
const sessionId = 'session-456';
|
||||
const metadata = { updated: true, importance: 'high' };
|
||||
const mockUpdatedMessage = {
|
||||
id: messageId,
|
||||
session_id: sessionId,
|
||||
content: 'Test message',
|
||||
peer_id: 'peer-789',
|
||||
metadata,
|
||||
};
|
||||
|
||||
mockClient.workspaces.sessions.messages.update.mockResolvedValue(mockUpdatedMessage);
|
||||
|
||||
const result = await honcho.updateMessage(messageId, metadata, sessionId);
|
||||
|
||||
expect(result).toEqual(mockUpdatedMessage);
|
||||
expect(mockClient.workspaces.sessions.messages.update).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
sessionId,
|
||||
messageId,
|
||||
{ metadata }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when message is string ID but session ID is not provided', async () => {
|
||||
const messageId = 'msg-123';
|
||||
const metadata = { updated: true };
|
||||
|
||||
await expect(honcho.updateMessage(messageId, metadata)).rejects.toThrow(
|
||||
'sessionId is required when message is a string ID'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const mockMessage: Message = {
|
||||
id: 'msg-123',
|
||||
session_id: 'session-456',
|
||||
content: 'Test message',
|
||||
peer_id: 'peer-789',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
token_count: 10,
|
||||
workspace_id: 'test-workspace',
|
||||
};
|
||||
const metadata = { updated: true };
|
||||
|
||||
mockClient.workspaces.sessions.messages.update.mockRejectedValue(
|
||||
new Error('Update failed')
|
||||
);
|
||||
|
||||
await expect(honcho.updateMessage(mockMessage, metadata)).rejects.toThrow('Update failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
'assistant',
|
||||
{
|
||||
query: 'How are you?',
|
||||
stream: undefined,
|
||||
stream: false,
|
||||
target: undefined,
|
||||
session_id: undefined,
|
||||
}
|
||||
|
|
@ -183,6 +183,11 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
for (const item of this.items) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
mockWorkspacesApi.workspaces.getOrCreate.mockResolvedValue({
|
||||
|
|
@ -302,8 +307,15 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
],
|
||||
total: 4,
|
||||
size: 2,
|
||||
page: 1,
|
||||
pages: 2,
|
||||
hasNextPage: true,
|
||||
nextPage: jest.fn(),
|
||||
getNextPage: jest.fn(),
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
for (const item of this.items) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const secondPageData = {
|
||||
|
|
@ -313,11 +325,18 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
],
|
||||
total: 4,
|
||||
size: 2,
|
||||
page: 2,
|
||||
pages: 2,
|
||||
hasNextPage: false,
|
||||
nextPage: jest.fn().mockResolvedValue(null),
|
||||
getNextPage: jest.fn().mockResolvedValue(null),
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
for (const item of this.items) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
firstPageData.nextPage.mockResolvedValue(secondPageData)
|
||||
firstPageData.getNextPage.mockResolvedValue(secondPageData)
|
||||
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue(firstPageData)
|
||||
|
||||
// Step 1: Get first page
|
||||
|
|
@ -327,20 +346,25 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
expect(firstPage.hasNextPage).toBe(true)
|
||||
|
||||
// Step 2: Get data from first page
|
||||
const firstPageData_ = await firstPage.data()
|
||||
const firstPageData_ = firstPage.items
|
||||
expect(firstPageData_).toHaveLength(2)
|
||||
expect(firstPageData_[0]).toBeInstanceOf(Peer)
|
||||
expect(firstPageData_[0].id).toBe('peer1')
|
||||
|
||||
// Step 3: Get next page
|
||||
const secondPage = await firstPage.nextPage()
|
||||
const secondPage = await firstPage.getNextPage()
|
||||
expect(secondPage).not.toBeNull()
|
||||
expect(secondPage!.hasNextPage).toBe(false)
|
||||
expect(secondPage!.page).toBe(2)
|
||||
|
||||
// Step 4: Get data from second page
|
||||
const secondPageData_ = await secondPage!.data()
|
||||
const secondPageData_ = secondPage!.items
|
||||
expect(secondPageData_).toHaveLength(2)
|
||||
expect(secondPageData_[0]).toBeInstanceOf(Peer)
|
||||
expect(secondPageData_[0].id).toBe('peer3')
|
||||
|
||||
// Step 5: Verify no more pages
|
||||
const thirdPage = await secondPage!.nextPage()
|
||||
const thirdPage = await secondPage!.getNextPage()
|
||||
expect(thirdPage).toBeNull()
|
||||
})
|
||||
|
||||
|
|
@ -405,7 +429,7 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
// Test empty peers list
|
||||
const peersPage = await honcho.getPeers()
|
||||
const peersList = await peersPage.data()
|
||||
const peersList = peersPage.items
|
||||
expect(peersList).toEqual([])
|
||||
|
||||
// Test empty context
|
||||
|
|
|
|||
|
|
@ -1,453 +1,330 @@
|
|||
import { Page } from '../src/pagination';
|
||||
import { Page } from '../src/pagination'
|
||||
|
||||
describe('Page', () => {
|
||||
let mockOriginalPage: any;
|
||||
let mockItems: any[];
|
||||
let mockOriginalPage: any
|
||||
let mockItems: any[]
|
||||
|
||||
beforeEach(() => {
|
||||
mockItems = [
|
||||
{ id: 'item1', name: 'Item 1' },
|
||||
{ id: 'item2', name: 'Item 2' },
|
||||
{ id: 'item3', name: 'Item 3' },
|
||||
];
|
||||
]
|
||||
|
||||
mockOriginalPage = {
|
||||
items: mockItems,
|
||||
data: mockItems,
|
||||
size: 3,
|
||||
total: 10,
|
||||
page: 1,
|
||||
pages: 4,
|
||||
hasNextPage: true,
|
||||
get: jest.fn(),
|
||||
nextPage: jest.fn(),
|
||||
};
|
||||
});
|
||||
[Symbol.asyncIterator]: async function*() {
|
||||
for (const item of mockItems) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
} as any
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with original page', () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
expect(page['_originalPage']).toBe(mockOriginalPage);
|
||||
expect(page['_transformFunc']).toBeUndefined();
|
||||
});
|
||||
expect(page['_originalPage']).toBe(mockOriginalPage)
|
||||
expect(page['_transformFunc']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should initialize with transform function', () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true });
|
||||
const page = new Page(mockOriginalPage, transformFunc);
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true })
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
|
||||
expect(page['_originalPage']).toBe(mockOriginalPage);
|
||||
expect(page['_transformFunc']).toBe(transformFunc);
|
||||
});
|
||||
});
|
||||
expect(page['_originalPage']).toBe(mockOriginalPage)
|
||||
expect(page['_transformFunc']).toBe(transformFunc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Symbol.asyncIterator', () => {
|
||||
it('should iterate through items without transform', async () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
const items: any[] = [];
|
||||
const page = new Page(mockOriginalPage)
|
||||
const items: any[] = []
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
expect(items).toEqual(mockItems);
|
||||
});
|
||||
expect(items).toEqual(mockItems)
|
||||
})
|
||||
|
||||
it('should iterate through items with transform', async () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true });
|
||||
const page = new Page(mockOriginalPage, transformFunc);
|
||||
const items: any[] = [];
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true })
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
const items: any[] = []
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
expect(items).toEqual([
|
||||
{ id: 'item1', name: 'Item 1', transformed: true },
|
||||
{ id: 'item2', name: 'Item 2', transformed: true },
|
||||
{ id: 'item3', name: 'Item 3', transformed: true },
|
||||
]);
|
||||
});
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle empty items array', async () => {
|
||||
const emptyPage = { items: [], data: [], size: 0, total: 0, hasNextPage: false };
|
||||
const page = new Page(emptyPage);
|
||||
const items: any[] = [];
|
||||
it('should handle transform function that throws error', async () => {
|
||||
const errorTransform = () => {
|
||||
throw new Error('Transform error')
|
||||
}
|
||||
const page = new Page(mockOriginalPage, errorTransform)
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
const iterate = async () => {
|
||||
for await (const item of page) {
|
||||
// This should throw
|
||||
}
|
||||
}
|
||||
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle page with data field instead of items', async () => {
|
||||
const pageWithData = {
|
||||
data: mockItems,
|
||||
size: 3,
|
||||
total: 10,
|
||||
hasNextPage: true,
|
||||
};
|
||||
const page = new Page(pageWithData);
|
||||
const items: any[] = [];
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
expect(items).toEqual(mockItems);
|
||||
});
|
||||
|
||||
it('should handle page with neither items nor data', async () => {
|
||||
const pageWithoutItems = {
|
||||
size: 0,
|
||||
total: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const page = new Page(pageWithoutItems);
|
||||
const items: any[] = [];
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
expect(items).toEqual([]);
|
||||
});
|
||||
});
|
||||
await expect(iterate()).rejects.toThrow('Transform error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('should get item by index without transform', async () => {
|
||||
mockOriginalPage.get.mockResolvedValue(mockItems[1]);
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should get item by index without transform', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const item = await page.get(1);
|
||||
const item = page.get(1)
|
||||
|
||||
expect(item).toEqual(mockItems[1]);
|
||||
expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
|
||||
});
|
||||
expect(item).toEqual(mockItems[1])
|
||||
})
|
||||
|
||||
it('should get item by index with transform', async () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true });
|
||||
mockOriginalPage.get.mockResolvedValue(mockItems[1]);
|
||||
const page = new Page(mockOriginalPage, transformFunc);
|
||||
it('should get item by index with transform', () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true })
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
|
||||
const item = await page.get(1);
|
||||
const item = page.get(1)
|
||||
|
||||
expect(item).toEqual({ ...mockItems[1], transformed: true });
|
||||
expect(mockOriginalPage.get).toHaveBeenCalledWith(1);
|
||||
});
|
||||
expect(item).toEqual({ ...mockItems[1], transformed: true })
|
||||
})
|
||||
|
||||
it('should handle out of bounds index', async () => {
|
||||
mockOriginalPage.get.mockResolvedValue(undefined);
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should handle out of bounds index', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const item = await page.get(999);
|
||||
expect(() => page.get(999)).toThrow(
|
||||
'Index 999 is out of bounds for page with 3 items'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
expect(item).toBeUndefined();
|
||||
expect(mockOriginalPage.get).toHaveBeenCalledWith(999);
|
||||
});
|
||||
describe('length getter', () => {
|
||||
it('should return length of items array', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
it('should handle negative index', async () => {
|
||||
mockOriginalPage.get.mockResolvedValue(undefined);
|
||||
const page = new Page(mockOriginalPage);
|
||||
expect(page.length).toBe(3)
|
||||
})
|
||||
|
||||
const item = await page.get(-1);
|
||||
it('should handle empty items array', () => {
|
||||
const emptyPage = { ...mockOriginalPage, items: [] }
|
||||
const page = new Page(emptyPage)
|
||||
|
||||
expect(item).toBeUndefined();
|
||||
expect(mockOriginalPage.get).toHaveBeenCalledWith(-1);
|
||||
});
|
||||
});
|
||||
expect(page.length).toBe(0)
|
||||
})
|
||||
|
||||
describe('size getter', () => {
|
||||
it('should return size from original page', () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should handle undefined items', () => {
|
||||
const noItemsPage = { ...mockOriginalPage, items: undefined }
|
||||
const page = new Page(noItemsPage)
|
||||
|
||||
expect(page.size).toBe(3);
|
||||
});
|
||||
expect(page.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle missing size', () => {
|
||||
const pageWithoutSize = { items: mockItems };
|
||||
const page = new Page(pageWithoutSize);
|
||||
describe('items getter', () => {
|
||||
it('should return items array without transform', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
expect(page.size).toBeUndefined();
|
||||
});
|
||||
});
|
||||
const data = page.items
|
||||
|
||||
describe('total getter', () => {
|
||||
it('should return total from original page', () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
expect(data).toEqual(mockItems)
|
||||
})
|
||||
|
||||
expect(page.total).toBe(10);
|
||||
});
|
||||
it('should return items array with transform', () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true })
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
|
||||
it('should handle missing total', () => {
|
||||
const pageWithoutTotal = { items: mockItems };
|
||||
const page = new Page(pageWithoutTotal);
|
||||
|
||||
expect(page.total).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('should return data array without transform', async () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
|
||||
const data = await page.data();
|
||||
|
||||
expect(data).toEqual(mockItems);
|
||||
});
|
||||
|
||||
it('should return data array with transform', async () => {
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true });
|
||||
const page = new Page(mockOriginalPage, transformFunc);
|
||||
|
||||
const data = await page.data();
|
||||
const data = page.items
|
||||
|
||||
expect(data).toEqual([
|
||||
{ id: 'item1', name: 'Item 1', transformed: true },
|
||||
{ id: 'item2', name: 'Item 2', transformed: true },
|
||||
{ id: 'item3', name: 'Item 3', transformed: true },
|
||||
]);
|
||||
});
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle data as function', async () => {
|
||||
const mockDataFunction = jest.fn().mockResolvedValue(mockItems);
|
||||
const pageWithDataFunction = {
|
||||
data: mockDataFunction,
|
||||
size: 3,
|
||||
total: 10,
|
||||
hasNextPage: true,
|
||||
};
|
||||
const page = new Page(pageWithDataFunction);
|
||||
it('should handle transform function returning null', () => {
|
||||
const nullTransform = () => null
|
||||
const page = new Page(mockOriginalPage, nullTransform)
|
||||
|
||||
const data = await page.data();
|
||||
const data = page.items
|
||||
|
||||
expect(data).toEqual(mockItems);
|
||||
expect(mockDataFunction).toHaveBeenCalled();
|
||||
});
|
||||
expect(data).toEqual([null, null, null])
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle items as function', async () => {
|
||||
const mockItemsFunction = jest.fn().mockResolvedValue(mockItems);
|
||||
const pageWithItemsFunction = {
|
||||
items: mockItemsFunction,
|
||||
size: 3,
|
||||
total: 10,
|
||||
hasNextPage: true,
|
||||
};
|
||||
const page = new Page(pageWithItemsFunction);
|
||||
describe('pagination metadata getters', () => {
|
||||
it('should return total from original page', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const data = await page.data();
|
||||
expect(page.total).toBe(10)
|
||||
})
|
||||
|
||||
expect(data).toEqual(mockItems);
|
||||
expect(mockItemsFunction).toHaveBeenCalled();
|
||||
});
|
||||
it('should return page number from original page', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
it('should handle empty data', async () => {
|
||||
const emptyPage = { items: [], size: 0, total: 0, hasNextPage: false };
|
||||
const page = new Page(emptyPage);
|
||||
expect(page.page).toBe(1)
|
||||
})
|
||||
|
||||
const data = await page.data();
|
||||
it('should return size from original page', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
expect(page.size).toBe(3)
|
||||
})
|
||||
|
||||
it('should prioritize items over data field', async () => {
|
||||
const pageWithBoth = {
|
||||
items: [{ id: 'from-items' }],
|
||||
data: [{ id: 'from-data' }],
|
||||
size: 1,
|
||||
total: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const page = new Page(pageWithBoth);
|
||||
it('should return pages from original page', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const data = await page.data();
|
||||
expect(page.pages).toBe(4)
|
||||
})
|
||||
|
||||
expect(data).toEqual([{ id: 'from-items' }]);
|
||||
});
|
||||
});
|
||||
it('should handle undefined metadata', () => {
|
||||
const minimalPage = { items: mockItems }
|
||||
const page = new Page(minimalPage as any)
|
||||
|
||||
expect(page.total).toBeUndefined()
|
||||
expect(page.page).toBeUndefined()
|
||||
expect(page.size).toBeUndefined()
|
||||
expect(page.pages).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasNextPage getter', () => {
|
||||
it('should return hasNextPage from original page', () => {
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should return true when hasNextPage is true', () => {
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
expect(page.hasNextPage).toBe(true);
|
||||
});
|
||||
expect(page.hasNextPage).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle missing hasNextPage', () => {
|
||||
const pageWithoutHasNextPage = { items: mockItems };
|
||||
const page = new Page(pageWithoutHasNextPage);
|
||||
it('should return false when hasNextPage is false', () => {
|
||||
const lastPage = { ...mockOriginalPage, hasNextPage: false }
|
||||
const page = new Page(lastPage)
|
||||
|
||||
expect(page.hasNextPage).toBeUndefined();
|
||||
});
|
||||
expect(page.hasNextPage).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle false hasNextPage', () => {
|
||||
const lastPage = { ...mockOriginalPage, hasNextPage: false };
|
||||
const page = new Page(lastPage);
|
||||
it('should call hasNextPage function if it is a function', () => {
|
||||
const hasNextPageFn = jest.fn(() => true)
|
||||
const pageWithFn = { ...mockOriginalPage, hasNextPage: hasNextPageFn }
|
||||
const page = new Page(pageWithFn)
|
||||
|
||||
expect(page.hasNextPage).toBe(false);
|
||||
});
|
||||
});
|
||||
const result = page.hasNextPage
|
||||
|
||||
describe('nextPage', () => {
|
||||
expect(result).toBe(true)
|
||||
expect(hasNextPageFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return false when hasNextPage is undefined', () => {
|
||||
const noNextPage = { ...mockOriginalPage }
|
||||
delete noNextPage.hasNextPage
|
||||
const page = new Page(noNextPage)
|
||||
|
||||
expect(page.hasNextPage).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextPage', () => {
|
||||
it('should return next page with same transform function', async () => {
|
||||
const nextPageData = {
|
||||
items: [{ id: 'item4', name: 'Item 4' }],
|
||||
size: 1,
|
||||
total: 10,
|
||||
page: 2,
|
||||
pages: 4,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true });
|
||||
mockOriginalPage.nextPage.mockResolvedValue(nextPageData);
|
||||
const page = new Page(mockOriginalPage, transformFunc);
|
||||
[Symbol.asyncIterator]: async function*() {
|
||||
for (const item of this.items) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
}
|
||||
const transformFunc = (item: any) => ({ ...item, transformed: true })
|
||||
mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData)
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
|
||||
const nextPage = await page.nextPage();
|
||||
const nextPage = await page.getNextPage()
|
||||
|
||||
expect(nextPage).toBeInstanceOf(Page);
|
||||
expect(nextPage!['_transformFunc']).toBe(transformFunc);
|
||||
expect(mockOriginalPage.nextPage).toHaveBeenCalled();
|
||||
});
|
||||
expect(nextPage).toBeInstanceOf(Page)
|
||||
expect(nextPage!['_transformFunc']).toBe(transformFunc)
|
||||
expect(mockOriginalPage.getNextPage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return null when no next page', async () => {
|
||||
mockOriginalPage.nextPage.mockResolvedValue(null);
|
||||
const page = new Page(mockOriginalPage);
|
||||
mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(null)
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const nextPage = await page.nextPage();
|
||||
const nextPage = await page.getNextPage()
|
||||
|
||||
expect(nextPage).toBeNull();
|
||||
});
|
||||
expect(nextPage).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle next page returning undefined', async () => {
|
||||
mockOriginalPage.nextPage.mockResolvedValue(undefined);
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should return null when getNextPage returns undefined', async () => {
|
||||
mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(undefined)
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
const nextPage = await page.nextPage();
|
||||
const nextPage = await page.getNextPage()
|
||||
|
||||
expect(nextPage).toBeNull();
|
||||
});
|
||||
expect(nextPage).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle error from original page nextPage', async () => {
|
||||
mockOriginalPage.nextPage.mockRejectedValue(new Error('Failed to get next page'));
|
||||
const page = new Page(mockOriginalPage);
|
||||
it('should return null when getNextPage method does not exist', async () => {
|
||||
const pageWithoutNext = { ...mockOriginalPage }
|
||||
delete pageWithoutNext.getNextPage
|
||||
const page = new Page(pageWithoutNext)
|
||||
|
||||
await expect(page.nextPage()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
const nextPage = await page.getNextPage()
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle null original page', () => {
|
||||
const page = new Page(null);
|
||||
expect(nextPage).toBeNull()
|
||||
})
|
||||
|
||||
expect(page['_originalPage']).toBeNull();
|
||||
expect(page.size).toBeUndefined();
|
||||
expect(page.total).toBeUndefined();
|
||||
expect(page.hasNextPage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle transform function that throws error', async () => {
|
||||
const errorTransform = () => {
|
||||
throw new Error('Transform error');
|
||||
};
|
||||
const page = new Page(mockOriginalPage, errorTransform);
|
||||
|
||||
await expect(async () => {
|
||||
for await (const item of page) {
|
||||
// This should throw
|
||||
}
|
||||
}).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle transform function returning null', async () => {
|
||||
const nullTransform = () => null;
|
||||
const page = new Page(mockOriginalPage, nullTransform);
|
||||
const items: any[] = [];
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
expect(items).toEqual([null, null, null]);
|
||||
});
|
||||
|
||||
it('should handle very large datasets', async () => {
|
||||
const largeItems = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
|
||||
const largePage = {
|
||||
items: largeItems,
|
||||
size: 10000,
|
||||
total: 10000,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const page = new Page(largePage);
|
||||
let count = 0;
|
||||
|
||||
for await (const item of page) {
|
||||
count++;
|
||||
if (count > 10) break; // Don't actually iterate through all 10k items
|
||||
}
|
||||
|
||||
expect(count).toBe(11);
|
||||
});
|
||||
|
||||
it('should handle circular reference in items', async () => {
|
||||
const circularItem: any = { id: 'circular' };
|
||||
circularItem.self = circularItem;
|
||||
const pageWithCircular = {
|
||||
items: [circularItem],
|
||||
size: 1,
|
||||
total: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const page = new Page(pageWithCircular);
|
||||
const items: any[] = [];
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].id).toBe('circular');
|
||||
expect(items[0].self).toBe(items[0]);
|
||||
});
|
||||
|
||||
it('should handle items with complex nested structures', async () => {
|
||||
const complexItems = [
|
||||
{
|
||||
id: 'complex1',
|
||||
nested: {
|
||||
deep: {
|
||||
value: 'deeply nested',
|
||||
array: [1, 2, { nested: 'array object' }],
|
||||
},
|
||||
},
|
||||
metadata: new Map([['key', 'value']]),
|
||||
it('should propagate transform function to next page', async () => {
|
||||
const nextPageData = {
|
||||
items: [{ id: 'item4', name: 'Item 4' }],
|
||||
[Symbol.asyncIterator]: async function*() {
|
||||
for (const item of this.items) {
|
||||
yield item
|
||||
}
|
||||
},
|
||||
];
|
||||
const complexPage = {
|
||||
items: complexItems,
|
||||
size: 1,
|
||||
total: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const page = new Page(complexPage);
|
||||
const items: any[] = [];
|
||||
|
||||
for await (const item of page) {
|
||||
items.push(item);
|
||||
}
|
||||
const transformFunc = (item: any) => ({ ...item, count: 999 })
|
||||
mockOriginalPage.getNextPage = jest.fn().mockResolvedValue(nextPageData)
|
||||
const page = new Page(mockOriginalPage, transformFunc)
|
||||
|
||||
expect(items).toEqual(complexItems);
|
||||
expect(items[0].nested.deep.array[2].nested).toBe('array object');
|
||||
});
|
||||
const nextPage = await page.getNextPage()
|
||||
|
||||
it('should handle async transform function', async () => {
|
||||
const asyncTransform = async (item: any) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
return { ...item, asyncTransformed: true };
|
||||
};
|
||||
mockOriginalPage.get.mockResolvedValue(mockItems[0]);
|
||||
const page = new Page(mockOriginalPage, asyncTransform);
|
||||
expect(nextPage).not.toBeNull()
|
||||
const transformedItem = nextPage!.get(0)
|
||||
expect(transformedItem).toEqual({
|
||||
id: 'item4',
|
||||
name: 'Item 4',
|
||||
count: 999,
|
||||
})
|
||||
})
|
||||
|
||||
const item = await page.get(0);
|
||||
it('should handle error from getNextPage', async () => {
|
||||
mockOriginalPage.getNextPage = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Failed to get next page'))
|
||||
const page = new Page(mockOriginalPage)
|
||||
|
||||
expect(item).toEqual({ ...mockItems[0], asyncTransformed: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
await expect(page.getNextPage()).rejects.toThrow(
|
||||
'Failed to get next page'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('Peer', () => {
|
|||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: undefined, target: undefined, session_id: undefined }
|
||||
{ query: 'Hello', stream: false, target: undefined, session_id: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -89,17 +89,9 @@ describe('Peer', () => {
|
|||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle chat with streaming option', async () => {
|
||||
const mockResponse = { content: 'Streamed response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
await peer.chat('Hello', { stream: true });
|
||||
|
||||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: true, target: undefined, session_id: undefined }
|
||||
);
|
||||
it.skip('should handle chat with streaming option', async () => {
|
||||
// Skipped: streaming now uses fetch API directly, not the mocked client
|
||||
// TODO: Add proper streaming tests with fetch mocking when needed
|
||||
});
|
||||
|
||||
it('should handle chat with target peer', async () => {
|
||||
|
|
@ -112,7 +104,7 @@ describe('Peer', () => {
|
|||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: undefined, target: 'target-peer', session_id: undefined }
|
||||
{ query: 'Hello', stream: false, target: 'target-peer', session_id: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -125,7 +117,7 @@ describe('Peer', () => {
|
|||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: undefined, target: 'string-target', session_id: undefined }
|
||||
{ query: 'Hello', stream: false, target: 'string-target', session_id: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -138,23 +130,24 @@ describe('Peer', () => {
|
|||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: undefined, target: undefined, session_id: 'session-123' }
|
||||
{ query: 'Hello', stream: false, target: undefined, session_id: 'session-123' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all options together', async () => {
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockResponse = { content: 'Full options response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
// TODO: Re-enable after regenerating Stainless SDK with streaming support
|
||||
// it('should handle all options together', async () => {
|
||||
// const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
// const mockResponse = { content: 'Full options response' };
|
||||
// mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
await peer.chat('Hello', { stream: true, target: targetPeer, sessionId: 'session-456' });
|
||||
// await peer.chat('Hello', { stream: true, target: targetPeer, sessionId: 'session-456' });
|
||||
|
||||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'Hello', stream: true, target: 'target-peer', session_id: 'session-456' }
|
||||
);
|
||||
});
|
||||
// expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
// 'test-workspace',
|
||||
// 'test-peer',
|
||||
// { query: 'Hello', stream: true, target: 'target-peer', session_id: 'session-456' }
|
||||
// );
|
||||
// });
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.chat.mockRejectedValue(new Error('Chat failed'));
|
||||
|
|
@ -429,4 +422,96 @@ describe('Peer', () => {
|
|||
await expect(peer.search('test')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('card', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.workspaces.peers.card = jest.fn();
|
||||
});
|
||||
|
||||
it('should get peer card without target', async () => {
|
||||
const mockCardResponse = {
|
||||
peer_card: ['Fact 1 about peer', 'Fact 2 about peer', 'Fact 3 about peer'],
|
||||
};
|
||||
mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
|
||||
|
||||
const result = await peer.card();
|
||||
|
||||
expect(result).toBe('Fact 1 about peer\nFact 2 about peer\nFact 3 about peer');
|
||||
expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ target: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
it('should get peer card with target as string', async () => {
|
||||
const mockCardResponse = {
|
||||
peer_card: ['What peer knows about target'],
|
||||
};
|
||||
mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
|
||||
|
||||
const result = await peer.card('target-peer');
|
||||
|
||||
expect(result).toBe('What peer knows about target');
|
||||
expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ target: 'target-peer' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should get peer card with target as Peer object', async () => {
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockCardResponse = {
|
||||
peer_card: ['What peer knows about target peer'],
|
||||
};
|
||||
mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
|
||||
|
||||
const result = await peer.card(targetPeer);
|
||||
|
||||
expect(result).toBe('What peer knows about target peer');
|
||||
expect(mockClient.workspaces.peers.card).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ target: 'target-peer' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty string when peer_card is null', async () => {
|
||||
const mockCardResponse = {
|
||||
peer_card: null,
|
||||
};
|
||||
mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
|
||||
|
||||
const result = await peer.card();
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when peer_card is undefined', async () => {
|
||||
const mockCardResponse = {};
|
||||
mockClient.workspaces.peers.card.mockResolvedValue(mockCardResponse);
|
||||
|
||||
const result = await peer.card();
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should throw error for empty string target', async () => {
|
||||
await expect(peer.card('')).rejects.toThrow('target string cannot be empty');
|
||||
await expect(peer.card(' ')).rejects.toThrow('target string cannot be empty');
|
||||
});
|
||||
|
||||
it('should throw error for invalid target type', async () => {
|
||||
await expect(peer.card(123 as any)).rejects.toThrow('target must be string, Peer, or undefined');
|
||||
await expect(peer.card(null as any)).rejects.toThrow('target must be string, Peer, or undefined');
|
||||
await expect(peer.card({} as any)).rejects.toThrow('target must be string, Peer, or undefined');
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.card.mockRejectedValue(new Error('Card fetch failed'));
|
||||
|
||||
await expect(peer.card()).rejects.toThrow('Card fetch failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,7 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.4.0",
|
||||
"@honcho-ai/core": "^1.4.1-alpha.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
|
|
@ -18,15 +18,13 @@
|
|||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
|
|
@ -34,7 +32,7 @@
|
|||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
|
|
@ -44,9 +42,9 @@
|
|||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="],
|
||||
|
||||
|
|
@ -84,31 +82,33 @@
|
|||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.1.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.3", "@biomejs/cli-darwin-x64": "2.1.3", "@biomejs/cli-linux-arm64": "2.1.3", "@biomejs/cli-linux-arm64-musl": "2.1.3", "@biomejs/cli-linux-x64": "2.1.3", "@biomejs/cli-linux-x64-musl": "2.1.3", "@biomejs/cli-win32-arm64": "2.1.3", "@biomejs/cli-win32-x64": "2.1.3" }, "bin": { "biome": "bin/biome" } }, "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.4.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-pHyUgkiIlf+pMfZJx7NmJ1UH8yOKWZ+vmPRummg3olgstMXApcBrW0bhJTPjHWoGGA+OFYcOPxJBoZKY6VCtMw=="],
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.4.1-alpha.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-cRp3ug8cBe9E7NZDgCXkI0EHIgm8x5Jq/nQB0xA5629GuniS99yq6M786I//FjOZhB2YEZoOxd4Zkg412uk5Yg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
|
||||
|
||||
|
|
@ -142,13 +142,15 @@
|
|||
|
||||
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
|
||||
|
||||
|
|
@ -156,6 +158,40 @@
|
|||
|
||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
|
||||
|
||||
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
|
||||
|
||||
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
|
||||
|
||||
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
|
||||
|
||||
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
|
@ -174,7 +210,7 @@
|
|||
|
||||
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
"@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
|
|
@ -186,6 +222,10 @@
|
|||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
|
@ -196,6 +236,8 @@
|
|||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
|
@ -212,11 +254,13 @@
|
|||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||
"browserslist": ["browserslist@4.26.2", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
|
||||
|
||||
"bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="],
|
||||
|
||||
|
|
@ -230,7 +274,7 @@
|
|||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001746", "", {}, "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
|
|
@ -258,11 +302,13 @@
|
|||
|
||||
"create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="],
|
||||
|
||||
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
|
|
@ -270,17 +316,19 @@
|
|||
|
||||
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
|
||||
|
||||
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||
|
||||
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.197", "", {}, "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.228", "", {}, "sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA=="],
|
||||
|
||||
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
|
|
@ -388,7 +436,7 @@
|
|||
|
||||
"istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="],
|
||||
|
||||
"istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="],
|
||||
"istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
|
||||
|
||||
"jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="],
|
||||
|
||||
|
|
@ -498,7 +546,7 @@
|
|||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
"node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
|
|
@ -594,20 +642,24 @@
|
|||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-jest": ["ts-jest@29.4.1", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw=="],
|
||||
"ts-jest": ["ts-jest@29.4.4", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw=="],
|
||||
|
||||
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
|
||||
|
||||
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
|
||||
|
||||
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
|
||||
|
|
@ -636,6 +688,8 @@
|
|||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
|
||||
|
|
@ -644,7 +698,9 @@
|
|||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="],
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.129", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.4.0",
|
||||
"@honcho-ai/core": "^1.4.1-alpha.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import HonchoCore from '@honcho-ai/core'
|
||||
import type { DefaultQuery } from '@honcho-ai/core/src/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { DefaultQuery } from '@honcho-ai/core/core'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import type {
|
||||
DeriverStatus,
|
||||
WorkspaceDeriverStatusParams,
|
||||
} from '@honcho-ai/core/src/resources/workspaces/workspaces'
|
||||
} from '@honcho-ai/core/resources/workspaces/workspaces'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import { Session } from './session'
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
type HonchoConfig,
|
||||
HonchoConfigSchema,
|
||||
LimitSchema,
|
||||
MessageMetadataSchema,
|
||||
type PeerConfig,
|
||||
PeerConfigSchema,
|
||||
PeerIdSchema,
|
||||
|
|
@ -449,6 +450,47 @@ export class Honcho {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metadata of a message.
|
||||
*
|
||||
* Makes an API call to update the metadata of a specific message within a session.
|
||||
*
|
||||
* @param message - Either a Message object or a message ID string
|
||||
* @param metadata - The metadata to update for the message
|
||||
* @param sessionId - The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
* @returns Promise resolving to the updated Message object
|
||||
* @throws Error if message is a string ID but sessionId is not provided
|
||||
*/
|
||||
async updateMessage(
|
||||
message: Message | string,
|
||||
metadata: Record<string, unknown>,
|
||||
sessionId?: string
|
||||
): Promise<Message> {
|
||||
const validatedMetadata = MessageMetadataSchema.parse(metadata)
|
||||
let messageId: string
|
||||
let resolvedSessionId: string
|
||||
|
||||
if (typeof message === 'string') {
|
||||
messageId = message
|
||||
if (!sessionId) {
|
||||
throw new Error('sessionId is required when message is a string ID')
|
||||
}
|
||||
resolvedSessionId = sessionId
|
||||
} else {
|
||||
messageId = message.id
|
||||
resolvedSessionId = message.session_id
|
||||
}
|
||||
|
||||
return await this._client.workspaces.sessions.messages.update(
|
||||
this.workspaceId,
|
||||
resolvedSessionId,
|
||||
messageId,
|
||||
{
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Honcho client.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Main entry point for the Honcho TypeScript SDK
|
||||
// Exports all main classes and types
|
||||
|
||||
export { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
export type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
export { Honcho } from './client'
|
||||
export { Page } from './pagination'
|
||||
export { Peer } from './peer'
|
||||
|
|
@ -12,6 +12,11 @@ export {
|
|||
Summary,
|
||||
SummaryData,
|
||||
} from './session_context'
|
||||
export {
|
||||
type DialecticStreamChunk,
|
||||
type DialecticStreamDelta,
|
||||
DialecticStreamResponse,
|
||||
} from './types'
|
||||
|
||||
// Export validation types for advanced usage
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -1,80 +1,127 @@
|
|||
import type { Page as CorePage } from '@honcho-ai/core/pagination'
|
||||
|
||||
/**
|
||||
* Generic paginated result wrapper for Honcho SDK.
|
||||
* Provides async iteration and transformation capabilities.
|
||||
* Provides async iteration and transformation capabilities while preserving
|
||||
* pagination functionality from the underlying core Page.
|
||||
*/
|
||||
export class Page<T> implements AsyncIterable<T> {
|
||||
private _originalPage: any
|
||||
private _transformFunc?: (item: any) => T
|
||||
export class Page<T, TOriginal = any> implements AsyncIterable<T> {
|
||||
private _originalPage: CorePage<TOriginal>
|
||||
private _transformFunc?: (item: TOriginal) => T
|
||||
|
||||
/**
|
||||
* Initialize a new Page.
|
||||
*
|
||||
* @param originalPage - The original Page to wrap
|
||||
* @param transformFunc - Optional function to transform objects from the original type to type T.
|
||||
* If not provided, objects are passed through unchanged.
|
||||
*/
|
||||
constructor(originalPage: any, transformFunc?: (item: any) => T) {
|
||||
constructor(
|
||||
originalPage: CorePage<TOriginal>,
|
||||
transformFunc?: (item: TOriginal) => T
|
||||
) {
|
||||
this._originalPage = originalPage
|
||||
this._transformFunc = transformFunc
|
||||
}
|
||||
|
||||
/**
|
||||
* Async iterator for the page's items.
|
||||
* Async iterator for all transformed items across all pages.
|
||||
*/
|
||||
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
// Handle different page structure formats
|
||||
const items = this._originalPage.items || this._originalPage.data || []
|
||||
for (const item of items) {
|
||||
yield this._transformFunc ? this._transformFunc(item) : item
|
||||
for await (const item of this._originalPage) {
|
||||
yield this._transformFunc
|
||||
? this._transformFunc(item)
|
||||
: (item as unknown as T)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item by index.
|
||||
* Get a transformed item by index on the current page.
|
||||
*/
|
||||
async get(index: number): Promise<T> {
|
||||
if (
|
||||
!this._originalPage?.get ||
|
||||
typeof this._originalPage.get !== 'function'
|
||||
) {
|
||||
throw new Error('Original page does not support indexed access')
|
||||
get(index: number): T {
|
||||
const items = this._originalPage.items || []
|
||||
if (index < 0 || index >= items.length) {
|
||||
throw new RangeError(
|
||||
`Index ${index} is out of bounds for page with ${items.length} items`
|
||||
)
|
||||
}
|
||||
const item = await this._originalPage.get(index)
|
||||
return this._transformFunc ? this._transformFunc(item) : item
|
||||
const item = items[index]
|
||||
return this._transformFunc
|
||||
? this._transformFunc(item)
|
||||
: (item as unknown as T)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the page.
|
||||
* Get the number of items on the current page.
|
||||
*/
|
||||
get size(): number {
|
||||
return this._originalPage?.size
|
||||
get length(): number {
|
||||
const items = this._originalPage.items || []
|
||||
return items.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of items.
|
||||
* Get all transformed items on the current page.
|
||||
*/
|
||||
get total(): number {
|
||||
get items(): T[] {
|
||||
const items = this._originalPage.items || []
|
||||
return this._transformFunc
|
||||
? items.map(this._transformFunc)
|
||||
: (items as unknown as T[])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of items across all pages.
|
||||
*/
|
||||
get total(): number | undefined {
|
||||
return this._originalPage?.total
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all data as a list.
|
||||
* Get the current page number.
|
||||
*/
|
||||
async data(): Promise<T[]> {
|
||||
const items = this._originalPage.items || this._originalPage.data || []
|
||||
const data = typeof items === 'function' ? await items() : items
|
||||
return this._transformFunc ? data.map(this._transformFunc) : data
|
||||
get page(): number | undefined {
|
||||
return this._originalPage?.page
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page size.
|
||||
*/
|
||||
get size(): number | undefined {
|
||||
return this._originalPage?.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of pages.
|
||||
*/
|
||||
get pages(): number | undefined {
|
||||
return this._originalPage?.pages
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a next page.
|
||||
*/
|
||||
get hasNextPage(): boolean {
|
||||
return this._originalPage?.hasNextPage
|
||||
const hasNext = this._originalPage.hasNextPage
|
||||
if (typeof hasNext === 'function') {
|
||||
return hasNext()
|
||||
}
|
||||
return hasNext || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next page.
|
||||
* Fetch the next page of results.
|
||||
* Returns null if there are no more pages.
|
||||
*/
|
||||
async nextPage(): Promise<Page<T> | null> {
|
||||
const nextPage = await this._originalPage.nextPage()
|
||||
if (!nextPage) return null
|
||||
return new Page(nextPage, this._transformFunc)
|
||||
async getNextPage(): Promise<Page<T, TOriginal> | null> {
|
||||
if (!this._originalPage.getNextPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextOriginalPage = await this._originalPage.getNextPage()
|
||||
if (!nextOriginalPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Page(nextOriginalPage, this._transformFunc)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import { Page } from './pagination'
|
||||
import { Session } from './session'
|
||||
import { type DialecticStreamChunk, DialecticStreamResponse } from './types'
|
||||
import {
|
||||
ChatQuerySchema,
|
||||
FilterSchema,
|
||||
|
|
@ -61,8 +62,9 @@ export class Peer {
|
|||
* querying the peer's global representation
|
||||
* @param sessionId - Optional session ID to scope the query to a specific session.
|
||||
* If provided, only information from that session is considered
|
||||
* @returns Promise resolving to response string containing the answer to the query,
|
||||
* or null if no relevant information is available
|
||||
* @returns Promise resolving to:
|
||||
* - For non-streaming: response string or null if no relevant information
|
||||
* - For streaming: DialecticStreamResponse that can be iterated over
|
||||
*/
|
||||
async chat(
|
||||
query: string,
|
||||
|
|
@ -71,19 +73,98 @@ export class Peer {
|
|||
target?: string | Peer
|
||||
sessionId?: string
|
||||
}
|
||||
): Promise<string | null> {
|
||||
): Promise<string | DialecticStreamResponse | null> {
|
||||
const chatParams = ChatQuerySchema.parse({
|
||||
query,
|
||||
stream: options?.stream,
|
||||
target: options?.target,
|
||||
sessionId: options?.sessionId,
|
||||
})
|
||||
|
||||
if (chatParams.stream) {
|
||||
const body = {
|
||||
query: chatParams.query,
|
||||
stream: true,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: chatParams.sessionId,
|
||||
}
|
||||
|
||||
const url = `${this._client.baseURL}/v2/workspaces/${this.workspaceId}/peers/${this.id}/chat`
|
||||
const apiKey = this._client.apiKey
|
||||
|
||||
async function* streamResponse(): AsyncGenerator<
|
||||
string,
|
||||
void,
|
||||
undefined
|
||||
> {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
// Include auth headers if present
|
||||
...(apiKey && {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is null')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const jsonStr = line.slice(6) // Remove "data: " prefix
|
||||
try {
|
||||
const chunkData: DialecticStreamChunk = JSON.parse(jsonStr)
|
||||
if (chunkData.done) {
|
||||
return
|
||||
}
|
||||
const content = chunkData.delta.content
|
||||
if (content) {
|
||||
yield content
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
return new DialecticStreamResponse(streamResponse())
|
||||
}
|
||||
|
||||
const response = await this._client.workspaces.peers.chat(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
query: chatParams.query,
|
||||
stream: chatParams.stream,
|
||||
stream: false,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
|
|
@ -258,6 +339,50 @@ export class Peer {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the peer card for this peer.
|
||||
*
|
||||
* Makes an API call to retrieve the peer card, which contains a representation
|
||||
* of what this peer knows. If a target is provided, returns this peer's local
|
||||
* representation of the target peer.
|
||||
*
|
||||
* @param target - Optional target peer for local card. If provided, returns this
|
||||
* peer's card of the target peer. Can be a Peer object or peer ID string.
|
||||
* @returns Promise resolving to a string containing the peer card
|
||||
*/
|
||||
async card(target?: string | Peer): Promise<string> {
|
||||
// Validate target parameter
|
||||
if (
|
||||
target !== undefined &&
|
||||
typeof target !== 'string' &&
|
||||
!(target instanceof Peer)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`target must be string, Peer, or undefined, got ${typeof target}`
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof target === 'string' && target.trim().length === 0) {
|
||||
throw new Error('target string cannot be empty')
|
||||
}
|
||||
|
||||
const response = await this._client.workspaces.peers.card(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
target: target instanceof Peer ? target.id : target,
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.peer_card) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const items: string[] = response.peer_card
|
||||
|
||||
return items.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Peer.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Uploadable } from '@honcho-ai/core/src/uploads'
|
||||
import type {
|
||||
DeriverStatus,
|
||||
WorkspaceDeriverStatusParams,
|
||||
} from '@honcho-ai/core/resources/index'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import type { Uploadable } from '@honcho-ai/core/uploads'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import { SessionContext, SessionSummaries, Summary } from './session_context'
|
||||
import {
|
||||
ContextParamsSchema,
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FileUploadSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
|
|
@ -427,6 +433,15 @@ export class Session {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this session.
|
||||
*
|
||||
* Makes an API call to mark this session as inactive.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this._client.workspaces.sessions.delete(this.workspaceId, this.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized context for this session within a token limit.
|
||||
*
|
||||
|
|
@ -532,6 +547,112 @@ export class Session {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deriver processing status for this session, optionally scoped to an observer or sender.
|
||||
*
|
||||
* Makes an API call to retrieve the current status of the deriver processing queue.
|
||||
* The deriver is responsible for processing messages and updating peer representations.
|
||||
* This method automatically scopes the status to this session.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const queryParams: WorkspaceDeriverStatusParams = {
|
||||
session_id: this.id, // Always use this session's ID
|
||||
}
|
||||
if (validatedOptions?.observerId)
|
||||
queryParams.observer_id = validatedOptions.observerId
|
||||
if (validatedOptions?.senderId)
|
||||
queryParams.sender_id = validatedOptions.senderId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
queryParams
|
||||
)
|
||||
|
||||
return {
|
||||
totalWorkUnits: status.total_work_units,
|
||||
completedWorkUnits: status.completed_work_units,
|
||||
inProgressWorkUnits: status.in_progress_work_units,
|
||||
pendingWorkUnits: status.pending_work_units,
|
||||
sessions: status.sessions || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll getDeriverStatus until pending_work_units and in_progress_work_units are both 0.
|
||||
* This allows you to guarantee that all messages have been processed by the deriver for
|
||||
* use with the dialectic endpoint.
|
||||
*
|
||||
* The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @returns Promise resolving to the final deriver status when processing is complete
|
||||
* @throws Error if timeout is exceeded before processing completes
|
||||
*/
|
||||
async pollDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
||||
// Check if timeout has been exceeded
|
||||
const elapsedTime = Date.now() - startTime
|
||||
if (elapsedTime >= timeoutMs) {
|
||||
throw new Error(
|
||||
`Polling timeout exceeded after ${timeoutMs}ms. ` +
|
||||
`Current status: ${status.pendingWorkUnits} pending, ${status.inProgressWorkUnits} in progress work units.`
|
||||
)
|
||||
}
|
||||
|
||||
// Sleep for the expected time to complete all current work units
|
||||
// Assuming each pending and in-progress work unit takes 1 second
|
||||
const totalWorkUnits =
|
||||
status.pendingWorkUnits + status.inProgressWorkUnits
|
||||
const sleepMs = Math.max(1000, totalWorkUnits * 1000) // Sleep at least 1 second
|
||||
|
||||
// Ensure we don't sleep past the timeout
|
||||
const remainingTime = timeoutMs - elapsedTime
|
||||
const actualSleepMs = Math.min(sleepMs, remainingTime)
|
||||
|
||||
if (actualSleepMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, actualSleepMs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to create messages in this session.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import type { Peer } from './peer'
|
||||
|
||||
export interface SummaryData {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Shared types for the Honcho TypeScript SDK.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Delta object for streaming dialectic responses.
|
||||
*/
|
||||
export interface DialecticStreamDelta {
|
||||
content?: string
|
||||
// Future fields can be added here:
|
||||
// premises?: string[]
|
||||
// tokens?: number
|
||||
// analytics?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk in a streaming dialectic response.
|
||||
*/
|
||||
export interface DialecticStreamChunk {
|
||||
delta: DialecticStreamDelta
|
||||
done: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator for streaming dialectic responses with utilities for accessing the final response.
|
||||
*
|
||||
* Similar to OpenAI and Anthropic streaming patterns, this allows you to:
|
||||
* - Iterate over chunks as they arrive
|
||||
* - Access the final accumulated response after streaming completes
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const stream = await peer.chat("Hello", { stream: true })
|
||||
*
|
||||
* // Stream chunks
|
||||
* for await (const chunk of stream) {
|
||||
* process.stdout.write(chunk)
|
||||
* }
|
||||
*
|
||||
* // Get final response object
|
||||
* const final = stream.getFinalResponse()
|
||||
* console.log(`\nFull content: ${final.content}`)
|
||||
* ```
|
||||
*/
|
||||
export class DialecticStreamResponse implements AsyncIterable<string> {
|
||||
private iterator: AsyncIterator<string>
|
||||
private accumulatedContent: string[] = []
|
||||
private _isComplete = false
|
||||
|
||||
constructor(iterator: AsyncIterator<string>) {
|
||||
this.iterator = iterator
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterator<string> {
|
||||
return {
|
||||
next: async () => {
|
||||
const result = await this.iterator.next()
|
||||
if (result.done) {
|
||||
this._isComplete = true
|
||||
return { done: true, value: undefined }
|
||||
}
|
||||
this.accumulatedContent.push(result.value)
|
||||
return { done: false, value: result.value }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the final accumulated response after streaming completes.
|
||||
*
|
||||
* @returns An object with the full content
|
||||
*
|
||||
* @note This should be called after the stream has been fully consumed.
|
||||
* If called before completion, it returns the content accumulated so far.
|
||||
*/
|
||||
getFinalResponse(): { content: string } {
|
||||
return { content: this.accumulatedContent.join('') }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the stream has finished.
|
||||
*/
|
||||
get isComplete(): boolean {
|
||||
return this._isComplete
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ export const FilterSchema = z.record(z.string(), z.unknown()).optional()
|
|||
*/
|
||||
export const ChatQuerySchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
stream: z.boolean().optional(),
|
||||
stream: z.boolean().optional().default(false),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sessionId: z.string().optional(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ class DeriverSettings(HonchoSettings):
|
|||
STALE_SESSION_TIMEOUT_MINUTES: Annotated[int, Field(default=5, gt=0, le=1440)] = 5
|
||||
|
||||
PROVIDER: SupportedProviders = "google"
|
||||
MODEL: str = "gemini-2.5-flash"
|
||||
MODEL: str = "gemini-2.5-flash-lite"
|
||||
|
||||
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2500, gt=0, le=100_000)] = 2500
|
||||
# Thinking budget tokens are only applied when using Anthropic as provider
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import sentry_sdk
|
||||
from langfuse import get_client
|
||||
from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
|
|
@ -51,19 +52,29 @@ async def process_item(task_type: str, queue_payload: dict[str, Any]) -> None:
|
|||
queue_payload,
|
||||
)
|
||||
raise ValueError(f"Invalid payload structure: {str(e)}") from e
|
||||
|
||||
if settings.LANGFUSE_PUBLIC_KEY:
|
||||
with lf.start_as_current_span(
|
||||
name="summary_processing",
|
||||
input={
|
||||
"workspace_name": validated.workspace_name,
|
||||
"session_name": validated.session_name,
|
||||
"message_id": validated.message_id,
|
||||
},
|
||||
metadata={
|
||||
"summary_model": settings.SUMMARY.MODEL,
|
||||
},
|
||||
):
|
||||
with sentry_sdk.start_transaction(name="process_summary_task", op="deriver"):
|
||||
if settings.LANGFUSE_PUBLIC_KEY:
|
||||
with lf.start_as_current_span(
|
||||
name="summary_processing",
|
||||
input={
|
||||
"workspace_name": validated.workspace_name,
|
||||
"session_name": validated.session_name,
|
||||
"message_id": validated.message_id,
|
||||
},
|
||||
metadata={
|
||||
"summary_model": settings.SUMMARY.MODEL,
|
||||
},
|
||||
):
|
||||
await summarizer.summarize_if_needed(
|
||||
validated.workspace_name,
|
||||
validated.session_name,
|
||||
validated.message_id,
|
||||
validated.message_seq_in_session,
|
||||
)
|
||||
log_performance_metrics(
|
||||
f"summary_{validated.workspace_name}_{validated.message_id}"
|
||||
)
|
||||
else:
|
||||
await summarizer.summarize_if_needed(
|
||||
validated.workspace_name,
|
||||
validated.session_name,
|
||||
|
|
@ -73,16 +84,6 @@ async def process_item(task_type: str, queue_payload: dict[str, Any]) -> None:
|
|||
log_performance_metrics(
|
||||
f"summary_{validated.workspace_name}_{validated.message_id}"
|
||||
)
|
||||
else:
|
||||
await summarizer.summarize_if_needed(
|
||||
validated.workspace_name,
|
||||
validated.session_name,
|
||||
validated.message_id,
|
||||
validated.message_seq_in_session,
|
||||
)
|
||||
log_performance_metrics(
|
||||
f"summary_{validated.workspace_name}_{validated.message_id}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid task type: {task_type}")
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from src.utils.shared_models import (
|
|||
ReasoningResponseWithThinking,
|
||||
UnifiedObservation,
|
||||
)
|
||||
from src.utils.tracing import with_sentry_transaction
|
||||
|
||||
from .prompts import (
|
||||
critical_analysis_prompt,
|
||||
|
|
@ -116,7 +117,7 @@ async def peer_card_call(
|
|||
return response.content
|
||||
|
||||
|
||||
@sentry_sdk.trace
|
||||
@with_sentry_transaction("process_representation_tasks_batch", op="deriver")
|
||||
async def process_representation_tasks_batch(
|
||||
sender_name: str,
|
||||
target_name: str,
|
||||
|
|
|
|||
|
|
@ -288,8 +288,6 @@ class QueueManager:
|
|||
######################
|
||||
# Queue Worker Logic #
|
||||
######################
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_work_unit(self, work_unit_key: str, worker_id: str) -> None:
|
||||
"""Process all messages for a specific work unit by routing to the correct handler."""
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ app = FastAPI(
|
|||
title="Honcho API",
|
||||
summary="The Identity Layer for the Agentic World",
|
||||
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
|
||||
version="2.3.2",
|
||||
version="2.3.3",
|
||||
contact={
|
||||
"name": "Plastic Labs",
|
||||
"url": "https://honcho.dev",
|
||||
|
|
|
|||
|
|
@ -137,11 +137,15 @@ async def get_sessions_for_peer(
|
|||
|
||||
@router.post(
|
||||
"/{peer_id}/chat",
|
||||
response_model=schemas.DialecticResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "Response to a question informed by Honcho's User Representation",
|
||||
"content": {"text/event-stream": {}},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": schemas.DialecticResponse.model_json_schema()
|
||||
},
|
||||
"text/event-stream": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies=[
|
||||
|
|
@ -186,7 +190,17 @@ async def chat(
|
|||
)
|
||||
if isinstance(stream, AsyncIterator):
|
||||
async for chunk in stream:
|
||||
yield chunk.content
|
||||
if chunk.content:
|
||||
# Send each chunk as JSON with nested delta object
|
||||
stream_chunk = schemas.DialecticStreamChunk(
|
||||
delta=schemas.DialecticStreamDelta(content=chunk.content)
|
||||
)
|
||||
yield f"data: {stream_chunk.model_dump_json()}\n\n"
|
||||
# Send final done message
|
||||
final_chunk = schemas.DialecticStreamChunk(
|
||||
delta=schemas.DialecticStreamDelta(), done=True
|
||||
)
|
||||
yield f"data: {final_chunk.model_dump_json()}\n\n"
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Invalid stream type")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -315,6 +315,23 @@ class DialecticResponse(BaseModel):
|
|||
content: str
|
||||
|
||||
|
||||
class DialecticStreamDelta(BaseModel):
|
||||
"""Delta object for streaming dialectic responses."""
|
||||
|
||||
content: str | None = None
|
||||
# Future fields can be added here:
|
||||
# premises: str | None = None
|
||||
# tokens: int | None = None
|
||||
# analytics: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class DialecticStreamChunk(BaseModel):
|
||||
"""Chunk in a streaming dialectic response."""
|
||||
|
||||
delta: DialecticStreamDelta
|
||||
done: bool = False
|
||||
|
||||
|
||||
class SessionCounts(BaseModel):
|
||||
"""Counts for a specific session in queue processing."""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import ParamSpec, TypeVar, cast
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def with_sentry_transaction(
|
||||
name: str, op: str
|
||||
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
||||
"""Decorator to wrap a function in a Sentry transaction.
|
||||
|
||||
Args:
|
||||
name: The name of the transaction.
|
||||
op: The operation type (e.g., "deriver", "http.server").
|
||||
|
||||
Returns:
|
||||
A decorator that wraps the target function in a Sentry transaction.
|
||||
|
||||
Example:
|
||||
@with_sentry_transaction("process_task", op="deriver")
|
||||
def process_task(data):
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
with sentry_sdk.start_transaction(name=name, op=op):
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return cast(Callable[P, T], async_wrapper)
|
||||
else:
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
with sentry_sdk.start_transaction(name=name, op=op):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
|
@ -563,8 +563,10 @@ except Exception as e:
|
|||
|
||||
# Create an empty .env file in temp directory to satisfy docker-compose
|
||||
# (even though the database service doesn't actually use it)
|
||||
if self.temp_dir:
|
||||
if self.temp_dir and self.temp_dir.exists():
|
||||
(self.temp_dir / ".env").touch()
|
||||
else:
|
||||
raise Exception("Temporary directory does not exist")
|
||||
|
||||
# Start database
|
||||
self.start_database()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.async_client.pagination import AsyncPage
|
||||
|
|
@ -318,3 +319,118 @@ async def test_poll_deriver_status(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_with_message_object(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests updating message metadata using a Message object.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-update-msg-session")
|
||||
peer = await honcho_client.peer(id="test-update-msg-peer")
|
||||
|
||||
# Create a message
|
||||
await session.add_messages([peer.message("test message")])
|
||||
messages = await session.get_messages()
|
||||
assert len(messages) >= 1
|
||||
message = messages[0]
|
||||
assert isinstance(message, Message)
|
||||
|
||||
# Update using Message object
|
||||
updated = await honcho_client.update_message(message, {"key": "value"})
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"key": "value"}
|
||||
assert updated.id == message.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-update-msg-session")
|
||||
peer = honcho_client.peer(id="test-update-msg-peer")
|
||||
|
||||
# Create a message
|
||||
session.add_messages([peer.message("test message")])
|
||||
messages = session.get_messages()
|
||||
assert len(messages) >= 1
|
||||
message = messages[0]
|
||||
assert isinstance(message, Message)
|
||||
|
||||
# Update using Message object
|
||||
updated = honcho_client.update_message(message, {"key": "value"})
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"key": "value"}
|
||||
assert updated.id == message.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_with_message_id(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests updating message metadata using message_id string.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-update-msg-id-session")
|
||||
peer = await honcho_client.peer(id="test-update-msg-id-peer")
|
||||
|
||||
# Create a message
|
||||
await session.add_messages([peer.message("test message")])
|
||||
|
||||
messages = await session.get_messages()
|
||||
assert len(messages) >= 1
|
||||
message = messages[0]
|
||||
assert message.metadata == {}
|
||||
|
||||
# Update using message_id string
|
||||
updated = await honcho_client.update_message(
|
||||
message.id, {"updated": True}, session_id=session.id
|
||||
)
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"updated": True}
|
||||
assert updated.id == message.id
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-update-msg-id-session")
|
||||
peer = honcho_client.peer(id="test-update-msg-id-peer")
|
||||
|
||||
# Create a message
|
||||
messages = session.add_messages([peer.message("test message")])
|
||||
|
||||
messages = session.get_messages()
|
||||
assert len(messages) >= 1
|
||||
message = messages[0]
|
||||
assert message.metadata == {}
|
||||
|
||||
# Update using message_id string
|
||||
updated = honcho_client.update_message(
|
||||
message.id, {"updated": True}, session_id=session.id
|
||||
)
|
||||
assert isinstance(updated, Message)
|
||||
assert updated.metadata == {"updated": True}
|
||||
assert updated.id == message.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_validation(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that update_message raises ValueError when message_id is provided without session_id.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
with pytest.raises(ValueError, match="session_id is required"):
|
||||
await honcho_client.update_message("msg_123", {"key": "value"})
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
with pytest.raises(ValueError, match="session_id is required"):
|
||||
honcho_client.update_message("msg_123", {"key": "value"})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,284 @@
|
|||
import pytest
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.async_client.pagination import AsyncPage
|
||||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.pagination import SyncPage
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_page_get_next_page(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that SyncPage.get_next_page() works correctly for sync clients.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "sync":
|
||||
pytest.skip("Test only applicable to sync client")
|
||||
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create multiple peers to test pagination
|
||||
for i in range(15):
|
||||
peer = honcho_client.peer(id=f"pagination-test-peer-{i}")
|
||||
peer.get_metadata() # Create the peer
|
||||
|
||||
# Get first page - SDK doesn't expose size parameter yet, so we get default pagination
|
||||
first_page = honcho_client.get_peers()
|
||||
assert isinstance(first_page, SyncPage)
|
||||
assert len(first_page.items) > 0
|
||||
|
||||
# If there's a next page, test get_next_page()
|
||||
if first_page.has_next_page():
|
||||
second_page = first_page.get_next_page()
|
||||
assert second_page is not None
|
||||
assert isinstance(second_page, SyncPage)
|
||||
assert second_page.page == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_page_get_next_page(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that AsyncPage.get_next_page() works correctly for async clients.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "async":
|
||||
pytest.skip("Test only applicable to async client")
|
||||
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create multiple peers to test pagination
|
||||
for i in range(15):
|
||||
peer = await honcho_client.peer(id=f"pagination-test-peer-async-{i}")
|
||||
await peer.get_metadata() # Create the peer
|
||||
|
||||
# Get first page - SDK doesn't expose size parameter yet, so we get default pagination
|
||||
first_page = await honcho_client.get_peers()
|
||||
assert isinstance(first_page, AsyncPage)
|
||||
assert len(first_page.items) > 0
|
||||
|
||||
# If there's a next page, test get_next_page()
|
||||
if first_page.has_next_page():
|
||||
second_page = await first_page.get_next_page()
|
||||
assert second_page is not None
|
||||
assert isinstance(second_page, AsyncPage)
|
||||
assert second_page.page == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_page_transform_preserved_across_pages(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that transformation function is preserved when getting next page (sync).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "sync":
|
||||
pytest.skip("Test only applicable to sync client")
|
||||
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create multiple peers to ensure pagination
|
||||
for i in range(25):
|
||||
peer = honcho_client.peer(id=f"transform-test-peer-{i}")
|
||||
peer.get_metadata()
|
||||
|
||||
# Get first page
|
||||
first_page = honcho_client.get_peers()
|
||||
assert isinstance(first_page, SyncPage)
|
||||
|
||||
# Verify items are Peer instances (transformed)
|
||||
for item in first_page.items:
|
||||
assert isinstance(item, Peer)
|
||||
|
||||
# Get next page and verify transformation is preserved
|
||||
if first_page.has_next_page():
|
||||
second_page = first_page.get_next_page()
|
||||
assert second_page is not None
|
||||
for item in second_page.items:
|
||||
assert isinstance(item, Peer)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_page_transform_preserved_across_pages(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that transformation function is preserved when getting next page (async).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "async":
|
||||
pytest.skip("Test only applicable to async client")
|
||||
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create multiple peers to ensure pagination
|
||||
for i in range(25):
|
||||
peer = await honcho_client.peer(id=f"transform-test-peer-async-{i}")
|
||||
await peer.get_metadata()
|
||||
|
||||
# Get first page
|
||||
first_page = await honcho_client.get_peers()
|
||||
assert isinstance(first_page, AsyncPage)
|
||||
|
||||
# Verify items are AsyncPeer instances (transformed)
|
||||
for item in first_page.items:
|
||||
assert isinstance(item, AsyncPeer)
|
||||
|
||||
# Get next page and verify transformation is preserved
|
||||
if first_page.has_next_page():
|
||||
second_page = await first_page.get_next_page()
|
||||
assert second_page is not None
|
||||
for item in second_page.items:
|
||||
assert isinstance(item, AsyncPeer)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_page_get_next_page_throws_exception_on_last_page(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that get_next_page() returns None when on the last page (sync).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "sync":
|
||||
pytest.skip("Test only applicable to sync client")
|
||||
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create just a few peers to ensure we're on the last page
|
||||
for i in range(3):
|
||||
peer = honcho_client.peer(id=f"last-page-test-peer-{i}")
|
||||
peer.get_metadata()
|
||||
|
||||
# Get first page
|
||||
first_page = honcho_client.get_peers()
|
||||
assert isinstance(first_page, SyncPage)
|
||||
|
||||
# Should be on last page (or only page)
|
||||
if not first_page.has_next_page():
|
||||
# get_next_page should return None
|
||||
try:
|
||||
first_page.get_next_page()
|
||||
except Exception as e:
|
||||
assert isinstance(e, RuntimeError)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_page_get_next_page_throws_exception_on_last_page(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests that get_next_page() returns None when on the last page (async).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "async":
|
||||
pytest.skip("Test only applicable to async client")
|
||||
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create just a few peers to ensure we're on the last page
|
||||
for i in range(3):
|
||||
peer = await honcho_client.peer(id=f"last-page-test-peer-async-{i}")
|
||||
await peer.get_metadata()
|
||||
|
||||
# Get first page
|
||||
first_page = await honcho_client.get_peers()
|
||||
assert isinstance(first_page, AsyncPage)
|
||||
|
||||
# Should be on last page (or only page)
|
||||
if not first_page.has_next_page():
|
||||
# get_next_page should throw runtime error
|
||||
try:
|
||||
await first_page.get_next_page()
|
||||
except Exception as e:
|
||||
assert isinstance(e, RuntimeError)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_page_manual_pagination(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests manual pagination with get_next_page (sync).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "sync":
|
||||
pytest.skip("Test only applicable to sync client")
|
||||
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
|
||||
# Create enough peers to ensure multiple pages
|
||||
for i in range(25):
|
||||
peer = honcho_client.peer(id=f"manual-pagination-test-peer-{i}")
|
||||
peer.get_metadata()
|
||||
|
||||
# Collect items via manual pagination
|
||||
manual_items = []
|
||||
manual_page = honcho_client.get_peers()
|
||||
page_count = 0
|
||||
|
||||
while manual_page is not None:
|
||||
page_count += 1
|
||||
manual_items.extend(manual_page.items) # pyright: ignore
|
||||
|
||||
if not manual_page.has_next_page():
|
||||
break
|
||||
|
||||
manual_page = manual_page.get_next_page()
|
||||
|
||||
# Should have collected all items
|
||||
assert len(manual_items) >= 25 # pyright: ignore
|
||||
# All items should be Peer instances
|
||||
assert all(isinstance(item, Peer) for item in manual_items) # pyright: ignore
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_page_manual_pagination(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests manual pagination with get_next_page (async).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type != "async":
|
||||
pytest.skip("Test only applicable to async client")
|
||||
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
|
||||
# Create enough peers to ensure multiple pages
|
||||
for i in range(25):
|
||||
peer = await honcho_client.peer(id=f"manual-pagination-test-peer-async-{i}")
|
||||
await peer.get_metadata()
|
||||
|
||||
# Collect items via manual pagination
|
||||
manual_items = []
|
||||
manual_page = await honcho_client.get_peers()
|
||||
page_count = 0
|
||||
|
||||
while manual_page is not None:
|
||||
page_count += 1
|
||||
manual_items.extend(manual_page.items) # pyright: ignore
|
||||
|
||||
if not manual_page.has_next_page():
|
||||
break
|
||||
|
||||
manual_page = await manual_page.get_next_page()
|
||||
|
||||
# Should have collected all items
|
||||
assert len(manual_items) >= 25 # pyright: ignore
|
||||
# All items should be AsyncPeer instances
|
||||
assert all(isinstance(item, AsyncPeer) for item in manual_items) # pyright: ignore
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
from sdks.python.src.honcho.types import DialecticStreamResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -74,3 +77,211 @@ async def test_peer_get_sessions(client_fixture: tuple[Honcho | AsyncHoncho, str
|
|||
session_ids = {s.id for s in sessions}
|
||||
assert "s1" in session_ids
|
||||
assert "s2" in session_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_card_global(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
||||
"""
|
||||
Tests getting a global peer card (no target).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-card-global-peer")
|
||||
session = await honcho_client.session(id="test-card-global-session")
|
||||
|
||||
# Add some messages to create context
|
||||
await session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Get global peer card
|
||||
card_response = await peer.card()
|
||||
assert isinstance(card_response, str)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-card-global-peer")
|
||||
session = honcho_client.session(id="test-card-global-session")
|
||||
|
||||
# Add some messages to create context
|
||||
session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Get global peer card
|
||||
card_response = peer.card()
|
||||
assert isinstance(card_response, str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_card_local(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
||||
"""
|
||||
Tests getting a local peer card (with target).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
observer = await honcho_client.peer(id="test-card-local-observer")
|
||||
target = await honcho_client.peer(id="test-card-local-target")
|
||||
session = await honcho_client.session(id="test-card-local-session")
|
||||
|
||||
# Add messages from both peers
|
||||
await session.add_messages(
|
||||
[observer.message("Hello"), target.message("Hi there")]
|
||||
)
|
||||
|
||||
# Get local peer card with target as Peer object
|
||||
card_response = await observer.card(target=target)
|
||||
assert isinstance(card_response, str)
|
||||
|
||||
# Get local peer card with target as string
|
||||
card_response = await observer.card(target=target.id)
|
||||
assert isinstance(card_response, str)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
observer = honcho_client.peer(id="test-card-local-observer")
|
||||
target = honcho_client.peer(id="test-card-local-target")
|
||||
session = honcho_client.session(id="test-card-local-session")
|
||||
|
||||
# Add messages from both peers
|
||||
session.add_messages([observer.message("Hello"), target.message("Hi there")])
|
||||
|
||||
# Get local peer card with target as Peer object
|
||||
card_response = observer.card(target=target)
|
||||
assert isinstance(card_response, str)
|
||||
|
||||
# Get local peer card with target as string
|
||||
card_response = observer.card(target=target.id)
|
||||
assert isinstance(card_response, str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_card_validation(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
||||
"""
|
||||
Tests peer card validation.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-card-validation-peer")
|
||||
|
||||
# Test with empty string
|
||||
with pytest.raises(ValueError, match="target string cannot be empty"):
|
||||
await peer.card(target="")
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-card-validation-peer")
|
||||
|
||||
# Test with empty string
|
||||
with pytest.raises(ValueError, match="target string cannot be empty"):
|
||||
peer.card(target="")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_chat_streaming(client_fixture: tuple[Honcho | AsyncHoncho, str]):
|
||||
"""
|
||||
Tests streaming chat with mocked response generator.
|
||||
"""
|
||||
|
||||
honcho_client, client_type = client_fixture
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-stream-async-peer")
|
||||
session = await honcho_client.session(id="test-stream-async-session")
|
||||
|
||||
# Add some messages to create context
|
||||
await session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Mock the async streaming response
|
||||
async def mock_aiter_lines():
|
||||
yield 'data: {"delta": {"content": "Hello"}}'
|
||||
yield 'data: {"delta": {"content": " async"}}'
|
||||
yield 'data: {"done": true}'
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.iter_lines = mock_aiter_lines
|
||||
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
|
||||
mock_response.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
# Mock the with_streaming_response.chat call
|
||||
with patch.object(
|
||||
honcho_client.core.workspaces.peers.with_streaming_response,
|
||||
"chat",
|
||||
return_value=mock_response,
|
||||
):
|
||||
result = await peer.chat("Tell me something", stream=True)
|
||||
assert isinstance(result, DialecticStreamResponse)
|
||||
|
||||
# Collect chunks
|
||||
chunks: list[str] = []
|
||||
async for chunk in result:
|
||||
assert isinstance(chunk, str)
|
||||
chunks.append(chunk)
|
||||
assert chunks == ["Hello", " async"]
|
||||
assert result.get_final_response()["content"] == "Hello async"
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-stream-peer")
|
||||
session = honcho_client.session(id="test-stream-session")
|
||||
|
||||
# Add some messages to create context
|
||||
session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Mock the streaming response
|
||||
def mock_iter_lines():
|
||||
yield 'data: {"delta": {"content": "Hello"}}'
|
||||
yield 'data: {"delta": {"content": " world"}}'
|
||||
yield 'data: {"done": true}'
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.iter_lines = mock_iter_lines
|
||||
mock_response.__enter__ = Mock(return_value=mock_response)
|
||||
mock_response.__exit__ = Mock(return_value=None)
|
||||
|
||||
# Mock the with_streaming_response.chat call
|
||||
with patch.object(
|
||||
honcho_client.core.workspaces.peers.with_streaming_response,
|
||||
"chat",
|
||||
return_value=mock_response,
|
||||
):
|
||||
result = peer.chat("Tell me something", stream=True)
|
||||
assert isinstance(result, DialecticStreamResponse)
|
||||
|
||||
# Collect chunks
|
||||
chunks = list(result)
|
||||
assert chunks == ["Hello", " world"]
|
||||
assert result.get_final_response()["content"] == "Hello world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_chat_non_streaming(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests non-streaming chat (already using core SDK).
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
peer = await honcho_client.peer(id="test-non-stream-peer")
|
||||
session = await honcho_client.session(id="test-non-stream-session")
|
||||
|
||||
# Add some messages
|
||||
await session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Non-streaming chat
|
||||
response = await peer.chat("What do I like?", stream=False)
|
||||
# Response can be None or a string
|
||||
assert response is None or isinstance(response, str)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
peer = honcho_client.peer(id="test-non-stream-peer")
|
||||
session = honcho_client.session(id="test-non-stream-session")
|
||||
|
||||
# Add some messages
|
||||
session.add_messages([peer.message("I like pizza")])
|
||||
|
||||
# Non-streaming chat
|
||||
response = peer.chat("What do I like?", stream=False)
|
||||
# Response can be None or a string
|
||||
assert response is None or isinstance(response, str)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from honcho_core.types import DeriverStatus
|
||||
|
||||
from sdks.python.src.honcho.async_client.client import AsyncHoncho
|
||||
from sdks.python.src.honcho.async_client.peer import AsyncPeer
|
||||
|
|
@ -297,3 +300,161 @@ async def test_session_working_rep(client_fixture: tuple[Honcho | AsyncHoncho, s
|
|||
assert isinstance(peer, Peer)
|
||||
session.add_messages([peer.message("test message for working rep")])
|
||||
_working_rep = session.working_rep(peer)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_delete(client_fixture: tuple[Honcho | AsyncHoncho, str]) -> None:
|
||||
"""
|
||||
Tests deleting a session.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-delete")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
# Add a peer to make the session exist
|
||||
user = await honcho_client.peer(id="user-delete")
|
||||
await session.add_peers([user])
|
||||
|
||||
# Delete should not raise an exception
|
||||
await session.delete()
|
||||
|
||||
all_sessions_page = await honcho_client.get_sessions({"is_active": True})
|
||||
all_sessions = all_sessions_page.items
|
||||
all_session_ids = [s.id for s in all_sessions]
|
||||
|
||||
assert "test-session-delete" not in all_session_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_get_deriver_status(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests getting deriver status with various parameter combinations for sessions.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-deriver-status")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
status = await session.get_deriver_status()
|
||||
assert isinstance(status, DeriverStatus)
|
||||
assert hasattr(status, "total_work_units")
|
||||
assert hasattr(status, "completed_work_units")
|
||||
assert hasattr(status, "in_progress_work_units")
|
||||
assert hasattr(status, "pending_work_units")
|
||||
assert status.sessions is None
|
||||
|
||||
# Test with observer_id only
|
||||
peer = await honcho_client.peer(id="test-peer-session-deriver")
|
||||
await peer.get_metadata() # Create the peer
|
||||
status = await session.get_deriver_status(observer_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with sender_id only
|
||||
status = await session.get_deriver_status(sender_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both observer_id and sender_id
|
||||
status = await session.get_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-deriver-status")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
# Test with no parameters
|
||||
status = session.get_deriver_status()
|
||||
assert isinstance(status, DeriverStatus)
|
||||
assert hasattr(status, "total_work_units")
|
||||
assert hasattr(status, "completed_work_units")
|
||||
assert hasattr(status, "in_progress_work_units")
|
||||
assert hasattr(status, "pending_work_units")
|
||||
assert status.sessions is None
|
||||
|
||||
# Test with observer_id only
|
||||
peer = honcho_client.peer(id="test-peer-session-deriver")
|
||||
peer.get_metadata() # Create the peer
|
||||
status = session.get_deriver_status(observer_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with sender_id only
|
||||
status = session.get_deriver_status(sender_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
# Test with both observer_id and sender_id
|
||||
status = session.get_deriver_status(observer_id=peer.id, sender_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_poll_deriver_status(
|
||||
client_fixture: tuple[Honcho | AsyncHoncho, str],
|
||||
):
|
||||
"""
|
||||
Tests polling deriver status until completion for sessions.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
|
||||
# Mock the get_deriver_status method to return a "completed" status
|
||||
# to avoid infinite polling in tests
|
||||
completed_status = DeriverStatus(
|
||||
total_work_units=0,
|
||||
completed_work_units=0,
|
||||
in_progress_work_units=0,
|
||||
pending_work_units=0,
|
||||
)
|
||||
|
||||
if client_type == "async":
|
||||
assert isinstance(honcho_client, AsyncHoncho)
|
||||
session = await honcho_client.session(id="test-session-poll-deriver")
|
||||
assert isinstance(session, AsyncSession)
|
||||
|
||||
with patch.object(
|
||||
session.__class__,
|
||||
"get_deriver_status",
|
||||
new=AsyncMock(return_value=completed_status),
|
||||
):
|
||||
status = await session.poll_deriver_status()
|
||||
assert isinstance(status, DeriverStatus)
|
||||
assert status.pending_work_units == 0
|
||||
assert status.in_progress_work_units == 0
|
||||
|
||||
# Test with parameters
|
||||
peer = await honcho_client.peer(id="test-peer-session-poll")
|
||||
with patch.object(
|
||||
session.__class__,
|
||||
"get_deriver_status",
|
||||
new=AsyncMock(return_value=completed_status),
|
||||
):
|
||||
status = await session.poll_deriver_status(
|
||||
observer_id=peer.id, sender_id=peer.id
|
||||
)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
else:
|
||||
assert isinstance(honcho_client, Honcho)
|
||||
session = honcho_client.session(id="test-session-poll-deriver")
|
||||
assert isinstance(session, Session)
|
||||
|
||||
with patch.object(
|
||||
session.__class__, "get_deriver_status", return_value=completed_status
|
||||
):
|
||||
status = session.poll_deriver_status()
|
||||
assert isinstance(status, DeriverStatus)
|
||||
assert status.pending_work_units == 0
|
||||
assert status.in_progress_work_units == 0
|
||||
|
||||
# Test with parameters
|
||||
peer = honcho_client.peer(id="test-peer-session-poll")
|
||||
with patch.object(
|
||||
session.__class__, "get_deriver_status", return_value=completed_status
|
||||
):
|
||||
status = session.poll_deriver_status(observer_id=peer.id, sender_id=peer.id)
|
||||
assert isinstance(status, DeriverStatus)
|
||||
|
|
|
|||
16
uv.lock
16
uv.lock
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
|
|
@ -673,7 +673,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho"
|
||||
version = "2.3.2"
|
||||
version = "2.3.3"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
|
|
@ -760,12 +760,13 @@ dev = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = { editable = "sdks/python" }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
|
|
@ -775,9 +776,10 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.4.0" },
|
||||
{ name = "honcho-core", specifier = ">=1.4.1a0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
|
@ -785,7 +787,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
|||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1a0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -795,9 +797,9 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/fbf3ca017ffcdaef92e26fe84b77268c1c0b26f95e282d8684f50932d81a/honcho_core-1.4.1a0.tar.gz", hash = "sha256:e0023f46c65500d727e3fb1275781ae1e0b162c226a01352afd45c421c5f350b", size = 130826, upload-time = "2025-10-01T03:23:27.143Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/5f/2aa746b7fd92690ef782dcacc56a5c47a19a999b9729597c04c7cc509e52/honcho_core-1.4.1a0-py3-none-any.whl", hash = "sha256:8b4c86e3ac3964ff56fe6a449b9fa23be49530609c34bdf35c9ecde7cd871bba", size = 121704, upload-time = "2025-10-01T03:23:25.66Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Reference in New Issue