ts SDK fix: adding strict checking (#421)
* fix: adding strict checking and updating readme * chore: changelog and version * fix: Add strict validation to all Python and TypeScript classes * fix: Address Code Rabit Comments * fix: duplicate searchQuery param in typescript session.context() * feat: add created_at, is_active fields, and get_message method * feat: Add pagintion params to sdk * fix: Remove lazy initalization behavior from sdks * fix: Address File Upload Validation, add compatibility shims, address review comments * chore: Docs updates * fix: Convert session config from API format in Peer.sessions() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Pass all args to Session constructor in Peer.sessions() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Preserve createdAt in Peer.refresh(), pass all data in session.peers() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: changelog for ts * fix: Review Comments * fix: Add createAt and to peers call --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9ee9d9ddb
commit
a5423b52e8
|
|
@ -10,14 +10,14 @@ This guide helps you match the right SDK version to your Honcho API version. New
|
|||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Latest:** v2.0.1
|
||||
**Latest:** v2.1.0
|
||||
|
||||
```bash
|
||||
npm install @honcho-ai/sdk
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Latest:** v2.0.1
|
||||
**Latest:** v2.1.0
|
||||
|
||||
```bash
|
||||
pip install honcho-ai
|
||||
|
|
@ -30,7 +30,7 @@ This guide helps you match the right SDK version to your Honcho API version. New
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v3.0.3 (Current) | v2.0.1 | v2.0.1 |
|
||||
| v3.0.3 (Current) | v2.1.0 | v2.1.0 |
|
||||
| v3.0.2 | v2.0.0+ | v2.0.0+ |
|
||||
| v3.0.1 | v2.0.0+ | v2.0.0+ |
|
||||
| v3.0.0 | v2.0.0+ | v2.0.0+ |
|
||||
|
|
|
|||
|
|
@ -524,7 +524,30 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="Python SDK">
|
||||
[Python SDK](https://pypi.org/project/honcho-ai/)
|
||||
<Update label="v2.0.1 (Current)">
|
||||
<Update label="v2.1.0 (Current)">
|
||||
### Added
|
||||
|
||||
- `created_at` property on `Peer` and `Session` objects
|
||||
- `is_active` property on `Session` objects
|
||||
- `get_message(message_id)` method on `Session` (sync and async) to fetch a single message by ID
|
||||
- `page`, `size`, and `reverse` pagination parameters on all list methods
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call — no more lazy initialization
|
||||
- Response configuration models now tolerate unknown fields from newer servers for forward compatibility
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sync and async `Session.get_metadata()`, `get_configuration()`, and `refresh()` now refresh cached `created_at` and `is_active` values along with metadata and configuration
|
||||
- `honcho.__version__` now derives from package metadata, with a source-checkout fallback, so it stays aligned with released package versions
|
||||
</Update>
|
||||
<Update label="v2.0.2">
|
||||
### Changed
|
||||
|
||||
- All input models now reject unknown fields via strict Pydantic validation (`extra="forbid"`). Previously, misspelled or extraneous fields were silently ignored. Now a `ValidationError` is raised with the unrecognized field name.
|
||||
</Update>
|
||||
<Update label="v2.0.1">
|
||||
### Added
|
||||
|
||||
- `set_peer_card` method
|
||||
|
|
@ -638,7 +661,54 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="TypeScript SDK">
|
||||
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
|
||||
<Update label="v2.0.1 (Current)">
|
||||
<Update label="v2.1.0 (Current)">
|
||||
### Added
|
||||
|
||||
- `createdAt` property on `Peer` and `Session` wrapper objects
|
||||
- `isActive` property on `Session` wrapper objects
|
||||
- `getMessage(messageId)` method on `Session` to fetch a single message by ID
|
||||
- `Peer.representation()`, `Session.representation()`, and `Session.context()` now accept `Message` objects for `searchQuery`
|
||||
- `page`, `size`, and `reverse` pagination controls on all list methods
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking**: `searchQuery` removed from top-level `context()` options — use `representationOptions.searchQuery` instead:
|
||||
```typescript
|
||||
// Before (v2.0.x)
|
||||
await session.context({ searchQuery: "..." });
|
||||
// After (v2.1.0)
|
||||
await session.context({ representationOptions: { searchQuery: "..." } });
|
||||
```
|
||||
- List methods (`peers()`, `sessions()`, `messages()`, `workspaces()`) support both the new options object and the legacy raw-filter form
|
||||
- Representation search options now accept strings and content-like objects, including `Message` instances, while rejecting whitespace-only or invalid runtime inputs
|
||||
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call — no more lazy initialization. If you relied on constructing SDK objects without triggering a network request, note that every `peer()` and `session()` call now hits the API:
|
||||
```typescript
|
||||
// Before (v2.0.x) — no API call
|
||||
const session = honcho.session("my-session");
|
||||
// After (v2.1.0) — makes a get-or-create API call
|
||||
const session = await honcho.session("my-session");
|
||||
```
|
||||
- Response configuration models now tolerate unknown fields from newer servers for forward compatibility
|
||||
- Moved `@types/node` from `dependencies` to `devDependencies`
|
||||
|
||||
### Fixed
|
||||
|
||||
- `uploadFile()` now rejects unsupported top-level binary/object inputs and only validates inputs the serializer can actually upload
|
||||
- `uploadFile()` now serializes message configuration using API field names, matching `addMessages()`
|
||||
- Session fetch methods now refresh cached `createdAt` and `isActive` values alongside metadata and configuration
|
||||
</Update>
|
||||
<Update label="v2.0.2">
|
||||
### Changed
|
||||
|
||||
- Client constructor now rejects unknown options via `.strict()` Zod validation. Previously, misspelled options (e.g., `baseUrl` instead of `baseURL`) were silently ignored, causing the SDK to fall back to defaults. Now a `ZodError` is thrown with the unrecognized key name.
|
||||
- All input schemas now use `.strict()` validation to reject unknown fields.
|
||||
- `FileUploadSchema.configuration` now uses `MessageConfigurationSchema` instead of open record type.
|
||||
|
||||
### Fixed
|
||||
|
||||
- README example used `baseUrl` instead of `baseURL`.
|
||||
</Update>
|
||||
<Update label="v2.0.1">
|
||||
### Added
|
||||
|
||||
- `setPeerCard` method
|
||||
|
|
|
|||
|
|
@ -353,6 +353,20 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Observation and Peer Join Order
|
||||
|
||||
Reasoning tasks are scheduled at the time a message is created, based on which peers are in the session **at that moment**. Honcho does not retroactively schedule reasoning for peers that join later.
|
||||
|
||||
This means:
|
||||
|
||||
- If Peer C joins a session **after** messages from Peer A and Peer B have already been sent, Peer C will **not** receive reasoning tasks for those earlier messages—even if Peer C has `observe_others` enabled.
|
||||
- Peer C will only begin observing new messages sent after they join the session.
|
||||
- Similarly, if a peer leaves a session, they stop being included as an observer for any messages sent after their departure.
|
||||
|
||||
<Warning>
|
||||
There is no retroactive reasoning. If your application needs an observer peer to reason about prior conversation history, add the peer to the session **before** messages are sent. Alternatively use the .chat() endpoint to include the conversation history in the agent's context, regardless of if they were reasoned against or not
|
||||
</Warning>
|
||||
|
||||
## Full Configuration Schema Reference
|
||||
|
||||
### Workspace & Session Configuration
|
||||
|
|
|
|||
|
|
@ -266,6 +266,20 @@ Directional representations update automatically through the reasoning pipeline
|
|||
|
||||
The pipeline respects scoping—Honcho's representations reason over messages across all sessions, while directional representations only reason over messages from sessions where the observer was an active participant.
|
||||
|
||||
### Peer Join Order Matters
|
||||
|
||||
Reasoning tasks are scheduled at the time a message is created, based on which peers are in the session **at that moment**. Honcho does not retroactively schedule reasoning for peers that join later.
|
||||
|
||||
This means:
|
||||
|
||||
- If Peer C joins a session **after** messages from Peer A and Peer B have already been sent, Peer C will **not** receive reasoning tasks for those earlier messages—even if Peer C has `observe_others=true`.
|
||||
- Peer C will only begin observing new messages sent after they join the session.
|
||||
- Similarly, if a peer leaves a session, they stop being included as an observer for any messages sent after their departure.
|
||||
|
||||
<Warning>
|
||||
There is no retroactive reasoning. If your application needs an observer peer to reason about prior conversation history, add the peer to the session **before** messages are sent. Alternatively, use `peer.chat()` to include conversation history in the agent's context whether or not those messages were previously reasoned over.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Conclusions are cached for fast retrieval. Use `representation()` to retrieve stored conclusions for dashboards and analytics. Use `peer.chat()` when you need query-specific reasoning with natural language.
|
||||
</Note>
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@ context = session.context(
|
|||
const context = await session.context({
|
||||
tokens: 2000,
|
||||
peerTarget: "user-123",
|
||||
searchQuery: "What are my coding preferences?",
|
||||
representationOptions: {
|
||||
searchQuery: "What are my coding preferences?",
|
||||
searchTopK: 10, // Number of relevant conclusions to fetch
|
||||
searchMaxDistance: 0.8, // Max semantic distance (0.0-1.0)
|
||||
includeMostFrequent: true, // Include most frequent conclusions
|
||||
|
|
|
|||
|
|
@ -218,6 +218,14 @@ const session = await honcho.session(id);
|
|||
// List all peers in workspace (returns Page<Peer>)
|
||||
const peers = await honcho.peers();
|
||||
|
||||
// List with pagination and filtering
|
||||
const filtered = await honcho.peers({
|
||||
filters: { metadata: { role: "user" } },
|
||||
page: 1,
|
||||
size: 25,
|
||||
reverse: true
|
||||
});
|
||||
|
||||
// List all sessions in workspace (returns Page<Session>)
|
||||
const sessions = await honcho.sessions();
|
||||
|
||||
|
|
@ -234,7 +242,7 @@ const workspaces = await honcho.workspaces();
|
|||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
Peer and session creation is **lazy** - no API calls are made until you actually use the peer or session.
|
||||
`peer()` and `session()` always make a get-or-create API call, returning objects with cached metadata, configuration, and timestamps.
|
||||
</Info>
|
||||
|
||||
### Peer
|
||||
|
|
@ -243,7 +251,7 @@ Represents an entity that can participate in conversations:
|
|||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Create peers (lazy creation - no API call yet)
|
||||
# Create peers (get-or-create API call)
|
||||
alice = honcho.peer("alice")
|
||||
assistant = honcho.peer("assistant")
|
||||
|
||||
|
|
@ -254,6 +262,7 @@ alice = honcho.peer("bob", config={"role": "user", "active": True}, metadata={"l
|
|||
# Peer properties
|
||||
print(f"Peer ID: {alice.id}")
|
||||
print(f"Workspace: {alice.workspace_id}")
|
||||
print(f"Created: {alice.created_at}") # Available after API fetch
|
||||
|
||||
# Chat with peer's representations (supports streaming)
|
||||
response = alice.chat("What did I have for breakfast?")
|
||||
|
|
@ -305,6 +314,7 @@ const assistant = await honcho.peer("assistant");
|
|||
|
||||
// Peer properties
|
||||
console.log(`Peer ID: ${alice.id}`);
|
||||
console.log(`Created: ${alice.createdAt}`); // Available after API fetch
|
||||
|
||||
// Chat with peer's representations (supports streaming)
|
||||
const response = await alice.chat("What did I have for breakfast?");
|
||||
|
|
@ -551,7 +561,7 @@ Manages multi-party conversations:
|
|||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Create session (like peers, lazy creation)
|
||||
# Create session (get-or-create API call)
|
||||
session = honcho.session("conversation-1")
|
||||
|
||||
# Create with immediate configuration
|
||||
|
|
@ -561,6 +571,8 @@ session = honcho.session("meeting-1", config={"type": "meeting", "max_peers": 10
|
|||
# Session properties
|
||||
print(f"Session ID: {session.id}")
|
||||
print(f"Workspace: {session.workspace_id}")
|
||||
print(f"Created: {session.created_at}") # Available after API fetch
|
||||
print(f"Active: {session.is_active}") # Available after API fetch
|
||||
|
||||
# Peer management
|
||||
session.add_peers([alice, assistant])
|
||||
|
|
@ -579,8 +591,12 @@ session.add_messages([
|
|||
assistant.message("Hi Alice! How can I help today?")
|
||||
])
|
||||
|
||||
# Get messages
|
||||
# Get messages (with optional pagination)
|
||||
messages = session.messages()
|
||||
messages = session.messages(page=1, size=100, reverse=True)
|
||||
|
||||
# Get a single message by ID
|
||||
message = session.get_message("message-id")
|
||||
|
||||
# Get conversation context
|
||||
context = session.context(summary=True, tokens=2000)
|
||||
|
|
@ -641,6 +657,8 @@ const session = await honcho.session("conversation-1");
|
|||
|
||||
// Session properties
|
||||
console.log(`Session ID: ${session.id}`);
|
||||
console.log(`Created: ${session.createdAt}`); // Available after API fetch
|
||||
console.log(`Active: ${session.isActive}`); // Available after API fetch
|
||||
|
||||
// Peer management
|
||||
await session.addPeers([alice, assistant]);
|
||||
|
|
@ -658,8 +676,12 @@ await session.addMessages([
|
|||
assistant.message("Hi Alice! How can I help today?")
|
||||
]);
|
||||
|
||||
// Get messages
|
||||
// Get messages (with optional pagination)
|
||||
const messages = await session.messages();
|
||||
const paged = await session.messages({ page: 1, size: 100, reverse: true });
|
||||
|
||||
// Get a single message by ID
|
||||
const message = await session.getMessage("message-id");
|
||||
|
||||
// Get conversation context
|
||||
const context = await session.context({ summary: true, tokens: 2000 });
|
||||
|
|
@ -668,10 +690,10 @@ const context = await session.context({ summary: true, tokens: 2000 });
|
|||
const richContext = await session.context({
|
||||
tokens: 2000,
|
||||
peerTarget: "user",
|
||||
searchQuery: "What are my preferences?",
|
||||
peerPerspective: "assistant",
|
||||
limitToSession: true,
|
||||
representationOptions: {
|
||||
searchQuery: "What are my preferences?",
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.8,
|
||||
includeMostFrequent: true,
|
||||
|
|
@ -811,12 +833,12 @@ The SessionContext object has the following structure:
|
|||
| `tokens` | `int` | Maximum tokens to include |
|
||||
| `peer_target` | `str` | Peer ID to get representation for |
|
||||
| `peer_perspective` | `str` | Peer ID for perspective (requires peer_target) |
|
||||
| `search_query` | `str` or `Message` | Query string or Message object for semantic search |
|
||||
| `limit_to_session` | `bool` | Limit representation to session only |
|
||||
| `search_top_k` | `int` | Number of semantic search results (1-100) |
|
||||
| `search_max_distance` | `float` | Max semantic distance (0.0-1.0) |
|
||||
| `include_most_frequent` | `bool` | Include most frequent conclusions |
|
||||
| `max_conclusions` | `int` | Max conclusions to include (1-100) |
|
||||
| `representationOptions.searchQuery` | `str` or `Message` | Query string or Message object for semantic search |
|
||||
| `representationOptions.searchTopK` | `int` | Number of semantic search results (1-100) |
|
||||
| `representationOptions.searchMaxDistance` | `float` | Max semantic distance (0.0-1.0) |
|
||||
| `representationOptions.includeMostFrequent` | `bool` | Include most frequent conclusions |
|
||||
| `representationOptions.maxConclusions` | `int` | Max conclusions to include (1-100) |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
|
|
@ -988,23 +1010,46 @@ const actionItems = await session.messages({
|
|||
|
||||
### Pagination
|
||||
|
||||
All list methods support `page`, `size`, and `reverse` parameters:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Iterate through all sessions
|
||||
# Default pagination (page 1, size 50)
|
||||
for session in honcho.sessions():
|
||||
print(f"Session: {session.id}")
|
||||
|
||||
# Iterate through session messages
|
||||
for message in session.messages():
|
||||
print(f" {message.peer_id}: {message.content}")
|
||||
# Custom page size
|
||||
for message in session.messages(size=100):
|
||||
print(f" {message.peer_id}: {message.content}")
|
||||
|
||||
# Start at a specific page
|
||||
page3 = session.messages(page=3, size=25)
|
||||
|
||||
# Reverse ordering
|
||||
recent_first = session.messages(reverse=True)
|
||||
|
||||
# Combine with filters
|
||||
filtered = session.messages(filters={"peer_id": "alice"}, size=10)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get paginated results
|
||||
// Default pagination (page 1, size 50)
|
||||
const peersPage = await honcho.peers();
|
||||
|
||||
// Iterate through all items
|
||||
for await (const peer of peersPage) {
|
||||
// Custom page size and filtering
|
||||
const filtered = await honcho.peers({
|
||||
filters: { metadata: { role: "user" } },
|
||||
size: 25
|
||||
});
|
||||
|
||||
// Start at a specific page
|
||||
const page3 = await session.messages({ page: 3, size: 25 });
|
||||
|
||||
// Reverse ordering
|
||||
const recent = await session.messages({ reverse: true });
|
||||
|
||||
// Iterate through all items (auto-paginates)
|
||||
for await (const peer of await honcho.peers()) {
|
||||
console.log(`Peer: ${peer.id}`);
|
||||
}
|
||||
|
||||
|
|
@ -1048,8 +1093,8 @@ const supportAgent = await honcho.peer(`agent-${agentId}`);
|
|||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Lazy creation - no API calls until needed
|
||||
peers = [honcho.peer(f"user-{i}") for i in range(100)] # Fast
|
||||
# Create peers (each makes a get-or-create call)
|
||||
peers = [honcho.peer(f"user-{i}") for i in range(100)]
|
||||
|
||||
# Batch operations when possible
|
||||
session.add_messages([peer.message(f"Message {i}") for i, peer in enumerate(peers)])
|
||||
|
|
@ -1059,7 +1104,7 @@ context = session.context(tokens=1500) # Limit context size
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Lazy creation - no API calls until needed
|
||||
// Create peers (each makes a get-or-create call)
|
||||
const peers = await Promise.all(
|
||||
Array.from({ length: 100 }, (_, i) => honcho.peer(`user-${i}`))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,31 @@ 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.1.0] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- `created_at` property on `Peer` and `Session` objects
|
||||
- `is_active` property on `Session` objects
|
||||
- `get_message(message_id)` method on `Session` (sync and async) to fetch a single message by ID
|
||||
- `page`, `size`, and `reverse` pagination parameters on all list methods: `peers()`, `sessions()`, `messages()`, and `conclusions.list()`
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call. Previously, calling without metadata/configuration returned a lazy object with no API call. All Peer/Session objects now have `created_at` populated immediately.
|
||||
- Response configuration models (`WorkspaceConfigurationResponse`, `SessionConfigurationResponse`) now tolerate unknown fields from newer servers for forward compatibility
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sync and async `Session.get_metadata()`, `get_configuration()`, and `refresh()` now refresh cached `created_at` and `is_active` values along with metadata and configuration.
|
||||
- `honcho.__version__` now derives from package metadata, with a `pyproject.toml` fallback in source checkouts, so it stays aligned with SDK releases.
|
||||
|
||||
## [2.0.2] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
|
||||
- All input models now reject unknown fields via `extra="forbid"` Pydantic validation. Previously, misspelled or extraneous fields were silently ignored. Now a `ValidationError` is raised with the unrecognized field name.
|
||||
|
||||
## [2.0.1] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
description = "Official DX Optimized Python SDK for Honcho"
|
||||
dynamic = ["readme"]
|
||||
license = "Apache-2.0"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ Usage:
|
|||
print(p.id)
|
||||
"""
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from .aio import ConclusionScopeAio, HonchoAio, PeerAio, SessionAio
|
||||
from .api_types import MessageCreateParams
|
||||
from .base import PeerBase, SessionBase
|
||||
|
|
@ -66,7 +70,23 @@ from .types import (
|
|||
DialecticStreamResponse,
|
||||
)
|
||||
|
||||
__version__ = "2.0.1"
|
||||
|
||||
def _detect_version() -> str:
|
||||
try:
|
||||
return version("honcho-ai")
|
||||
except PackageNotFoundError:
|
||||
try:
|
||||
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
||||
pyproject_text = pyproject_path.read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject_text, re.MULTILINE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except OSError:
|
||||
pass
|
||||
return "0.0.0"
|
||||
|
||||
|
||||
__version__ = _detect_version()
|
||||
__author__ = "Plastic Labs"
|
||||
__email__ = "hello@plasticlabs.ai"
|
||||
|
||||
|
|
|
|||
|
|
@ -167,42 +167,52 @@ class HonchoAio(AsyncMetadataConfigMixin):
|
|||
configuration: Optional configuration to set for this peer.
|
||||
|
||||
Returns:
|
||||
A Peer object
|
||||
A Peer object with cached values from the API response.
|
||||
"""
|
||||
if configuration is not None or metadata is not None:
|
||||
await self._honcho._ensure_workspace_async()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
await self._honcho._ensure_workspace_async()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.peers(self._honcho.workspace_id), body=body
|
||||
)
|
||||
peer_data = PeerResponse.model_validate(data)
|
||||
return Peer(
|
||||
id,
|
||||
self._honcho,
|
||||
metadata=peer_data.metadata,
|
||||
configuration=peer_data.configuration,
|
||||
)
|
||||
|
||||
return Peer(id, self._honcho, metadata=metadata, configuration=configuration)
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.peers(self._honcho.workspace_id), body=body
|
||||
)
|
||||
peer_data = PeerResponse.model_validate(data)
|
||||
return Peer(
|
||||
id,
|
||||
self._honcho,
|
||||
metadata=peer_data.metadata,
|
||||
configuration=peer_data.configuration,
|
||||
created_at=peer_data.created_at,
|
||||
)
|
||||
|
||||
async def peers(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> AsyncPage[PeerResponse, Peer]:
|
||||
"""
|
||||
Get all peers in the current workspace asynchronously.
|
||||
|
||||
Returns:
|
||||
An AsyncPage of Peer objects
|
||||
Args:
|
||||
filters: Optional filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
"""
|
||||
await self._honcho._ensure_workspace_async()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.peers_list(self._honcho.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(peer: PeerResponse) -> Peer:
|
||||
|
|
@ -211,13 +221,17 @@ class HonchoAio(AsyncMetadataConfigMixin):
|
|||
self._honcho,
|
||||
metadata=peer.metadata,
|
||||
configuration=peer.configuration,
|
||||
created_at=peer.created_at,
|
||||
)
|
||||
|
||||
async def fetch_next(page: int) -> AsyncPage[PeerResponse, Peer]:
|
||||
async def fetch_next(next_page: int) -> AsyncPage[PeerResponse, Peer]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = await self._honcho._async_http_client.post(
|
||||
routes.peers_list(self._honcho.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return AsyncPage(next_data, PeerResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -239,42 +253,55 @@ class HonchoAio(AsyncMetadataConfigMixin):
|
|||
configuration: Optional configuration to set for this session.
|
||||
|
||||
Returns:
|
||||
A Session object
|
||||
A Session object with cached values from the API response.
|
||||
"""
|
||||
if configuration is not None or metadata is not None:
|
||||
await self._honcho._ensure_workspace_async()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
await self._honcho._ensure_workspace_async()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.sessions(self._honcho.workspace_id), body=body
|
||||
)
|
||||
session_data = SessionResponse.model_validate(data)
|
||||
return Session(
|
||||
id,
|
||||
self._honcho,
|
||||
metadata=session_data.metadata,
|
||||
configuration=session_data.configuration,
|
||||
)
|
||||
|
||||
return Session(id, self._honcho, metadata=metadata, configuration=configuration)
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.sessions(self._honcho.workspace_id), body=body
|
||||
)
|
||||
session_data = SessionResponse.model_validate(data)
|
||||
return Session(
|
||||
id,
|
||||
self._honcho,
|
||||
metadata=session_data.metadata,
|
||||
configuration=SessionConfiguration.model_validate(
|
||||
session_data.configuration.model_dump()
|
||||
),
|
||||
created_at=session_data.created_at,
|
||||
is_active=session_data.is_active,
|
||||
)
|
||||
|
||||
async def sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> AsyncPage[SessionResponse, Session]:
|
||||
"""
|
||||
Get all sessions in the current workspace asynchronously.
|
||||
|
||||
Returns:
|
||||
An AsyncPage of Session objects
|
||||
Args:
|
||||
filters: Optional filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
"""
|
||||
await self._honcho._ensure_workspace_async()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = await self._honcho._async_http_client.post(
|
||||
routes.sessions_list(self._honcho.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(session: SessionResponse) -> Session:
|
||||
|
|
@ -283,13 +310,18 @@ class HonchoAio(AsyncMetadataConfigMixin):
|
|||
self._honcho,
|
||||
metadata=session.metadata,
|
||||
configuration=session.configuration,
|
||||
created_at=session.created_at,
|
||||
is_active=session.is_active,
|
||||
)
|
||||
|
||||
async def fetch_next(page: int) -> AsyncPage[SessionResponse, Session]:
|
||||
async def fetch_next(next_page: int) -> AsyncPage[SessionResponse, Session]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = await self._honcho._async_http_client.post(
|
||||
routes.sessions_list(self._honcho.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return AsyncPage(next_data, SessionResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -457,6 +489,21 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
return {}
|
||||
return self._peer._configuration.model_dump(exclude_none=True)
|
||||
|
||||
def _apply_peer_response(self, peer: PeerResponse) -> None:
|
||||
self._peer._metadata = peer.metadata or {}
|
||||
self._peer._configuration = peer.configuration
|
||||
self._peer._created_at = peer.created_at
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
"""Get metadata from the server asynchronously."""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
data = await self._get_async_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._apply_peer_response(peer)
|
||||
return self._peer._metadata or {}
|
||||
|
||||
async def get_configuration(self) -> PeerConfig: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""Get configuration from the server asynchronously."""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
|
|
@ -464,9 +511,17 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._peer._metadata = peer.metadata or {}
|
||||
self._peer._configuration = peer.configuration
|
||||
return self._peer._configuration
|
||||
self._apply_peer_response(peer)
|
||||
return self._peer._configuration or PeerConfig()
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""Refresh cached metadata, configuration, and created_at asynchronously."""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
data = await self._get_async_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._apply_peer_response(peer)
|
||||
|
||||
async def set_configuration(self, configuration: PeerConfig) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""Set configuration on the server asynchronously."""
|
||||
|
|
@ -544,23 +599,44 @@ class PeerAio(AsyncMetadataConfigMixin):
|
|||
return AsyncDialecticStreamResponse(stream_response())
|
||||
|
||||
async def sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> AsyncPage[SessionResponse, Session]:
|
||||
"""Get all sessions this peer is a member of asynchronously."""
|
||||
await self._peer._honcho._ensure_workspace_async()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = await self._peer._honcho._async_http_client.post(
|
||||
routes.peer_sessions_list(self._peer.workspace_id, self._peer.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(session: SessionResponse) -> Session:
|
||||
return Session(session.id, self._peer._honcho)
|
||||
return Session(
|
||||
session.id,
|
||||
self._peer._honcho,
|
||||
metadata=session.metadata,
|
||||
configuration=SessionConfiguration.model_validate(
|
||||
session.configuration.model_dump()
|
||||
),
|
||||
created_at=session.created_at,
|
||||
is_active=session.is_active,
|
||||
)
|
||||
|
||||
async def fetch_next(page: int) -> AsyncPage[SessionResponse, Session]:
|
||||
async def fetch_next(next_page: int) -> AsyncPage[SessionResponse, Session]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = await self._peer._honcho._async_http_client.post(
|
||||
routes.peer_sessions_list(self._peer.workspace_id, self._peer.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return AsyncPage(next_data, SessionResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -777,6 +853,24 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
return {}
|
||||
return self._session._configuration.model_dump(exclude_none=True)
|
||||
|
||||
def _apply_session_response(self, session: SessionResponse) -> None:
|
||||
self._session._metadata = session.metadata or {}
|
||||
self._session._configuration = SessionConfiguration.model_validate(
|
||||
session.configuration.model_dump()
|
||||
)
|
||||
self._session._created_at = session.created_at
|
||||
self._session._is_active = session.is_active
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
"""Get metadata from the server asynchronously."""
|
||||
await self._session._honcho._ensure_workspace_async()
|
||||
data = await self._get_async_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._apply_session_response(session)
|
||||
return self._get_metadata()
|
||||
|
||||
async def get_configuration(self) -> SessionConfiguration: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""Get configuration from the server asynchronously."""
|
||||
await self._session._honcho._ensure_workspace_async()
|
||||
|
|
@ -784,9 +878,17 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._session._metadata = session.metadata or {}
|
||||
self._session._configuration = session.configuration
|
||||
return self._session._configuration
|
||||
self._apply_session_response(session)
|
||||
return self._session._configuration or SessionConfiguration()
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""Refresh cached metadata, configuration, and session status asynchronously."""
|
||||
await self._session._honcho._ensure_workspace_async()
|
||||
data = await self._get_async_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._apply_session_response(session)
|
||||
|
||||
async def set_configuration(self, configuration: SessionConfiguration) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""Set configuration on the server asynchronously."""
|
||||
|
|
@ -921,22 +1023,32 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
self,
|
||||
*,
|
||||
filters: dict[str, object] | None = None,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> AsyncPage[MessageResponse, Message]:
|
||||
"""Get messages from this session asynchronously."""
|
||||
await self._session._honcho._ensure_workspace_async()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = await self._session._honcho._async_http_client.post(
|
||||
routes.messages_list(self._session.workspace_id, self._session.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(response: MessageResponse) -> Message:
|
||||
return Message.from_api_response(response)
|
||||
|
||||
async def fetch_next(page: int) -> AsyncPage[MessageResponse, Message]:
|
||||
async def fetch_next(next_page: int) -> AsyncPage[MessageResponse, Message]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = await self._session._honcho._async_http_client.post(
|
||||
routes.messages_list(self._session.workspace_id, self._session.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return AsyncPage(next_data, MessageResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -966,6 +1078,8 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
self._session._honcho,
|
||||
metadata=cloned.metadata,
|
||||
configuration=cloned.configuration,
|
||||
created_at=cloned.created_at,
|
||||
is_active=cloned.is_active,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
|
|
@ -1248,6 +1362,21 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
)
|
||||
return QueueStatusResponse.model_validate(data)
|
||||
|
||||
async def get_message(self, message_id: str) -> Message:
|
||||
"""Get a single message by ID from this session asynchronously.
|
||||
|
||||
Args:
|
||||
message_id: The ID of the message to retrieve
|
||||
|
||||
Returns:
|
||||
The Message object
|
||||
"""
|
||||
await self._session._honcho._ensure_workspace_async()
|
||||
data = await self._session._honcho._async_http_client.get(
|
||||
routes.message(self._session.workspace_id, self._session.id, message_id)
|
||||
)
|
||||
return Message.from_api_response(MessageResponse.model_validate(data))
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def update_message(
|
||||
self,
|
||||
|
|
@ -1288,6 +1417,8 @@ class ConclusionScopeAio:
|
|||
page: int = 1,
|
||||
size: int = 50,
|
||||
session: str | SessionBase | None = None,
|
||||
*,
|
||||
reverse: bool = False,
|
||||
) -> AsyncPage[ConclusionResponse, Conclusion]:
|
||||
"""List conclusions in this scope asynchronously."""
|
||||
await self._scope._honcho._ensure_workspace_async()
|
||||
|
|
@ -1299,22 +1430,28 @@ class ConclusionScopeAio:
|
|||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_session_id
|
||||
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = await self._scope._honcho._async_http_client.post(
|
||||
routes.conclusions_list(self._scope.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": page, "size": size},
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(response: ConclusionResponse) -> Conclusion:
|
||||
return Conclusion.from_api_response(response)
|
||||
|
||||
async def fetch_next(
|
||||
page: int,
|
||||
next_page: int,
|
||||
) -> AsyncPage[ConclusionResponse, Conclusion]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = await self._scope._honcho._async_http_client.post(
|
||||
routes.conclusions_list(self._scope.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": page, "size": size},
|
||||
query=next_query,
|
||||
)
|
||||
return AsyncPage(next_data, ConclusionResponse, transform, fetch_next)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||
class ReasoningConfiguration(BaseModel):
|
||||
"""Configuration for reasoning functionality."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
enabled: bool | None = None
|
||||
custom_instructions: str | None = None
|
||||
|
||||
|
|
@ -25,6 +27,8 @@ class ReasoningConfiguration(BaseModel):
|
|||
class PeerCardConfiguration(BaseModel):
|
||||
"""Configuration for peer card functionality."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
use: bool | None = None
|
||||
create: bool | None = None
|
||||
|
||||
|
|
@ -32,6 +36,8 @@ class PeerCardConfiguration(BaseModel):
|
|||
class SummaryConfiguration(BaseModel):
|
||||
"""Configuration for summary functionality."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
enabled: bool | None = None
|
||||
messages_per_short_summary: int | None = None
|
||||
messages_per_long_summary: int | None = None
|
||||
|
|
@ -40,13 +46,15 @@ class SummaryConfiguration(BaseModel):
|
|||
class DreamConfiguration(BaseModel):
|
||||
"""Configuration for dream functionality."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
class WorkspaceConfiguration(BaseModel):
|
||||
"""Workspace-level configuration options."""
|
||||
|
||||
model_config = ConfigDict(extra="allow") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
reasoning: ReasoningConfiguration | None = None
|
||||
peer_card: PeerCardConfiguration | None = None
|
||||
|
|
@ -60,9 +68,23 @@ class SessionConfiguration(WorkspaceConfiguration):
|
|||
pass
|
||||
|
||||
|
||||
class WorkspaceConfigurationResponse(WorkspaceConfiguration):
|
||||
"""Workspace configuration for response parsing — tolerates unknown fields from newer servers."""
|
||||
|
||||
model_config = ConfigDict(extra="ignore") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
|
||||
class SessionConfigurationResponse(SessionConfiguration):
|
||||
"""Session configuration for response parsing — tolerates unknown fields from newer servers."""
|
||||
|
||||
model_config = ConfigDict(extra="ignore") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
|
||||
class MessageConfiguration(BaseModel):
|
||||
"""Message-level configuration options."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
reasoning: ReasoningConfiguration | None = None
|
||||
|
||||
|
||||
|
|
@ -74,6 +96,8 @@ class MessageConfiguration(BaseModel):
|
|||
class PeerConfig(BaseModel):
|
||||
"""Configuration for peer-level settings."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
observe_me: bool | None = None
|
||||
"""Whether Honcho will use reasoning to form a representation of this peer."""
|
||||
|
||||
|
|
@ -81,6 +105,8 @@ class PeerConfig(BaseModel):
|
|||
class SessionPeerConfig(BaseModel):
|
||||
"""Configuration for a peer within a session."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
observe_others: bool | None = Field(
|
||||
None,
|
||||
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
|
||||
|
|
@ -103,8 +129,8 @@ class WorkspaceResponse(BaseModel):
|
|||
|
||||
id: str
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
configuration: WorkspaceConfiguration = Field(
|
||||
default_factory=WorkspaceConfiguration
|
||||
configuration: WorkspaceConfigurationResponse = Field(
|
||||
default_factory=WorkspaceConfigurationResponse
|
||||
)
|
||||
created_at: datetime.datetime
|
||||
|
||||
|
|
@ -112,6 +138,8 @@ class WorkspaceResponse(BaseModel):
|
|||
class WorkspaceCreateParams(BaseModel):
|
||||
"""Parameters for creating a workspace."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
id: str = Field(min_length=1, max_length=100)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
configuration: WorkspaceConfiguration = Field(
|
||||
|
|
@ -122,6 +150,8 @@ class WorkspaceCreateParams(BaseModel):
|
|||
class WorkspaceUpdateParams(BaseModel):
|
||||
"""Parameters for updating a workspace."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: WorkspaceConfiguration | None = None
|
||||
|
||||
|
|
@ -129,6 +159,8 @@ class WorkspaceUpdateParams(BaseModel):
|
|||
class WorkspaceListParams(BaseModel):
|
||||
"""Parameters for listing workspaces."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -152,6 +184,8 @@ class PeerResponse(BaseModel):
|
|||
class PeerCreateParams(BaseModel):
|
||||
"""Parameters for creating a peer."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
id: str = Field(min_length=1, max_length=100)
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: PeerConfig | None = None
|
||||
|
|
@ -160,6 +194,8 @@ class PeerCreateParams(BaseModel):
|
|||
class PeerUpdateParams(BaseModel):
|
||||
"""Parameters for updating a peer."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: PeerConfig | None = None
|
||||
|
||||
|
|
@ -167,12 +203,16 @@ class PeerUpdateParams(BaseModel):
|
|||
class PeerListParams(BaseModel):
|
||||
"""Parameters for listing peers."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class PeerRepresentationParams(BaseModel):
|
||||
"""Parameters for getting peer representation."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
session_id: str | None = None
|
||||
target: str | None = None
|
||||
search_query: str | None = None
|
||||
|
|
@ -217,13 +257,17 @@ class SessionResponse(BaseModel):
|
|||
is_active: bool
|
||||
workspace_id: str
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
configuration: SessionConfiguration = Field(default_factory=SessionConfiguration)
|
||||
configuration: SessionConfigurationResponse = Field(
|
||||
default_factory=SessionConfigurationResponse
|
||||
)
|
||||
created_at: datetime.datetime
|
||||
|
||||
|
||||
class SessionCreateParams(BaseModel):
|
||||
"""Parameters for creating a session."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
id: str = Field(min_length=1, max_length=100)
|
||||
metadata: dict[str, Any] | None = None
|
||||
peers: dict[str, SessionPeerConfig] | None = None
|
||||
|
|
@ -233,6 +277,8 @@ class SessionCreateParams(BaseModel):
|
|||
class SessionUpdateParams(BaseModel):
|
||||
"""Parameters for updating a session."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: SessionConfiguration | None = None
|
||||
|
||||
|
|
@ -240,6 +286,8 @@ class SessionUpdateParams(BaseModel):
|
|||
class SessionListParams(BaseModel):
|
||||
"""Parameters for listing sessions."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -308,6 +356,8 @@ class MessageResponse(BaseModel):
|
|||
class MessageCreateParams(BaseModel):
|
||||
"""Parameters for creating a message."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
content: str
|
||||
peer_id: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
|
@ -318,24 +368,32 @@ class MessageCreateParams(BaseModel):
|
|||
class MessageBatchCreateParams(BaseModel):
|
||||
"""Parameters for batch message creation."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
messages: list[MessageCreateParams] = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class MessageUpdateParams(BaseModel):
|
||||
"""Parameters for updating a message."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MessageListParams(BaseModel):
|
||||
"""Parameters for listing messages."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MessageSearchParams(BaseModel):
|
||||
"""Parameters for searching messages."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
query: str
|
||||
filters: dict[str, Any] | None = None
|
||||
limit: int = Field(default=10, ge=1, le=100)
|
||||
|
|
@ -362,6 +420,8 @@ class ConclusionResponse(BaseModel):
|
|||
class ConclusionCreateParams(BaseModel):
|
||||
"""Parameters for creating a conclusion."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
content: str = Field(min_length=1, max_length=65535)
|
||||
observer_id: str
|
||||
observed_id: str
|
||||
|
|
@ -371,18 +431,24 @@ class ConclusionCreateParams(BaseModel):
|
|||
class ConclusionBatchCreateParams(BaseModel):
|
||||
"""Parameters for batch conclusion creation."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
conclusions: list[ConclusionCreateParams] = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class ConclusionListParams(BaseModel):
|
||||
"""Parameters for listing conclusions."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ConclusionQueryParams(BaseModel):
|
||||
"""Parameters for querying conclusions."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
query: str
|
||||
top_k: int = Field(default=10, ge=1, le=100)
|
||||
distance: float | None = Field(default=None, ge=0.0, le=1.0)
|
||||
|
|
@ -425,6 +491,8 @@ ReasoningLevel = Literal["minimal", "low", "medium", "high", "max"]
|
|||
class DialecticParams(BaseModel):
|
||||
"""Parameters for dialectic chat."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
session_id: str | None = None
|
||||
target: str | None = None
|
||||
query: str = Field(min_length=1, max_length=10000)
|
||||
|
|
@ -474,6 +542,8 @@ class PageResponse(BaseModel):
|
|||
class MessageUploadParams(BaseModel):
|
||||
"""Parameters for file upload message creation."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
peer_id: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: MessageConfiguration | None = None
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class Honcho(BaseModel, MetadataConfigMixin): # pyright: ignore[reportUnsafeMul
|
|||
recently fetched. Call get_configuration() for fresh data.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow") # pyright: ignore
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore
|
||||
|
||||
workspace_id: str = Field(
|
||||
...,
|
||||
|
|
@ -305,44 +305,62 @@ class Honcho(BaseModel, MetadataConfigMixin): # pyright: ignore[reportUnsafeMul
|
|||
"""
|
||||
Get or create a peer with the given ID.
|
||||
|
||||
Creates a Peer object that can be used to interact with the specified peer.
|
||||
This method does not make an API call unless `configuration` or `metadata` is
|
||||
provided.
|
||||
Makes an API call to get or create the peer, then returns a Peer object
|
||||
with cached values from the response.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for the peer within the workspace. Should be a
|
||||
stable identifier that can be used consistently across sessions.
|
||||
id: Unique identifier for the peer within the workspace.
|
||||
metadata: Optional metadata dictionary to associate with this peer.
|
||||
If set, will get/create peer immediately with metadata.
|
||||
configuration: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
|
||||
Returns:
|
||||
A Peer object that can be used to send messages, join sessions, and
|
||||
query the peer's knowledge representations
|
||||
|
||||
Raises:
|
||||
ValidationError: If the peer ID is empty or invalid
|
||||
A Peer object with cached metadata, configuration, and created_at.
|
||||
"""
|
||||
return Peer(id, self, configuration=configuration, metadata=metadata)
|
||||
self._ensure_workspace()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = self._http.post(routes.peers(self.workspace_id), body=body)
|
||||
peer_data = PeerResponse.model_validate(data)
|
||||
return Peer(
|
||||
id,
|
||||
self,
|
||||
metadata=peer_data.metadata,
|
||||
configuration=peer_data.configuration,
|
||||
created_at=peer_data.created_at,
|
||||
)
|
||||
|
||||
def peers(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> SyncPage[PeerResponse, Peer]:
|
||||
"""
|
||||
Get all peers in the current workspace.
|
||||
|
||||
Makes an API call to retrieve all peers that have been created or used
|
||||
within the current workspace. Returns a paginated result that transforms
|
||||
inner client Peer objects to SDK Peer objects as they are consumed.
|
||||
Args:
|
||||
filters: Optional filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
|
||||
Returns:
|
||||
A SyncPage of Peer objects representing all peers in the workspace
|
||||
"""
|
||||
self._ensure_workspace()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = self._http.post(
|
||||
routes.peers_list(self.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(peer: PeerResponse) -> Peer:
|
||||
|
|
@ -351,13 +369,17 @@ class Honcho(BaseModel, MetadataConfigMixin): # pyright: ignore[reportUnsafeMul
|
|||
self,
|
||||
metadata=peer.metadata,
|
||||
configuration=peer.configuration,
|
||||
created_at=peer.created_at,
|
||||
)
|
||||
|
||||
def fetch_next(page: int) -> SyncPage[PeerResponse, Peer]:
|
||||
def fetch_next(next_page: int) -> SyncPage[PeerResponse, Peer]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = self._http.post(
|
||||
routes.peers_list(self.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return SyncPage(next_data, PeerResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -382,45 +404,65 @@ class Honcho(BaseModel, MetadataConfigMixin): # pyright: ignore[reportUnsafeMul
|
|||
"""
|
||||
Get or create a session with the given ID.
|
||||
|
||||
Creates a Session object that can be used to manage conversations between
|
||||
multiple peers. This method does not make an API call unless `configuration` or
|
||||
`metadata` is provided.
|
||||
Makes an API call to get or create the session, then returns a Session object
|
||||
with cached values from the response.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for the session within the workspace. Should be a
|
||||
stable identifier that can be used consistently to reference the
|
||||
same conversation
|
||||
id: Unique identifier for the session within the workspace.
|
||||
metadata: Optional metadata dictionary to associate with this session.
|
||||
If set, will get/create session immediately with metadata.
|
||||
configuration: Optional configuration to set for this session.
|
||||
If set, will get/create session immediately with flags.
|
||||
|
||||
Returns:
|
||||
A Session object that can be used to add peers, send messages, and
|
||||
manage conversation context
|
||||
|
||||
Raises:
|
||||
ValidationError: If the session ID is empty or invalid
|
||||
A Session object with cached metadata, configuration, created_at, and is_active.
|
||||
"""
|
||||
return Session(id, self, configuration=configuration, metadata=metadata)
|
||||
self._ensure_workspace()
|
||||
body: dict[str, Any] = {"id": id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = self._http.post(routes.sessions(self.workspace_id), body=body)
|
||||
session_data = SessionResponse.model_validate(data)
|
||||
return Session(
|
||||
id,
|
||||
self,
|
||||
metadata=session_data.metadata,
|
||||
configuration=SessionConfiguration.model_validate(
|
||||
session_data.configuration.model_dump()
|
||||
),
|
||||
created_at=session_data.created_at,
|
||||
is_active=session_data.is_active,
|
||||
)
|
||||
|
||||
def sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> SyncPage[SessionResponse, Session]:
|
||||
"""
|
||||
Get all sessions in the current workspace.
|
||||
|
||||
Makes an API call to retrieve all sessions that have been created within
|
||||
the current workspace.
|
||||
Args:
|
||||
filters: Optional filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
|
||||
Returns:
|
||||
A SyncPage of Session objects representing all sessions in the workspace.
|
||||
Returns an empty page if no sessions exist
|
||||
"""
|
||||
self._ensure_workspace()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = self._http.post(
|
||||
routes.sessions_list(self.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(session: SessionResponse) -> Session:
|
||||
|
|
@ -429,13 +471,18 @@ class Honcho(BaseModel, MetadataConfigMixin): # pyright: ignore[reportUnsafeMul
|
|||
self,
|
||||
metadata=session.metadata,
|
||||
configuration=session.configuration,
|
||||
created_at=session.created_at,
|
||||
is_active=session.is_active,
|
||||
)
|
||||
|
||||
def fetch_next(page: int) -> SyncPage[SessionResponse, Session]:
|
||||
def fetch_next(next_page: int) -> SyncPage[SessionResponse, Session]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = self._http.post(
|
||||
routes.sessions_list(self.workspace_id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return SyncPage(next_data, SessionResponse, transform, fetch_next)
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ class ConclusionScope:
|
|||
page: int = 1,
|
||||
size: int = 50,
|
||||
session: str | SessionBase | None = None,
|
||||
*,
|
||||
reverse: bool = False,
|
||||
) -> SyncPage[ConclusionResponse, Conclusion]:
|
||||
"""
|
||||
List conclusions in this scope.
|
||||
|
|
@ -176,6 +178,7 @@ class ConclusionScope:
|
|||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session: Optional session (ID string or Session object) to filter by
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
|
||||
Returns:
|
||||
Paginated response containing Conclusion objects
|
||||
|
|
@ -189,22 +192,28 @@ class ConclusionScope:
|
|||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_session_id
|
||||
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = self._honcho._http.post(
|
||||
routes.conclusions_list(self.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": page, "size": size},
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(response: ConclusionResponse) -> Conclusion:
|
||||
return Conclusion.from_api_response(response)
|
||||
|
||||
def fetch_next(
|
||||
page: int,
|
||||
next_page: int,
|
||||
) -> SyncPage[ConclusionResponse, Conclusion]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = self._honcho._http.post(
|
||||
routes.conclusions_list(self.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": page, "size": size},
|
||||
query=next_query,
|
||||
)
|
||||
return SyncPage(next_data, ConclusionResponse, transform, fetch_next)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from .api_types import (
|
|||
PeerContextResponse,
|
||||
PeerResponse,
|
||||
RepresentationResponse,
|
||||
SessionConfiguration,
|
||||
SessionResponse,
|
||||
)
|
||||
from .base import PeerBase, SessionBase
|
||||
|
|
@ -57,6 +58,7 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: PeerConfig | None = PrivateAttr(default=None)
|
||||
_created_at: datetime.datetime | None = PrivateAttr(default=None)
|
||||
_honcho: "Honcho" = PrivateAttr()
|
||||
|
||||
@property
|
||||
|
|
@ -69,6 +71,11 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
"""Cached configuration for this peer. May be stale. Use get_configuration() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime.datetime | None:
|
||||
"""Timestamp when this peer was created. Only available if fetched from the API."""
|
||||
return self._created_at
|
||||
|
||||
# MetadataConfigMixin implementation
|
||||
def _get_http_client(self):
|
||||
self._honcho._ensure_workspace()
|
||||
|
|
@ -90,6 +97,21 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
# Return configuration as dict for mixin compatibility
|
||||
return peer.metadata or {}, peer.configuration.model_dump(exclude_none=True)
|
||||
|
||||
def _apply_peer_response(self, peer: PeerResponse) -> None:
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration
|
||||
self._created_at = peer.created_at
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
"""Get metadata from the server and update the cache."""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._get_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._apply_peer_response(peer)
|
||||
return self._metadata or {}
|
||||
|
||||
def get_configuration(self) -> PeerConfig: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""
|
||||
Get configuration from the server and update the cache.
|
||||
|
|
@ -102,9 +124,17 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration
|
||||
return self._configuration
|
||||
self._apply_peer_response(peer)
|
||||
return self._configuration or PeerConfig()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Refresh cached metadata, configuration, and created_at from the server."""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._get_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
peer = PeerResponse.model_validate(data)
|
||||
self._apply_peer_response(peer)
|
||||
|
||||
@validate_call
|
||||
def set_configuration( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
|
|
@ -163,6 +193,10 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
None,
|
||||
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
|
||||
),
|
||||
created_at: datetime.datetime | None = Field(
|
||||
None,
|
||||
description="Timestamp when this peer was created.",
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new Peer.
|
||||
|
|
@ -184,21 +218,8 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
)
|
||||
self._honcho = honcho
|
||||
self._metadata = metadata
|
||||
self._configuration = configuration
|
||||
|
||||
if configuration is not None or metadata is not None:
|
||||
self._honcho._ensure_workspace()
|
||||
body: dict[str, Any] = {"id": peer_id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = honcho._http.post(routes.peers(honcho.workspace_id), body=body)
|
||||
peer_data = PeerResponse.model_validate(data)
|
||||
# Update cached values with API response
|
||||
self._metadata = peer_data.metadata
|
||||
self._configuration = peer_data.configuration # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
self._configuration = configuration # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
self._created_at = created_at
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def chat(
|
||||
|
|
@ -309,35 +330,58 @@ class Peer(PeerBase, MetadataConfigMixin):
|
|||
return DialecticStreamResponse(stream_response())
|
||||
|
||||
def sessions(
|
||||
self, filters: dict[str, object] | None = None
|
||||
self,
|
||||
filters: dict[str, object] | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> SyncPage[SessionResponse, "Session"]:
|
||||
"""
|
||||
Get all sessions this peer is a member of.
|
||||
|
||||
Makes an API call to retrieve all sessions where this peer is an active participant.
|
||||
Sessions are created when peers are added to them or send messages to them.
|
||||
Args:
|
||||
filters: Optional filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, reverses the default ordering. Default: False.
|
||||
|
||||
Returns:
|
||||
A paginated list of Session objects this peer belongs to. Returns an empty
|
||||
list if the peer is not a member of any sessions
|
||||
A paginated list of Session objects this peer belongs to.
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
# Import here to avoid circular import (session.py imports Peer)
|
||||
from .session import Session
|
||||
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = self._honcho._http.post(
|
||||
routes.peer_sessions_list(self.workspace_id, self.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(session: SessionResponse) -> Session:
|
||||
return Session(session.id, self._honcho)
|
||||
return Session(
|
||||
session.id,
|
||||
self._honcho,
|
||||
metadata=session.metadata,
|
||||
configuration=SessionConfiguration.model_validate(
|
||||
session.configuration.model_dump()
|
||||
),
|
||||
created_at=session.created_at,
|
||||
is_active=session.is_active,
|
||||
)
|
||||
|
||||
def fetch_next(page: int) -> SyncPage[SessionResponse, Session]:
|
||||
def fetch_next(next_page: int) -> SyncPage[SessionResponse, Session]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = self._honcho._http.post(
|
||||
routes.peer_sessions_list(self.workspace_id, self.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return SyncPage(next_data, SessionResponse, transform, fetch_next)
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: SessionConfiguration | None = PrivateAttr(default=None)
|
||||
_created_at: datetime | None = PrivateAttr(default=None)
|
||||
_is_active: bool | None = PrivateAttr(default=None)
|
||||
_honcho: "Honcho" = PrivateAttr()
|
||||
|
||||
@property
|
||||
|
|
@ -74,6 +76,16 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
"""Cached configuration for this session. May be stale. Use get_configuration() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def created_at(self) -> datetime | None:
|
||||
"""Timestamp when this session was created. Only available if fetched from the API."""
|
||||
return self._created_at
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool | None:
|
||||
"""Whether this session is active. Only available if fetched from the API."""
|
||||
return self._is_active
|
||||
|
||||
# MetadataConfigMixin implementation
|
||||
def _get_http_client(self):
|
||||
self._honcho._ensure_workspace()
|
||||
|
|
@ -97,6 +109,30 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
exclude_none=True
|
||||
)
|
||||
|
||||
def _apply_session_response(self, session: SessionResponse) -> None:
|
||||
self._metadata = session.metadata or {}
|
||||
self._configuration = SessionConfiguration.model_validate(
|
||||
session.configuration.model_dump()
|
||||
)
|
||||
self._created_at = session.created_at
|
||||
self._is_active = session.is_active
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
Get metadata from the server and update the cache.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the metadata. Returns an empty dictionary
|
||||
if no metadata is set.
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._get_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._apply_session_response(session)
|
||||
return self._metadata or {}
|
||||
|
||||
def get_configuration(self) -> SessionConfiguration: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
"""
|
||||
Get configuration from the server and update the cache.
|
||||
|
|
@ -109,9 +145,19 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._metadata = session.metadata or {}
|
||||
self._configuration = session.configuration
|
||||
return self._configuration
|
||||
self._apply_session_response(session)
|
||||
return self._configuration or SessionConfiguration()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata, configuration, and session status from the server.
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._get_http_client().post(
|
||||
self._get_fetch_route(), body=self._get_fetch_body()
|
||||
)
|
||||
session = SessionResponse.model_validate(data)
|
||||
self._apply_session_response(session)
|
||||
|
||||
@validate_call
|
||||
def set_configuration( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
|
|
@ -171,6 +217,14 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
None,
|
||||
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
|
||||
),
|
||||
created_at: datetime | None = Field(
|
||||
None,
|
||||
description="Timestamp when this session was created.",
|
||||
),
|
||||
is_active: bool | None = Field(
|
||||
None,
|
||||
description="Whether this session is active.",
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new Session.
|
||||
|
|
@ -192,21 +246,9 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
)
|
||||
self._honcho = honcho
|
||||
self._metadata = metadata
|
||||
self._configuration = configuration
|
||||
|
||||
if configuration is not None or metadata is not None:
|
||||
self._honcho._ensure_workspace()
|
||||
body: dict[str, Any] = {"id": session_id}
|
||||
if metadata is not None:
|
||||
body["metadata"] = metadata
|
||||
if configuration is not None:
|
||||
body["configuration"] = configuration.model_dump(exclude_none=True)
|
||||
|
||||
data = honcho._http.post(routes.sessions(honcho.workspace_id), body=body)
|
||||
session_data = SessionResponse.model_validate(data)
|
||||
# Update cached values with API response
|
||||
self._metadata = session_data.metadata
|
||||
self._configuration = session_data.configuration # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
self._configuration = configuration # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
self._created_at = created_at
|
||||
self._is_active = is_active
|
||||
|
||||
def add_peers(
|
||||
self,
|
||||
|
|
@ -405,38 +447,43 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
filters: dict[str, object] | None = Field(
|
||||
None, description="Dictionary of filter criteria"
|
||||
),
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
reverse: bool = False,
|
||||
) -> SyncPage[MessageResponse, Message]:
|
||||
"""
|
||||
Get messages from this session with optional filtering.
|
||||
|
||||
Makes an API call to retrieve messages from this session. Results can be
|
||||
filtered based on various criteria.
|
||||
|
||||
Args:
|
||||
filters: Dictionary of filter criteria. Supported filters include:
|
||||
- peer_id: Filter messages by the peer who created them
|
||||
- metadata: Filter messages by metadata key-value pairs
|
||||
- timestamp_start: Filter messages after a specific timestamp
|
||||
- timestamp_end: Filter messages before a specific timestamp
|
||||
filters: Dictionary of filter criteria.
|
||||
page: Page number (1-indexed). Default: 1.
|
||||
size: Number of items per page. Default: 50.
|
||||
reverse: If True, returns messages in reverse chronological order. Default: False.
|
||||
|
||||
Returns:
|
||||
A list of Message objects matching the specified criteria, ordered by
|
||||
creation time (most recent first)
|
||||
A paginated result of Message objects.
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
query: dict[str, Any] = {"page": page, "size": size}
|
||||
if reverse:
|
||||
query["reverse"] = "true"
|
||||
data = self._honcho._http.post(
|
||||
routes.messages_list(self.workspace_id, self.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query=query,
|
||||
)
|
||||
|
||||
def transform(response: MessageResponse) -> Message:
|
||||
return Message.from_api_response(response)
|
||||
|
||||
def fetch_next(page: int) -> SyncPage[MessageResponse, Message]:
|
||||
def fetch_next(next_page: int) -> SyncPage[MessageResponse, Message]:
|
||||
next_query: dict[str, Any] = {"page": next_page, "size": size}
|
||||
if reverse:
|
||||
next_query["reverse"] = "true"
|
||||
next_data = self._honcho._http.post(
|
||||
routes.messages_list(self.workspace_id, self.id),
|
||||
body={"filters": filters} if filters else None,
|
||||
query={"page": page},
|
||||
query=next_query,
|
||||
)
|
||||
return SyncPage(next_data, MessageResponse, transform, fetch_next)
|
||||
|
||||
|
|
@ -503,6 +550,8 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
self._honcho,
|
||||
metadata=cloned.metadata,
|
||||
configuration=cloned.configuration,
|
||||
created_at=cloned.created_at,
|
||||
is_active=cloned.is_active,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
|
|
@ -919,6 +968,21 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
)
|
||||
return QueueStatusResponse.model_validate(data)
|
||||
|
||||
def get_message(self, message_id: str) -> Message:
|
||||
"""Get a single message by ID from this session.
|
||||
|
||||
Args:
|
||||
message_id: The ID of the message to retrieve
|
||||
|
||||
Returns:
|
||||
The Message object
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._honcho._http.get(
|
||||
routes.message(self.workspace_id, self.id, message_id)
|
||||
)
|
||||
return Message.from_api_response(MessageResponse.model_validate(data))
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def update_message(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,38 @@ 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.1.0] - 2026-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- `createdAt` property on `Peer` and `Session` wrapper objects
|
||||
- `isActive` property on `Session` wrapper objects
|
||||
- `getMessage(messageId)` method on `Session` to fetch a single message by ID
|
||||
- `Peer.representation()`, `Session.representation()`, and `Session.context()` now accept `Message` objects for `searchQuery` (extracts `.content` automatically)
|
||||
- `page`, `size`, and `reverse` pagination controls on all list methods: `peers()`, `sessions()`, `messages()`, `workspaces()`, and `conclusions.list()`
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking**: `searchQuery` removed from top-level `context()` options. Use `representationOptions.searchQuery` instead — this eliminates the duplicate parameter and is consistent with the API structure.
|
||||
- List methods (`peers()`, `sessions()`, `messages()`, `workspaces()`) support both the new options object and the legacy raw-filter form: `peers({ filters, page, size, reverse })` and `peers(filters)`.
|
||||
- Representation search options now accept strings and content-like objects (including `Message` instances) while rejecting whitespace-only or invalid runtime inputs.
|
||||
- **Breaking**: `peer()` and `session()` now always make a get-or-create API call. Previously, calling without metadata/configuration returned a lazy object with no API call. All Peer/Session objects now have `createdAt` populated immediately.
|
||||
- Response configuration models (`WorkspaceConfigurationResponse`, `SessionConfigurationResponse`) now tolerate unknown fields from newer servers for forward compatibility
|
||||
- Reusable `PeerIdObjectSchema` and `SessionIdObjectSchema` helpers for union validation
|
||||
- Moved `@types/node` from `dependencies` to `devDependencies`
|
||||
|
||||
### Fixed
|
||||
|
||||
- `uploadFile()` now rejects unsupported top-level binary/object inputs and only validates inputs the serializer can actually upload.
|
||||
- `uploadFile()` now serializes message configuration using API field names, matching `addMessages()`.
|
||||
- Session fetch methods now refresh cached `createdAt` and `isActive` values alongside metadata and configuration.
|
||||
|
||||
## [2.0.2] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking**: Client constructor now rejects unknown options via `.strict()` Zod validation. Previously, misspelled options (e.g., `baseUrl` instead of `baseURL`) were silently ignored, causing the SDK to fall back to defaults. Now a `ZodError` is thrown with the unrecognized key name.
|
||||
|
||||
## [2.0.1] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
|
||||
const honcho = new Honcho({
|
||||
apiKey: process.env.HONCHO_API_KEY,
|
||||
baseUrl: "http://localhost:8000",
|
||||
baseURL: "http://localhost:8000",
|
||||
workspaceId: "test",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,27 @@ describe('Honcho Client', () => {
|
|||
await testClient.deleteWorkspace(testClient.workspaceId)
|
||||
}
|
||||
})
|
||||
|
||||
test('workspaces with options.filters narrows results', async () => {
|
||||
const uniqueValue = `filter-options-${Date.now()}`
|
||||
const testClient = new Honcho({
|
||||
baseURL: TEST_CONFIG.baseURL,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
workspaceId: generateWorkspaceId('filter-options'),
|
||||
})
|
||||
|
||||
try {
|
||||
await testClient.setMetadata({ filterKey: uniqueValue })
|
||||
|
||||
const page = await client.workspaces({
|
||||
filters: { metadata: { filterKey: uniqueValue } },
|
||||
})
|
||||
|
||||
expect(page.items).toContain(testClient.workspaceId)
|
||||
} finally {
|
||||
await testClient.deleteWorkspace(testClient.workspaceId)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -172,11 +193,12 @@ describe('Honcho Client', () => {
|
|||
// ===========================================================================
|
||||
|
||||
describe('Peer access', () => {
|
||||
test('peer() returns Peer instance without API call', async () => {
|
||||
test('peer() returns Peer instance with cached data', async () => {
|
||||
const peer = await client.peer('lazy-peer')
|
||||
|
||||
expect(peer.id).toBe('lazy-peer')
|
||||
expect(peer.workspaceId).toBe(client.workspaceId)
|
||||
expect(peer.createdAt).toBeDefined()
|
||||
})
|
||||
|
||||
test('peer() with metadata makes API call', async () => {
|
||||
|
|
@ -205,11 +227,13 @@ describe('Honcho Client', () => {
|
|||
})
|
||||
|
||||
describe('Session access', () => {
|
||||
test('session() returns Session instance without API call', async () => {
|
||||
test('session() returns Session instance with cached data', async () => {
|
||||
const session = await client.session('lazy-session', { metadata: {} })
|
||||
|
||||
expect(session.id).toBe('lazy-session')
|
||||
expect(session.workspaceId).toBe(client.workspaceId)
|
||||
expect(session.createdAt).toBeDefined()
|
||||
expect(session.isActive).toBe(true)
|
||||
})
|
||||
|
||||
test('session() with metadata makes API call', async () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ import type {
|
|||
PageResponse,
|
||||
} from '../src/types/api'
|
||||
|
||||
function expectValidDateString(value: string): void {
|
||||
const parsedDate = new Date(value)
|
||||
expect(Number.isNaN(parsedDate.getTime())).toBe(false)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response Shape Assertions
|
||||
// =============================================================================
|
||||
|
|
@ -28,8 +33,7 @@ export function assertWorkspaceShape(workspace: WorkspaceResponse): void {
|
|||
expect(typeof workspace.metadata).toBe('object')
|
||||
expect(typeof workspace.configuration).toBe('object')
|
||||
expect(typeof workspace.created_at).toBe('string')
|
||||
// Validate ISO 8601 date format
|
||||
expect(() => new Date(workspace.created_at)).not.toThrow()
|
||||
expectValidDateString(workspace.created_at)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +47,7 @@ export function assertPeerShape(peer: PeerResponse): void {
|
|||
expect(typeof peer.metadata).toBe('object')
|
||||
expect(typeof peer.configuration).toBe('object')
|
||||
expect(typeof peer.created_at).toBe('string')
|
||||
expect(() => new Date(peer.created_at)).not.toThrow()
|
||||
expectValidDateString(peer.created_at)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +62,7 @@ export function assertSessionShape(session: SessionResponse): void {
|
|||
expect(typeof session.metadata).toBe('object')
|
||||
expect(typeof session.configuration).toBe('object')
|
||||
expect(typeof session.created_at).toBe('string')
|
||||
expect(() => new Date(session.created_at)).not.toThrow()
|
||||
expectValidDateString(session.created_at)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,7 +80,7 @@ export function assertMessageShape(message: Message): void {
|
|||
expect(typeof message.createdAt).toBe('string')
|
||||
expect(typeof message.tokenCount).toBe('number')
|
||||
expect(message.tokenCount).toBeGreaterThanOrEqual(0)
|
||||
expect(() => new Date(message.createdAt)).not.toThrow()
|
||||
expectValidDateString(message.createdAt)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,7 +95,7 @@ export function assertConclusionShape(conclusion: ConclusionResponse): void {
|
|||
expect(typeof conclusion.observed_id).toBe('string')
|
||||
expect(typeof conclusion.session_id).toBe('string')
|
||||
expect(typeof conclusion.created_at).toBe('string')
|
||||
expect(() => new Date(conclusion.created_at)).not.toThrow()
|
||||
expectValidDateString(conclusion.created_at)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ describe('Messages', () => {
|
|||
bob.message('From Bob'),
|
||||
])
|
||||
|
||||
const page = await session.messages({ peer_id: alice.id })
|
||||
const page = await session.messages({ filters: { peer_id: alice.id } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].peerId).toBe(alice.id)
|
||||
|
|
@ -238,11 +238,27 @@ describe('Messages', () => {
|
|||
peer.message('Untagged'),
|
||||
])
|
||||
|
||||
const page = await session.messages({ metadata: { category: 'important' } })
|
||||
const page = await session.messages({ filters: { metadata: { category: 'important' } } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].metadata.category).toBe('important')
|
||||
})
|
||||
|
||||
test('accepts legacy raw filter objects', async () => {
|
||||
const session = await client.session('legacy-filter-msg-session', { metadata: {} })
|
||||
const alice = await client.peer('legacy-filter-alice')
|
||||
const bob = await client.peer('legacy-filter-bob')
|
||||
await session.addPeers([alice.id, bob.id])
|
||||
await session.addMessages([
|
||||
alice.message('Legacy Alice'),
|
||||
bob.message('Legacy Bob'),
|
||||
])
|
||||
|
||||
const page = await session.messages({ peer_id: alice.id })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].peerId).toBe(alice.id)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -382,8 +398,8 @@ describe('Messages', () => {
|
|||
expect(typeof message.tokenCount).toBe('number')
|
||||
|
||||
// Validate date format
|
||||
expect(() => new Date(message.createdAt)).not.toThrow()
|
||||
const date = new Date(message.createdAt)
|
||||
expect(Number.isNaN(date.getTime())).toBe(false)
|
||||
expect(date.getTime()).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
|
|
@ -403,4 +419,109 @@ describe('Messages', () => {
|
|||
expect(uniqueIds.size).toBe(ids.length)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Get Single Message (GET /messages/:id)
|
||||
// ===========================================================================
|
||||
|
||||
describe('GET /messages/:id (getMessage)', () => {
|
||||
test('getMessage returns message by ID', async () => {
|
||||
const session = await client.session('get-msg-session', { metadata: {} })
|
||||
const peer = await client.peer('get-msg-peer')
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
const [created] = await session.addMessages(peer.message('Retrievable'))
|
||||
const fetched = await session.getMessage(created.id)
|
||||
|
||||
expect(fetched.id).toBe(created.id)
|
||||
expect(fetched.content).toBe('Retrievable')
|
||||
expect(fetched.peerId).toBe(peer.id)
|
||||
expect(fetched.sessionId).toBe(session.id)
|
||||
})
|
||||
|
||||
test('getMessage preserves metadata', async () => {
|
||||
const session = await client.session('get-msg-meta-session', {
|
||||
metadata: {},
|
||||
})
|
||||
const peer = await client.peer('get-msg-meta-peer')
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
const [created] = await session.addMessages(
|
||||
peer.message('With metadata', { metadata: { key: 'value' } })
|
||||
)
|
||||
const fetched = await session.getMessage(created.id)
|
||||
|
||||
expect(fetched.metadata).toEqual({ key: 'value' })
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Pagination Controls
|
||||
// ===========================================================================
|
||||
|
||||
describe('Pagination controls', () => {
|
||||
test('messages with custom page size', async () => {
|
||||
const session = await client.session('page-size-session', { metadata: {} })
|
||||
const peer = await client.peer('page-size-peer')
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
// Create 3 messages
|
||||
await session.addMessages([
|
||||
peer.message('One'),
|
||||
peer.message('Two'),
|
||||
peer.message('Three'),
|
||||
])
|
||||
|
||||
// Fetch with size=2
|
||||
const page = await session.messages({ size: 2 })
|
||||
const items = await collectAll(page)
|
||||
|
||||
// Should get all 3 via auto-pagination, but first page had only 2
|
||||
expect(items.length).toBe(3)
|
||||
})
|
||||
|
||||
test('messages with explicit page number', async () => {
|
||||
const session = await client.session('page-num-session', { metadata: {} })
|
||||
const peer = await client.peer('page-num-peer')
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
await session.addMessages([
|
||||
peer.message('A'),
|
||||
peer.message('B'),
|
||||
peer.message('C'),
|
||||
])
|
||||
|
||||
// Fetch page 1 with size 2
|
||||
const page1 = await session.messages({ page: 1, size: 2 })
|
||||
expect(page1.items.length).toBe(2)
|
||||
|
||||
// Fetch page 2
|
||||
const page2 = await session.messages({ page: 2, size: 2 })
|
||||
expect(page2.items.length).toBe(1)
|
||||
})
|
||||
|
||||
test('messages with reverse ordering', async () => {
|
||||
const session = await client.session('reverse-session', { metadata: {} })
|
||||
const peer = await client.peer('reverse-peer')
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
await session.addMessages([
|
||||
peer.message('First'),
|
||||
peer.message('Second'),
|
||||
peer.message('Third'),
|
||||
])
|
||||
|
||||
const normal = await session.messages()
|
||||
const reversed = await session.messages({ reverse: true })
|
||||
|
||||
const normalItems = await collectAll(normal)
|
||||
const reversedItems = await collectAll(reversed)
|
||||
|
||||
expect(normalItems.length).toBe(reversedItems.length)
|
||||
// Reversed order should have different first element
|
||||
expect(normalItems[0].content).not.toBe(
|
||||
reversedItems[0].content
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -52,6 +52,18 @@ describe('Peer', () => {
|
|||
expect(peer).toBeInstanceOf(Peer)
|
||||
expect(peer.id).toBe('simple-peer')
|
||||
expect(peer.workspaceId).toBe(client.workspaceId)
|
||||
expect(peer.createdAt).toBeDefined()
|
||||
})
|
||||
|
||||
test('peer created with metadata exposes createdAt', async () => {
|
||||
const peer = await client.peer('peer-created-at', {
|
||||
metadata: { test: true },
|
||||
})
|
||||
|
||||
expect(peer.createdAt).toBeDefined()
|
||||
expect(typeof peer.createdAt).toBe('string')
|
||||
const createdAt = new Date(peer.createdAt!)
|
||||
expect(Number.isNaN(createdAt.getTime())).toBe(false)
|
||||
})
|
||||
|
||||
test('creates peer with metadata', async () => {
|
||||
|
|
@ -116,12 +128,24 @@ describe('Peer', () => {
|
|||
metadata: { uniqueTag },
|
||||
})
|
||||
|
||||
const page = await client.peers({ metadata: { uniqueTag } })
|
||||
const page = await client.peers({ filters: { metadata: { uniqueTag } } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].id).toBe('filtered-peer')
|
||||
})
|
||||
|
||||
test('peers accept legacy raw filter objects', async () => {
|
||||
const uniqueTag = `legacy-tag-${Date.now()}`
|
||||
await client.peer(`legacy-filtered-peer-${Date.now()}`, {
|
||||
metadata: { uniqueTag },
|
||||
})
|
||||
|
||||
const page = await client.peers({ metadata: { uniqueTag } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].metadata.uniqueTag).toBe(uniqueTag)
|
||||
})
|
||||
|
||||
test('Page is async iterable', async () => {
|
||||
await client.peer('iter-peer-1', { metadata: {} })
|
||||
await client.peer('iter-peer-2', { metadata: {} })
|
||||
|
|
@ -213,10 +237,24 @@ describe('Peer', () => {
|
|||
})
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
const sessions = await peer.sessions({ metadata: { category: 'special' } })
|
||||
const sessions = await peer.sessions({ filters: { metadata: { category: 'special' } } })
|
||||
|
||||
expect(sessions.items.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('sessions accept legacy raw filter objects', async () => {
|
||||
const category = `legacy-category-${Date.now()}`
|
||||
const peer = await client.peer(`legacy-filter-sessions-peer-${Date.now()}`)
|
||||
const session = await client.session(`legacy-filterable-session-${Date.now()}`, {
|
||||
metadata: { category },
|
||||
})
|
||||
await session.addPeers([peer.id])
|
||||
|
||||
const sessions = await peer.sessions({ metadata: { category } })
|
||||
|
||||
expect(sessions.items.length).toBeGreaterThanOrEqual(1)
|
||||
expect(sessions.items[0].metadata.category).toBe(category)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -637,4 +675,32 @@ describe('Peer', () => {
|
|||
expect(str).toBe("Peer(id='tostring-peer')")
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Pagination Controls
|
||||
// ===========================================================================
|
||||
|
||||
describe('Pagination controls', () => {
|
||||
test('peers with custom page size', async () => {
|
||||
// Create peers with unique metadata
|
||||
await client.peer('page-peer-1', { metadata: { group: 'page-test' } })
|
||||
await client.peer('page-peer-2', { metadata: { group: 'page-test' } })
|
||||
await client.peer('page-peer-3', { metadata: { group: 'page-test' } })
|
||||
|
||||
const page = await client.peers({
|
||||
filters: { metadata: { group: 'page-test' } },
|
||||
size: 2,
|
||||
})
|
||||
expect(page.items.length).toBe(2)
|
||||
})
|
||||
|
||||
test('peers with explicit page number', async () => {
|
||||
const page2 = await client.peers({
|
||||
filters: { metadata: { group: 'page-test' } },
|
||||
page: 2,
|
||||
size: 2,
|
||||
})
|
||||
expect(page2.items.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,20 @@ describe('Session', () => {
|
|||
expect(session).toBeInstanceOf(Session)
|
||||
expect(session.id).toBe('simple-session')
|
||||
expect(session.workspaceId).toBe(client.workspaceId)
|
||||
expect(session.createdAt).toBeDefined()
|
||||
expect(session.isActive).toBe(true)
|
||||
})
|
||||
|
||||
test('session created with metadata exposes createdAt and isActive', async () => {
|
||||
const session = await client.session('session-timestamps', {
|
||||
metadata: { test: true },
|
||||
})
|
||||
|
||||
expect(session.createdAt).toBeDefined()
|
||||
expect(typeof session.createdAt).toBe('string')
|
||||
const createdAt = new Date(session.createdAt!)
|
||||
expect(Number.isNaN(createdAt.getTime())).toBe(false)
|
||||
expect(session.isActive).toBe(true)
|
||||
})
|
||||
|
||||
test('creates session with metadata', async () => {
|
||||
|
|
@ -104,11 +118,23 @@ describe('Session', () => {
|
|||
const tag = `tag-${Date.now()}`
|
||||
await client.session('filtered-session', { metadata: { tag } })
|
||||
|
||||
const page = await client.sessions({ metadata: { tag } })
|
||||
const page = await client.sessions({ filters: { metadata: { tag } } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].id).toBe('filtered-session')
|
||||
})
|
||||
|
||||
test('sessions accept legacy raw filter objects', async () => {
|
||||
const tag = `legacy-tag-${Date.now()}`
|
||||
await client.session(`legacy-filtered-session-${Date.now()}`, {
|
||||
metadata: { tag },
|
||||
})
|
||||
|
||||
const page = await client.sessions({ metadata: { tag } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].metadata.tag).toBe(tag)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -446,7 +472,7 @@ describe('Session', () => {
|
|||
peer.message('Not this one'),
|
||||
])
|
||||
|
||||
const page = await session.messages({ metadata: { tag: 'special' } })
|
||||
const page = await session.messages({ filters: { metadata: { tag: 'special' } } })
|
||||
|
||||
expect(page.items.length).toBe(1)
|
||||
expect(page.items[0].metadata.tag).toBe('special')
|
||||
|
|
@ -610,4 +636,34 @@ describe('Session', () => {
|
|||
expect(str).toBe("Session(id='tostring-session')")
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Pagination Controls
|
||||
// ===========================================================================
|
||||
|
||||
describe('Pagination controls', () => {
|
||||
test('sessions with custom page size', async () => {
|
||||
// Create 3 sessions
|
||||
await client.session('page-test-1', { metadata: { group: 'page-test' } })
|
||||
await client.session('page-test-2', { metadata: { group: 'page-test' } })
|
||||
await client.session('page-test-3', { metadata: { group: 'page-test' } })
|
||||
|
||||
const page = await client.sessions({
|
||||
filters: { metadata: { group: 'page-test' } },
|
||||
size: 2,
|
||||
})
|
||||
// First page should have 2 items
|
||||
expect(page.items.length).toBe(2)
|
||||
})
|
||||
|
||||
test('sessions with explicit page number', async () => {
|
||||
const page2 = await client.sessions({
|
||||
filters: { metadata: { group: 'page-test' } },
|
||||
page: 2,
|
||||
size: 2,
|
||||
})
|
||||
// Page 2 should have the remaining item
|
||||
expect(page2.items.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
import { ZodError } from 'zod'
|
||||
import type { HonchoHTTPClient } from '../src/http/client'
|
||||
import { Session } from '../src/session'
|
||||
import type { SessionContextResponse, SessionResponse } from '../src/types/api'
|
||||
|
||||
function createSessionResponse(
|
||||
overrides: Partial<SessionResponse> = {}
|
||||
): SessionResponse {
|
||||
return {
|
||||
id: 'session-1',
|
||||
workspace_id: 'workspace-1',
|
||||
is_active: true,
|
||||
metadata: {},
|
||||
configuration: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionContextResponse(): SessionContextResponse {
|
||||
return {
|
||||
id: 'session-1',
|
||||
messages: [],
|
||||
summary: null,
|
||||
peer_representation: null,
|
||||
peer_card: null,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Session unit behavior', () => {
|
||||
test('getMetadata populates createdAt and isActive from fetched session data', async () => {
|
||||
const http = {
|
||||
post: async () =>
|
||||
createSessionResponse({
|
||||
metadata: { topic: 'testing' },
|
||||
created_at: '2024-02-01T12:00:00Z',
|
||||
is_active: true,
|
||||
}),
|
||||
} as unknown as HonchoHTTPClient
|
||||
|
||||
const session = new Session('session-1', 'workspace-1', http)
|
||||
|
||||
expect(session.createdAt).toBeUndefined()
|
||||
expect(session.isActive).toBeUndefined()
|
||||
|
||||
const metadata = await session.getMetadata()
|
||||
|
||||
expect(metadata).toEqual({ topic: 'testing' })
|
||||
expect(session.createdAt).toBe('2024-02-01T12:00:00Z')
|
||||
expect(session.isActive).toBe(true)
|
||||
})
|
||||
|
||||
test('refresh updates cached createdAt and isActive from the latest fetch', async () => {
|
||||
const responses = [
|
||||
createSessionResponse({
|
||||
metadata: { version: 1 },
|
||||
configuration: { reasoning: { enabled: true } },
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
is_active: true,
|
||||
}),
|
||||
createSessionResponse({
|
||||
metadata: { version: 2 },
|
||||
configuration: { reasoning: { enabled: false } },
|
||||
created_at: '2024-01-02T00:00:00Z',
|
||||
is_active: false,
|
||||
}),
|
||||
]
|
||||
|
||||
const http = {
|
||||
post: async () => responses.shift() ?? createSessionResponse(),
|
||||
} as unknown as HonchoHTTPClient
|
||||
|
||||
const session = new Session('session-1', 'workspace-1', http)
|
||||
|
||||
await session.getMetadata()
|
||||
await session.refresh()
|
||||
|
||||
expect(session.metadata).toEqual({ version: 2 })
|
||||
expect(session.configuration).toMatchObject({
|
||||
reasoning: { enabled: false },
|
||||
})
|
||||
expect(session.createdAt).toBe('2024-01-02T00:00:00Z')
|
||||
expect(session.isActive).toBe(false)
|
||||
})
|
||||
|
||||
test('uploadFile converts configuration to API field names', async () => {
|
||||
let capturedFormData: FormData | undefined
|
||||
|
||||
const http = {
|
||||
upload: async (_path: string, formData: FormData) => {
|
||||
capturedFormData = formData
|
||||
return []
|
||||
},
|
||||
} as unknown as HonchoHTTPClient
|
||||
|
||||
const session = new Session('session-1', 'workspace-1', http)
|
||||
|
||||
await session.uploadFile(new Blob(['hello'], { type: 'text/plain' }), 'peer-1', {
|
||||
configuration: {
|
||||
reasoning: {
|
||||
enabled: true,
|
||||
customInstructions: 'Keep headings intact',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(capturedFormData).toBeDefined()
|
||||
|
||||
const rawConfiguration = capturedFormData?.get('configuration')
|
||||
expect(typeof rawConfiguration).toBe('string')
|
||||
expect(JSON.parse(rawConfiguration as string)).toEqual({
|
||||
reasoning: {
|
||||
enabled: true,
|
||||
custom_instructions: 'Keep headings intact',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('context rejects invalid content-like searchQuery inputs', async () => {
|
||||
const http = {
|
||||
get: async () => createSessionContextResponse(),
|
||||
} as unknown as HonchoHTTPClient
|
||||
|
||||
const session = new Session('session-1', 'workspace-1', http)
|
||||
|
||||
await expect(
|
||||
session.context({
|
||||
peerTarget: 'peer-1',
|
||||
representationOptions: {
|
||||
searchQuery: { foo: 'bar' } as never,
|
||||
},
|
||||
})
|
||||
).rejects.toBeInstanceOf(ZodError)
|
||||
})
|
||||
|
||||
test('representation rejects whitespace-only content-like searchQuery inputs', async () => {
|
||||
const http = {
|
||||
post: async () => ({ representation: 'ok' }),
|
||||
} as unknown as HonchoHTTPClient
|
||||
|
||||
const session = new Session('session-1', 'workspace-1', http)
|
||||
|
||||
await expect(
|
||||
session.representation('peer-1', {
|
||||
searchQuery: { content: ' ' } as never,
|
||||
})
|
||||
).rejects.toBeInstanceOf(ZodError)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
/**
|
||||
* Validation Schema Tests
|
||||
*
|
||||
* Unit tests for Zod validation schemas. No server required.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { ZodError } from 'zod'
|
||||
import {
|
||||
ChatQuerySchema,
|
||||
ContextParamsSchema,
|
||||
HonchoConfigSchema,
|
||||
MessageInputSchema,
|
||||
FileUploadSchema,
|
||||
RepresentationOptionsSchema,
|
||||
} from '../src/validation'
|
||||
|
||||
// =============================================================================
|
||||
// ChatQuerySchema
|
||||
// =============================================================================
|
||||
|
||||
describe('ChatQuerySchema', () => {
|
||||
// --- Valid inputs ---
|
||||
|
||||
test('minimal valid query', () => {
|
||||
const result = ChatQuerySchema.parse({ query: 'hello' })
|
||||
expect(result.query).toBe('hello')
|
||||
expect(result.target).toBeUndefined()
|
||||
expect(result.session).toBeUndefined()
|
||||
expect(result.reasoningLevel).toBeUndefined()
|
||||
})
|
||||
|
||||
test('query with all optional fields', () => {
|
||||
const result = ChatQuerySchema.parse({
|
||||
query: 'hello',
|
||||
target: 'peer-1',
|
||||
session: 'session-1',
|
||||
reasoningLevel: 'high',
|
||||
})
|
||||
expect(result.query).toBe('hello')
|
||||
expect(result.target).toBe('peer-1')
|
||||
expect(result.session).toBe('session-1')
|
||||
expect(result.reasoningLevel).toBe('high')
|
||||
})
|
||||
|
||||
test('target as object with id is transformed to string', () => {
|
||||
const result = ChatQuerySchema.parse({
|
||||
query: 'hello',
|
||||
target: { id: 'peer-1' },
|
||||
})
|
||||
expect(result.target).toBe('peer-1')
|
||||
})
|
||||
|
||||
test('session as object with id is transformed to string', () => {
|
||||
const result = ChatQuerySchema.parse({
|
||||
query: 'hello',
|
||||
session: { id: 'session-1' },
|
||||
})
|
||||
expect(result.session).toBe('session-1')
|
||||
})
|
||||
|
||||
test.each(['minimal', 'low', 'medium', 'high', 'max'] as const)(
|
||||
'reasoning level "%s" is valid',
|
||||
(level) => {
|
||||
const result = ChatQuerySchema.parse({ query: 'hello', reasoningLevel: level })
|
||||
expect(result.reasoningLevel).toBe(level)
|
||||
}
|
||||
)
|
||||
|
||||
// --- Missing required fields ---
|
||||
|
||||
test('missing query throws', () => {
|
||||
expect(() => ChatQuerySchema.parse({})).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('empty query throws', () => {
|
||||
expect(() => ChatQuerySchema.parse({ query: '' })).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('whitespace-only query throws', () => {
|
||||
expect(() => ChatQuerySchema.parse({ query: ' ' })).toThrow(ZodError)
|
||||
})
|
||||
|
||||
// --- Invalid field values ---
|
||||
|
||||
test('invalid reasoning level throws', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', reasoningLevel: 'ultra' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('empty target string throws', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', target: '' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('empty session string throws', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', session: '' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('target with special characters throws', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', target: 'peer with spaces' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('session with special characters throws', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', session: 'session/bad' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
// --- Strict mode: unknown fields ---
|
||||
|
||||
test('unknown field throws (strict)', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', typo: 'oops' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('misspelled field throws (strict)', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({ query: 'hello', reasoning_level: 'high' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('extra field alongside valid fields throws (strict)', () => {
|
||||
expect(() =>
|
||||
ChatQuerySchema.parse({
|
||||
query: 'hello',
|
||||
target: 'peer-1',
|
||||
session: 'session-1',
|
||||
reasoningLevel: 'low',
|
||||
extra: true,
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// HonchoConfigSchema (strict validation)
|
||||
// =============================================================================
|
||||
|
||||
describe('HonchoConfigSchema (strict)', () => {
|
||||
test('baseUrl (wrong casing) throws', () => {
|
||||
expect(() =>
|
||||
HonchoConfigSchema.parse({
|
||||
baseUrl: 'http://localhost:8000',
|
||||
workspaceId: 'test',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('baseURL (correct casing) passes', () => {
|
||||
const result = HonchoConfigSchema.parse({
|
||||
baseURL: 'http://localhost:8000',
|
||||
workspaceId: 'test',
|
||||
})
|
||||
expect(result.baseURL).toBe('http://localhost:8000')
|
||||
})
|
||||
|
||||
test('unknown option throws', () => {
|
||||
expect(() =>
|
||||
HonchoConfigSchema.parse({
|
||||
workspaceId: 'test',
|
||||
retries: 3,
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// MessageInputSchema (strict validation)
|
||||
// =============================================================================
|
||||
|
||||
describe('MessageInputSchema (strict)', () => {
|
||||
test('valid message input', () => {
|
||||
const result = MessageInputSchema.parse({
|
||||
peerId: 'peer-1',
|
||||
content: 'hello',
|
||||
metadata: { key: 'value' },
|
||||
})
|
||||
expect(result.peerId).toBe('peer-1')
|
||||
expect(result.content).toBe('hello')
|
||||
})
|
||||
|
||||
test('snake_case peerId alias throws (strict)', () => {
|
||||
expect(() =>
|
||||
MessageInputSchema.parse({
|
||||
peer_id: 'peer-1',
|
||||
content: 'hello',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('unknown field throws (strict)', () => {
|
||||
expect(() =>
|
||||
MessageInputSchema.parse({
|
||||
peerId: 'peer-1',
|
||||
content: 'hello',
|
||||
role: 'user',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ContextParamsSchema (searchQuery in representationOptions)
|
||||
// =============================================================================
|
||||
|
||||
describe('ContextParamsSchema', () => {
|
||||
test('top-level searchQuery throws (strict)', () => {
|
||||
expect(() =>
|
||||
ContextParamsSchema.parse({
|
||||
peerTarget: 'peer-1',
|
||||
searchQuery: 'hello',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('searchQuery inside representationOptions passes', () => {
|
||||
const result = ContextParamsSchema.parse({
|
||||
peerTarget: 'peer-1',
|
||||
representationOptions: { searchQuery: 'hello' },
|
||||
})
|
||||
expect(result.representationOptions?.searchQuery).toBe('hello')
|
||||
})
|
||||
|
||||
test('representationOptions.searchQuery requires peerTarget', () => {
|
||||
expect(() =>
|
||||
ContextParamsSchema.parse({
|
||||
representationOptions: { searchQuery: 'hello' },
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('minimal valid context params', () => {
|
||||
const result = ContextParamsSchema.parse({})
|
||||
expect(result.summary).toBeUndefined()
|
||||
expect(result.peerTarget).toBeUndefined()
|
||||
})
|
||||
|
||||
test('all valid options', () => {
|
||||
const result = ContextParamsSchema.parse({
|
||||
summary: true,
|
||||
tokens: 1000,
|
||||
peerTarget: 'peer-1',
|
||||
peerPerspective: 'peer-2',
|
||||
limitToSession: true,
|
||||
representationOptions: {
|
||||
searchQuery: 'preferences',
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.5,
|
||||
includeMostFrequent: true,
|
||||
maxConclusions: 25,
|
||||
},
|
||||
})
|
||||
expect(result.summary).toBe(true)
|
||||
expect(result.peerTarget).toBe('peer-1')
|
||||
expect(result.representationOptions?.searchQuery).toBe('preferences')
|
||||
expect(result.representationOptions?.searchTopK).toBe(10)
|
||||
})
|
||||
|
||||
test('peerPerspective without peerTarget throws', () => {
|
||||
expect(() =>
|
||||
ContextParamsSchema.parse({ peerPerspective: 'peer-2' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// RepresentationOptionsSchema
|
||||
// =============================================================================
|
||||
|
||||
describe('RepresentationOptionsSchema', () => {
|
||||
test('string searchQuery passes', () => {
|
||||
const result = RepresentationOptionsSchema.parse({ searchQuery: 'hello' })
|
||||
expect(result.searchQuery).toBe('hello')
|
||||
})
|
||||
|
||||
test('content-like object searchQuery passes', () => {
|
||||
const result = RepresentationOptionsSchema.parse({
|
||||
searchQuery: {
|
||||
id: 'msg-1',
|
||||
content: 'hello world',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
peer_id: 'peer-1',
|
||||
session_id: 'session-1',
|
||||
token_count: 2,
|
||||
workspace_id: 'ws-1',
|
||||
metadata: {},
|
||||
},
|
||||
})
|
||||
expect(result.searchQuery).toBeDefined()
|
||||
})
|
||||
|
||||
test('whitespace-only string searchQuery throws', () => {
|
||||
expect(() =>
|
||||
RepresentationOptionsSchema.parse({ searchQuery: ' ' })
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('whitespace-only object content searchQuery throws', () => {
|
||||
expect(() =>
|
||||
RepresentationOptionsSchema.parse({
|
||||
searchQuery: { content: ' ' },
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('invalid object searchQuery throws', () => {
|
||||
expect(() =>
|
||||
RepresentationOptionsSchema.parse({
|
||||
searchQuery: { foo: 'bar' },
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('numeric searchQuery throws', () => {
|
||||
expect(() =>
|
||||
RepresentationOptionsSchema.parse({
|
||||
searchQuery: 42,
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('unknown field throws (strict)', () => {
|
||||
expect(() =>
|
||||
RepresentationOptionsSchema.parse({
|
||||
searchQuery: 'hello',
|
||||
typo: true,
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// FileUploadSchema
|
||||
// =============================================================================
|
||||
|
||||
describe('FileUploadSchema', () => {
|
||||
test('Blob upload passes', () => {
|
||||
const result = FileUploadSchema.parse({
|
||||
file: new Blob(['hello'], { type: 'text/plain' }),
|
||||
peer: 'peer-1',
|
||||
})
|
||||
|
||||
expect(result.file).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
test('custom upload object passes', () => {
|
||||
const result = FileUploadSchema.parse({
|
||||
file: {
|
||||
filename: 'test.txt',
|
||||
content: new TextEncoder().encode('hello'),
|
||||
content_type: 'text/plain',
|
||||
},
|
||||
peer: 'peer-1',
|
||||
})
|
||||
|
||||
expect(result.file).toBeDefined()
|
||||
})
|
||||
|
||||
test('custom upload object accepts Buffer content', () => {
|
||||
const result = FileUploadSchema.parse({
|
||||
file: {
|
||||
filename: 'test.txt',
|
||||
content: Buffer.from('hello'),
|
||||
content_type: 'text/plain',
|
||||
},
|
||||
peer: 'peer-1',
|
||||
})
|
||||
|
||||
expect(result.file).toBeDefined()
|
||||
})
|
||||
|
||||
test('top-level Uint8Array throws', () => {
|
||||
expect(() =>
|
||||
FileUploadSchema.parse({
|
||||
file: new Uint8Array([1, 2, 3]),
|
||||
peer: 'peer-1',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('top-level Buffer throws', () => {
|
||||
expect(() =>
|
||||
FileUploadSchema.parse({
|
||||
file: Buffer.from('hello'),
|
||||
peer: 'peer-1',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
|
||||
test('arbitrary object throws', () => {
|
||||
expect(() =>
|
||||
FileUploadSchema.parse({
|
||||
file: { filename: 'test.txt' },
|
||||
peer: 'peer-1',
|
||||
})
|
||||
).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
|
@ -5,12 +5,12 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.2",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.0.1",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -18,12 +18,12 @@
|
|||
"test": "echo 'Error: Tests must be run from the monorepo root via pytest. See tests/README.md' && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.2",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.0.1",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
type HonchoConfig,
|
||||
HonchoConfigSchema,
|
||||
LimitSchema,
|
||||
normalizeListOptions,
|
||||
type PeerConfig,
|
||||
PeerConfigSchema,
|
||||
PeerIdSchema,
|
||||
|
|
@ -279,13 +280,18 @@ export class Honcho {
|
|||
filters?: Record<string, unknown>
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<PageResponse<PeerResponse>> {
|
||||
return this._http.post<PageResponse<PeerResponse>>(
|
||||
`/${API_VERSION}/workspaces/${workspaceId}/peers/list`,
|
||||
{
|
||||
body: { filters: params?.filters },
|
||||
query: { page: params?.page, size: params?.size },
|
||||
query: {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
reverse: params?.reverse ? 'true' : undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -310,13 +316,18 @@ export class Honcho {
|
|||
filters?: Record<string, unknown>
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<PageResponse<SessionResponse>> {
|
||||
return this._http.post<PageResponse<SessionResponse>>(
|
||||
`/${API_VERSION}/workspaces/${workspaceId}/sessions/list`,
|
||||
{
|
||||
body: { filters: params?.filters },
|
||||
query: { page: params?.page, size: params?.size },
|
||||
query: {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
reverse: params?.reverse ? 'true' : undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -381,29 +392,19 @@ export class Honcho {
|
|||
? PeerConfigSchema.parse(options.configuration)
|
||||
: undefined
|
||||
|
||||
if (validatedConfiguration || validatedMetadata) {
|
||||
const peerData = await this._getOrCreatePeer(this.workspaceId, {
|
||||
id: validatedId,
|
||||
configuration: peerConfigToApi(validatedConfiguration),
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
return new Peer(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._http,
|
||||
peerData.metadata ?? undefined,
|
||||
peerConfigFromApi(peerData.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace()
|
||||
)
|
||||
}
|
||||
|
||||
const peerData = await this._getOrCreatePeer(this.workspaceId, {
|
||||
id: validatedId,
|
||||
configuration: peerConfigToApi(validatedConfiguration),
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
return new Peer(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._http,
|
||||
undefined,
|
||||
undefined,
|
||||
() => this._ensureWorkspace()
|
||||
peerData.metadata ?? undefined,
|
||||
peerConfigFromApi(peerData.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace(),
|
||||
peerData.created_at
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -413,14 +414,37 @@ export class Honcho {
|
|||
* Makes an API call to retrieve all peers that have been created or used
|
||||
* within the current workspace. Returns a paginated result.
|
||||
*
|
||||
* @param filters - Optional filter criteria for peers. See [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @param options - Either a legacy raw filter object or an options object with
|
||||
* `filters`, `page`, `size`, and `reverse`. See
|
||||
* [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @returns Promise resolving to a Page of Peer objects representing all peers in the workspace
|
||||
*/
|
||||
async peers(filters?: Filters): Promise<Page<Peer, PeerResponse>> {
|
||||
async peers(
|
||||
options?:
|
||||
| Filters
|
||||
| {
|
||||
filters?: Filters
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<Page<Peer, PeerResponse>> {
|
||||
await this._ensureWorkspace()
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const normalizedOptions = normalizeListOptions(options, [
|
||||
'filters',
|
||||
'page',
|
||||
'size',
|
||||
'reverse',
|
||||
])
|
||||
const validatedFilter = normalizedOptions.filters
|
||||
? FilterSchema.parse(normalizedOptions.filters)
|
||||
: undefined
|
||||
const reverse = normalizedOptions.reverse
|
||||
const peersPage = await this._listPeers(this.workspaceId, {
|
||||
filters: validatedFilter,
|
||||
page: normalizedOptions.page,
|
||||
size: normalizedOptions.size,
|
||||
reverse,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
|
|
@ -431,6 +455,7 @@ export class Honcho {
|
|||
filters: validatedFilter,
|
||||
page,
|
||||
size,
|
||||
reverse,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +468,8 @@ export class Honcho {
|
|||
this._http,
|
||||
peer.metadata ?? undefined,
|
||||
peerConfigFromApi(peer.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace()
|
||||
() => this._ensureWorkspace(),
|
||||
peer.created_at
|
||||
),
|
||||
fetchNextPage
|
||||
)
|
||||
|
|
@ -486,29 +512,20 @@ export class Honcho {
|
|||
? SessionConfigSchema.parse(options.configuration)
|
||||
: undefined
|
||||
|
||||
if (validatedConfiguration || validatedMetadata) {
|
||||
const sessionData = await this._getOrCreateSession(this.workspaceId, {
|
||||
id: validatedId,
|
||||
configuration: validatedConfiguration,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
return new Session(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._http,
|
||||
sessionData.metadata ?? undefined,
|
||||
sessionConfigFromApi(sessionData.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace()
|
||||
)
|
||||
}
|
||||
|
||||
const sessionData = await this._getOrCreateSession(this.workspaceId, {
|
||||
id: validatedId,
|
||||
configuration: validatedConfiguration,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
return new Session(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._http,
|
||||
undefined,
|
||||
undefined,
|
||||
() => this._ensureWorkspace()
|
||||
sessionData.metadata ?? undefined,
|
||||
sessionConfigFromApi(sessionData.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace(),
|
||||
sessionData.created_at,
|
||||
sessionData.is_active
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -518,15 +535,38 @@ export class Honcho {
|
|||
* Makes an API call to retrieve all sessions that have been created within
|
||||
* the current workspace.
|
||||
*
|
||||
* @param filters - Optional filter criteria for sessions. See [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @param options - Either a legacy raw filter object or an options object with
|
||||
* `filters`, `page`, `size`, and `reverse`. See
|
||||
* [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @returns Promise resolving to a Page of Session objects representing all sessions
|
||||
* in the workspace. Returns an empty page if no sessions exist
|
||||
*/
|
||||
async sessions(filters?: Filters): Promise<Page<Session, SessionResponse>> {
|
||||
async sessions(
|
||||
options?:
|
||||
| Filters
|
||||
| {
|
||||
filters?: Filters
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<Page<Session, SessionResponse>> {
|
||||
await this._ensureWorkspace()
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const normalizedOptions = normalizeListOptions(options, [
|
||||
'filters',
|
||||
'page',
|
||||
'size',
|
||||
'reverse',
|
||||
])
|
||||
const validatedFilter = normalizedOptions.filters
|
||||
? FilterSchema.parse(normalizedOptions.filters)
|
||||
: undefined
|
||||
const reverse = normalizedOptions.reverse
|
||||
const sessionsPage = await this._listSessions(this.workspaceId, {
|
||||
filters: validatedFilter,
|
||||
page: normalizedOptions.page,
|
||||
size: normalizedOptions.size,
|
||||
reverse,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
|
|
@ -537,6 +577,7 @@ export class Honcho {
|
|||
filters: validatedFilter,
|
||||
page,
|
||||
size,
|
||||
reverse,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -549,7 +590,9 @@ export class Honcho {
|
|||
this._http,
|
||||
session.metadata ?? undefined,
|
||||
sessionConfigFromApi(session.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace()
|
||||
() => this._ensureWorkspace(),
|
||||
session.created_at,
|
||||
session.is_active
|
||||
),
|
||||
fetchNextPage
|
||||
)
|
||||
|
|
@ -647,16 +690,33 @@ export class Honcho {
|
|||
* Makes an API call to retrieve all workspace IDs that the authenticated
|
||||
* user has access to.
|
||||
*
|
||||
* @param filters - Optional filter criteria for workspaces. See [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @param options - Either a legacy raw filter object or an options object with
|
||||
* `filters`, `page`, and `size`. See
|
||||
* [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @returns Promise resolving to a Page of workspace ID strings. Returns an empty
|
||||
* page if no workspaces are accessible or none exist
|
||||
*/
|
||||
async workspaces(
|
||||
filters?: Filters
|
||||
options?:
|
||||
| Filters
|
||||
| {
|
||||
filters?: Filters
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
): Promise<Page<string, WorkspaceResponse>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const normalizedOptions = normalizeListOptions(options, [
|
||||
'filters',
|
||||
'page',
|
||||
'size',
|
||||
])
|
||||
const validatedFilter = normalizedOptions.filters
|
||||
? FilterSchema.parse(normalizedOptions.filters)
|
||||
: undefined
|
||||
const workspacesPage = await this._listWorkspaces({
|
||||
filters: validatedFilter,
|
||||
page: normalizedOptions.page,
|
||||
size: normalizedOptions.size,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
RepresentationOptions,
|
||||
RepresentationResponse,
|
||||
} from './types/api'
|
||||
import { normalizeSearchQuery, RepresentationOptionsSchema } from './validation'
|
||||
|
||||
/**
|
||||
* Parameters for creating a conclusion.
|
||||
|
|
@ -101,13 +102,18 @@ export class ConclusionScope {
|
|||
filters?: Record<string, unknown>
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}): Promise<PageResponse<ConclusionResponse>> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.post<PageResponse<ConclusionResponse>>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/conclusions/list`,
|
||||
{
|
||||
body: { filters: params.filters },
|
||||
query: { page: params.page, size: params.size },
|
||||
query: {
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
reverse: params.reverse ? 'true' : undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -182,6 +188,7 @@ export class ConclusionScope {
|
|||
page?: number
|
||||
size?: number
|
||||
session?: string | Session
|
||||
reverse?: boolean
|
||||
}): Promise<Page<Conclusion, ConclusionResponse>> {
|
||||
const resolvedSessionId = options?.session
|
||||
? typeof options.session === 'string'
|
||||
|
|
@ -195,18 +202,20 @@ export class ConclusionScope {
|
|||
if (resolvedSessionId) {
|
||||
filters.session_id = resolvedSessionId
|
||||
}
|
||||
const reverse = options?.reverse
|
||||
|
||||
const response = await this._list({
|
||||
filters,
|
||||
page: options?.page ?? 1,
|
||||
size: options?.size ?? 50,
|
||||
reverse,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
page: number,
|
||||
size: number
|
||||
): Promise<PageResponse<ConclusionResponse>> => {
|
||||
return this._list({ filters, page, size })
|
||||
return this._list({ filters, page, size, reverse })
|
||||
}
|
||||
|
||||
return new Page(
|
||||
|
|
@ -277,13 +286,22 @@ export class ConclusionScope {
|
|||
* Get the computed representation for this scope.
|
||||
*/
|
||||
async representation(options?: RepresentationOptions): Promise<string> {
|
||||
const searchQuery = normalizeSearchQuery(options?.searchQuery)
|
||||
const validatedOptions = RepresentationOptionsSchema.parse({
|
||||
searchQuery,
|
||||
searchTopK: options?.searchTopK,
|
||||
searchMaxDistance: options?.searchMaxDistance,
|
||||
includeMostFrequent: options?.includeMostFrequent,
|
||||
maxConclusions: options?.maxConclusions,
|
||||
})
|
||||
|
||||
const response = await this._getRepresentation(this.observer, {
|
||||
target: this.observed,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_frequent: options?.includeMostFrequent,
|
||||
max_conclusions: options?.maxConclusions,
|
||||
search_query: searchQuery,
|
||||
search_top_k: validatedOptions.searchTopK,
|
||||
search_max_distance: validatedOptions.searchMaxDistance,
|
||||
include_most_frequent: validatedOptions.includeMostFrequent,
|
||||
max_conclusions: validatedOptions.maxConclusions,
|
||||
})
|
||||
return response.representation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import {
|
|||
MessageConfigurationSchema,
|
||||
MessageContentSchema,
|
||||
MessageMetadataSchema,
|
||||
normalizeListOptions,
|
||||
normalizeSearchQuery,
|
||||
PeerCardContentSchema,
|
||||
type PeerConfig,
|
||||
PeerConfigSchema,
|
||||
|
|
@ -35,7 +37,9 @@ import {
|
|||
PeerMetadataSchema,
|
||||
peerConfigFromApi,
|
||||
peerConfigToApi,
|
||||
RepresentationOptionsSchema,
|
||||
SearchQuerySchema,
|
||||
sessionConfigFromApi,
|
||||
} from './validation'
|
||||
|
||||
/**
|
||||
|
|
@ -120,6 +124,7 @@ export class Peer {
|
|||
* Private cached configuration for this peer.
|
||||
*/
|
||||
private _configuration?: PeerConfig
|
||||
private _createdAt?: string
|
||||
|
||||
/**
|
||||
* Cached metadata for this peer. May be stale if the peer
|
||||
|
|
@ -143,6 +148,13 @@ export class Peer {
|
|||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp when this peer was created. Only available if fetched from the API.
|
||||
*/
|
||||
get createdAt(): string | undefined {
|
||||
return this._createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Peer. **Do not call this directly, use the client.peer() method instead.**
|
||||
*
|
||||
|
|
@ -158,7 +170,8 @@ export class Peer {
|
|||
http: HonchoHTTPClient,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: PeerConfig,
|
||||
ensureWorkspace: () => Promise<void> = async () => undefined
|
||||
ensureWorkspace: () => Promise<void> = async () => undefined,
|
||||
createdAt?: string
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
|
|
@ -166,6 +179,7 @@ export class Peer {
|
|||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
this._ensureWorkspace = ensureWorkspace
|
||||
this._createdAt = createdAt
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -199,13 +213,18 @@ export class Peer {
|
|||
filters?: Record<string, unknown>
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}): Promise<PageResponse<SessionResponse>> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.post<PageResponse<SessionResponse>>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/peers/${this.id}/sessions`,
|
||||
{
|
||||
body: { filters: params?.filters },
|
||||
query: { page: params?.page, size: params?.size },
|
||||
query: {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
reverse: params?.reverse ? 'true' : undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -459,19 +478,49 @@ export class Peer {
|
|||
* Makes an API call to retrieve all sessions where this peer is an active participant.
|
||||
* Sessions are created when peers are added to them or send messages to them.
|
||||
*
|
||||
* @param filters - Optional filter criteria for sessions. See [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @param options - Either a legacy raw filter object or an options object with
|
||||
* `filters`, `page`, `size`, and `reverse`. See
|
||||
* [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @returns Promise resolving to a paginated list of Session objects this peer belongs to.
|
||||
* Returns an empty list if the peer is not a member of any sessions
|
||||
*/
|
||||
async sessions(filters?: Filters): Promise<Page<Session, SessionResponse>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const sessionsPage = await this._listSessions({ filters: validatedFilter })
|
||||
async sessions(
|
||||
options?:
|
||||
| Filters
|
||||
| {
|
||||
filters?: Filters
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<Page<Session, SessionResponse>> {
|
||||
const normalizedOptions = normalizeListOptions(options, [
|
||||
'filters',
|
||||
'page',
|
||||
'size',
|
||||
'reverse',
|
||||
])
|
||||
const validatedFilter = normalizedOptions.filters
|
||||
? FilterSchema.parse(normalizedOptions.filters)
|
||||
: undefined
|
||||
const reverse = normalizedOptions.reverse
|
||||
const sessionsPage = await this._listSessions({
|
||||
filters: validatedFilter,
|
||||
page: normalizedOptions.page,
|
||||
size: normalizedOptions.size,
|
||||
reverse,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
page: number,
|
||||
size: number
|
||||
): Promise<PageResponse<SessionResponse>> => {
|
||||
return this._listSessions({ filters: validatedFilter, page, size })
|
||||
return this._listSessions({
|
||||
filters: validatedFilter,
|
||||
page,
|
||||
size,
|
||||
reverse,
|
||||
})
|
||||
}
|
||||
|
||||
return new Page(
|
||||
|
|
@ -482,7 +531,10 @@ export class Peer {
|
|||
this.workspaceId,
|
||||
this._http,
|
||||
session.metadata ?? undefined,
|
||||
session.configuration ?? undefined
|
||||
sessionConfigFromApi(session.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace(),
|
||||
session.created_at,
|
||||
session.is_active
|
||||
),
|
||||
fetchNextPage
|
||||
)
|
||||
|
|
@ -559,6 +611,7 @@ export class Peer {
|
|||
async getMetadata(): Promise<Record<string, unknown>> {
|
||||
const peer = await this._getOrCreate({ id: this.id })
|
||||
this._metadata = peer.metadata || {}
|
||||
this._createdAt = peer.created_at
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
|
|
@ -590,6 +643,7 @@ export class Peer {
|
|||
async getConfiguration(): Promise<PeerConfig> {
|
||||
const peer = await this._getOrCreate({ id: this.id })
|
||||
this._configuration = peerConfigFromApi(peer.configuration) || {}
|
||||
this._createdAt = peer.created_at
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
|
|
@ -621,6 +675,7 @@ export class Peer {
|
|||
const peer = await this._getOrCreate({ id: this.id })
|
||||
this._metadata = peer.metadata || {}
|
||||
this._configuration = peerConfigFromApi(peer.configuration) || {}
|
||||
this._createdAt = peer.created_at
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -741,17 +796,18 @@ export class Peer {
|
|||
async representation(options?: {
|
||||
session?: string | Session
|
||||
target?: string | Peer
|
||||
searchQuery?: string
|
||||
searchQuery?: string | Message
|
||||
searchTopK?: number
|
||||
searchMaxDistance?: number
|
||||
includeMostFrequent?: boolean
|
||||
maxConclusions?: number
|
||||
}): Promise<string> {
|
||||
const searchQuery = normalizeSearchQuery(options?.searchQuery)
|
||||
const getRepresentationParams = PeerGetRepresentationParamsSchema.parse({
|
||||
session: options?.session,
|
||||
target: options?.target,
|
||||
options: {
|
||||
searchQuery: options?.searchQuery,
|
||||
searchQuery,
|
||||
searchTopK: options?.searchTopK,
|
||||
searchMaxDistance: options?.searchMaxDistance,
|
||||
includeMostFrequent: options?.includeMostFrequent,
|
||||
|
|
@ -772,7 +828,7 @@ export class Peer {
|
|||
const response = await this._getRepresentation({
|
||||
session_id: sessionId,
|
||||
target: targetId,
|
||||
search_query: getRepresentationParams.options?.searchQuery,
|
||||
search_query: searchQuery,
|
||||
search_top_k: getRepresentationParams.options?.searchTopK,
|
||||
search_max_distance: getRepresentationParams.options?.searchMaxDistance,
|
||||
include_most_frequent:
|
||||
|
|
@ -827,14 +883,25 @@ export class Peer {
|
|||
? options.target
|
||||
: options.target.id
|
||||
: undefined
|
||||
const searchQuery =
|
||||
options?.searchQuery === undefined
|
||||
? undefined
|
||||
: SearchQuerySchema.parse(options.searchQuery)
|
||||
const validatedOptions = RepresentationOptionsSchema.parse({
|
||||
searchQuery,
|
||||
searchTopK: options?.searchTopK,
|
||||
searchMaxDistance: options?.searchMaxDistance,
|
||||
includeMostFrequent: options?.includeMostFrequent,
|
||||
maxConclusions: options?.maxConclusions,
|
||||
})
|
||||
|
||||
const response = await this._getContext({
|
||||
target: targetId,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_frequent: options?.includeMostFrequent,
|
||||
max_conclusions: options?.maxConclusions,
|
||||
search_query: searchQuery,
|
||||
search_top_k: validatedOptions.searchTopK,
|
||||
search_max_distance: validatedOptions.searchMaxDistance,
|
||||
include_most_frequent: validatedOptions.includeMostFrequent,
|
||||
max_conclusions: validatedOptions.maxConclusions,
|
||||
})
|
||||
|
||||
return PeerContext.fromApiResponse(response)
|
||||
|
|
|
|||
|
|
@ -28,10 +28,14 @@ import {
|
|||
type MessageAddition,
|
||||
MessageAdditionToApiSchema,
|
||||
MessageMetadataSchema,
|
||||
messageConfigToApi,
|
||||
normalizeListOptions,
|
||||
normalizeSearchQuery,
|
||||
type PeerAddition,
|
||||
PeerAdditionToApiSchema,
|
||||
type PeerRemoval,
|
||||
PeerRemovalSchema,
|
||||
peerConfigFromApi,
|
||||
type QueueStatusOptions,
|
||||
SearchQuerySchema,
|
||||
type SessionConfig,
|
||||
|
|
@ -79,6 +83,8 @@ export class Session {
|
|||
private _http: HonchoHTTPClient
|
||||
private _metadata?: Record<string, unknown>
|
||||
private _configuration?: SessionConfig
|
||||
private _createdAt?: string
|
||||
private _isActive?: boolean
|
||||
private _ensureWorkspace: () => Promise<void>
|
||||
|
||||
/**
|
||||
|
|
@ -103,6 +109,20 @@ export class Session {
|
|||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp when this session was created. Only available if fetched from the API.
|
||||
*/
|
||||
get createdAt(): string | undefined {
|
||||
return this._createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this session is active. Only available if fetched from the API.
|
||||
*/
|
||||
get isActive(): boolean | undefined {
|
||||
return this._isActive
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Session. **Do not call this directly, use the client.session() method instead.**
|
||||
*
|
||||
|
|
@ -118,7 +138,9 @@ export class Session {
|
|||
http: HonchoHTTPClient,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: SessionConfig,
|
||||
ensureWorkspace: () => Promise<void> = async () => undefined
|
||||
ensureWorkspace: () => Promise<void> = async () => undefined,
|
||||
createdAt?: string,
|
||||
isActive?: boolean
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
|
|
@ -126,6 +148,15 @@ export class Session {
|
|||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
this._ensureWorkspace = ensureWorkspace
|
||||
this._createdAt = createdAt
|
||||
this._isActive = isActive
|
||||
}
|
||||
|
||||
private _applySessionResponse(session: SessionResponse): void {
|
||||
this._metadata = session.metadata || {}
|
||||
this._configuration = sessionConfigFromApi(session.configuration) || {}
|
||||
this._createdAt = session.created_at
|
||||
this._isActive = session.is_active
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -305,13 +336,18 @@ export class Session {
|
|||
filters?: Record<string, unknown>
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}): Promise<PageResponse<MessageResponse>> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.post<PageResponse<MessageResponse>>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/sessions/${this.id}/messages/list`,
|
||||
{
|
||||
body: { filters: params?.filters },
|
||||
query: { page: params?.page, size: params?.size },
|
||||
query: {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
reverse: params?.reverse ? 'true' : undefined,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -358,6 +394,13 @@ export class Session {
|
|||
)
|
||||
}
|
||||
|
||||
private async _getMessage(messageId: string): Promise<MessageResponse> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.get<MessageResponse>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/sessions/${this.id}/messages/${messageId}`
|
||||
)
|
||||
}
|
||||
|
||||
private async _updateMessage(
|
||||
messageId: string,
|
||||
params: { metadata: Record<string, unknown> }
|
||||
|
|
@ -450,9 +493,10 @@ export class Session {
|
|||
peer.id,
|
||||
this.workspaceId,
|
||||
this._http,
|
||||
undefined,
|
||||
undefined,
|
||||
() => this._ensureWorkspace()
|
||||
peer.metadata ?? undefined,
|
||||
peerConfigFromApi(peer.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace(),
|
||||
peer.created_at
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -539,19 +583,48 @@ export class Session {
|
|||
*
|
||||
* Makes an API call to retrieve messages in the session, with optional filtering.
|
||||
*
|
||||
* @param filters - Optional filter criteria for messages. See
|
||||
* @param options - Either a legacy raw filter object or an options object with
|
||||
* `filters`, `page`, `size`, and `reverse`. See
|
||||
* [search filters documentation](https://docs.honcho.dev/v3/documentation/core-concepts/features/using-filters).
|
||||
* @returns Promise resolving to a paginated Page of Message objects
|
||||
*/
|
||||
async messages(filters?: Filters): Promise<Page<Message, MessageResponse>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const messagesPage = await this._listMessages({ filters: validatedFilter })
|
||||
async messages(
|
||||
options?:
|
||||
| Filters
|
||||
| {
|
||||
filters?: Filters
|
||||
page?: number
|
||||
size?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
): Promise<Page<Message, MessageResponse>> {
|
||||
const normalizedOptions = normalizeListOptions(options, [
|
||||
'filters',
|
||||
'page',
|
||||
'size',
|
||||
'reverse',
|
||||
])
|
||||
const validatedFilter = normalizedOptions.filters
|
||||
? FilterSchema.parse(normalizedOptions.filters)
|
||||
: undefined
|
||||
const reverse = normalizedOptions.reverse
|
||||
const messagesPage = await this._listMessages({
|
||||
filters: validatedFilter,
|
||||
page: normalizedOptions.page,
|
||||
size: normalizedOptions.size,
|
||||
reverse,
|
||||
})
|
||||
|
||||
const fetchNextPage = async (
|
||||
page: number,
|
||||
size: number
|
||||
): Promise<PageResponse<MessageResponse>> => {
|
||||
return this._listMessages({ filters: validatedFilter, page, size })
|
||||
return this._listMessages({
|
||||
filters: validatedFilter,
|
||||
page,
|
||||
size,
|
||||
reverse,
|
||||
})
|
||||
}
|
||||
|
||||
return new Page(messagesPage, Message.fromApiResponse, fetchNextPage)
|
||||
|
|
@ -568,8 +641,8 @@ export class Session {
|
|||
*/
|
||||
async getMetadata(): Promise<Record<string, unknown>> {
|
||||
const session = await this._getOrCreate({ id: this.id })
|
||||
this._metadata = session.metadata || {}
|
||||
return this._metadata
|
||||
this._applySessionResponse(session)
|
||||
return this._metadata ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -599,8 +672,8 @@ export class Session {
|
|||
*/
|
||||
async getConfiguration(): Promise<SessionConfig> {
|
||||
const session = await this._getOrCreate({ id: this.id })
|
||||
this._configuration = sessionConfigFromApi(session.configuration) || {}
|
||||
return this._configuration
|
||||
this._applySessionResponse(session)
|
||||
return this._configuration ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -627,8 +700,7 @@ export class Session {
|
|||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const session = await this._getOrCreate({ id: this.id })
|
||||
this._metadata = session.metadata || {}
|
||||
this._configuration = sessionConfigFromApi(session.configuration) || {}
|
||||
this._applySessionResponse(session)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -662,7 +734,9 @@ export class Session {
|
|||
this._http,
|
||||
clonedSessionData.metadata ?? undefined,
|
||||
sessionConfigFromApi(clonedSessionData.configuration) ?? undefined,
|
||||
() => this._ensureWorkspace()
|
||||
() => this._ensureWorkspace(),
|
||||
clonedSessionData.created_at,
|
||||
clonedSessionData.is_active
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -677,10 +751,9 @@ export class Session {
|
|||
* @param options.summary - Whether to include a summary of earlier messages
|
||||
* @param options.tokens - Target token count for the context window
|
||||
* @param options.peerTarget - The peer to get representation for
|
||||
* @param options.lastUserMessage - Message text (string) or Message object whose content will be used for semantic search
|
||||
* @param options.peerPerspective - The peer whose perspective to use for representation
|
||||
* @param options.limitToSession - Whether to limit representation to this session only
|
||||
* @param options.representationOptions - Options for representation retrieval
|
||||
* @param options.representationOptions - Options for representation retrieval (searchQuery, searchTopK, etc.)
|
||||
* @returns Promise resolving to a SessionContext with messages, summary, and representation
|
||||
*
|
||||
* @example
|
||||
|
|
@ -699,7 +772,6 @@ export class Session {
|
|||
summary?: boolean
|
||||
tokens?: number
|
||||
peerTarget?: string | Peer
|
||||
searchQuery?: string | Message
|
||||
peerPerspective?: string | Peer
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
|
|
@ -713,30 +785,29 @@ export class Session {
|
|||
typeof opts.peerPerspective === 'object'
|
||||
? opts.peerPerspective.id
|
||||
: opts.peerPerspective
|
||||
const searchQueryText =
|
||||
typeof opts.searchQuery === 'string'
|
||||
? opts.searchQuery
|
||||
: opts.searchQuery?.content
|
||||
|
||||
const searchQuery = normalizeSearchQuery(
|
||||
opts.representationOptions?.searchQuery
|
||||
)
|
||||
|
||||
const contextParams = ContextParamsSchema.parse({
|
||||
summary: opts.summary,
|
||||
tokens: opts.tokens,
|
||||
peerTarget: peerTargetId,
|
||||
searchQuery: searchQueryText,
|
||||
peerPerspective: peerPerspectiveId,
|
||||
limitToSession: opts.limitToSession,
|
||||
representationOptions: opts.representationOptions,
|
||||
representationOptions: opts.representationOptions
|
||||
? {
|
||||
...opts.representationOptions,
|
||||
searchQuery,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const searchQueryParsed =
|
||||
typeof contextParams.searchQuery === 'string'
|
||||
? contextParams.searchQuery
|
||||
: contextParams.searchQuery?.content
|
||||
|
||||
const context = await this._getContext({
|
||||
tokens: contextParams.tokens,
|
||||
summary: contextParams.summary,
|
||||
search_query: searchQueryParsed,
|
||||
search_query: searchQuery,
|
||||
peer_target: contextParams.peerTarget,
|
||||
peer_perspective: contextParams.peerPerspective,
|
||||
limit_to_session: contextParams.limitToSession,
|
||||
|
|
@ -895,14 +966,15 @@ export class Session {
|
|||
})
|
||||
|
||||
const formData = new FormData()
|
||||
const uploadFile = uploadParams.file
|
||||
|
||||
if (file instanceof File || file instanceof Blob) {
|
||||
formData.append('file', file)
|
||||
if (uploadFile instanceof Blob) {
|
||||
formData.append('file', uploadFile)
|
||||
} else {
|
||||
// Convert to Uint8Array for Blob compatibility
|
||||
const content = new Uint8Array(file.content)
|
||||
const blob = new Blob([content], { type: file.content_type })
|
||||
formData.append('file', blob, file.filename)
|
||||
const content = new Uint8Array(uploadFile.content)
|
||||
const blob = new Blob([content], { type: uploadFile.content_type })
|
||||
formData.append('file', blob, uploadFile.filename)
|
||||
}
|
||||
|
||||
formData.append('peer_id', resolvedPeerId)
|
||||
|
|
@ -913,10 +985,8 @@ export class Session {
|
|||
uploadParams.configuration !== undefined &&
|
||||
uploadParams.configuration !== null
|
||||
) {
|
||||
formData.append(
|
||||
'configuration',
|
||||
JSON.stringify(uploadParams.configuration)
|
||||
)
|
||||
const apiConfiguration = messageConfigToApi(uploadParams.configuration)
|
||||
formData.append('configuration', JSON.stringify(apiConfiguration))
|
||||
}
|
||||
if (
|
||||
uploadParams.createdAt !== undefined &&
|
||||
|
|
@ -949,18 +1019,19 @@ export class Session {
|
|||
peer: string | Peer,
|
||||
options?: {
|
||||
target?: string | Peer
|
||||
searchQuery?: string
|
||||
searchQuery?: string | Message
|
||||
searchTopK?: number
|
||||
searchMaxDistance?: number
|
||||
includeMostFrequent?: boolean
|
||||
maxConclusions?: number
|
||||
}
|
||||
): Promise<string> {
|
||||
const searchQuery = normalizeSearchQuery(options?.searchQuery)
|
||||
const getRepresentationParams = GetRepresentationParamsSchema.parse({
|
||||
peer,
|
||||
target: options?.target,
|
||||
options: {
|
||||
searchQuery: options?.searchQuery,
|
||||
searchQuery,
|
||||
searchTopK: options?.searchTopK,
|
||||
searchMaxDistance: options?.searchMaxDistance,
|
||||
includeMostFrequent: options?.includeMostFrequent,
|
||||
|
|
@ -980,7 +1051,7 @@ export class Session {
|
|||
const response = await this._getRepresentation(peerId, {
|
||||
session_id: this.id,
|
||||
target: targetId,
|
||||
search_query: getRepresentationParams.options?.searchQuery,
|
||||
search_query: searchQuery,
|
||||
search_top_k: getRepresentationParams.options?.searchTopK,
|
||||
search_max_distance: getRepresentationParams.options?.searchMaxDistance,
|
||||
include_most_frequent:
|
||||
|
|
@ -990,6 +1061,17 @@ export class Session {
|
|||
return response.representation
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single message by ID from this session.
|
||||
*
|
||||
* @param messageId - The ID of the message to retrieve
|
||||
* @returns Promise resolving to the Message object
|
||||
*/
|
||||
async getMessage(messageId: string): Promise<Message> {
|
||||
const response = await this._getMessage(messageId)
|
||||
return Message.fromApiResponse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metadata of a message in this session.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -285,8 +285,9 @@ export interface RepresentationResponse {
|
|||
export interface RepresentationOptions {
|
||||
/**
|
||||
* Semantic search query to filter relevant conclusions.
|
||||
* Accepts a string or any object with a `content` property (e.g., a Message).
|
||||
*/
|
||||
searchQuery?: string
|
||||
searchQuery?: string | { content: string }
|
||||
|
||||
/**
|
||||
* Number of semantically relevant conclusions to return.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod'
|
||||
import type { MessageResponse } from './types/api'
|
||||
|
||||
/**
|
||||
* Validation schemas for the Honcho TypeScript SDK.
|
||||
|
|
@ -23,23 +22,28 @@ export const WorkspaceIdSchema = z
|
|||
/**
|
||||
* Schema for Honcho client configuration options.
|
||||
*/
|
||||
export const HonchoConfigSchema = z.object({
|
||||
apiKey: z.string().optional(),
|
||||
environment: z.enum(['local', 'production']).optional(),
|
||||
baseURL: z.url('Base URL must be a valid URL').optional(),
|
||||
workspaceId: WorkspaceIdSchema.optional(),
|
||||
timeout: z.number().positive('Timeout must be a positive number').optional(),
|
||||
maxRetries: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, 'Max retries must be a non-negative integer')
|
||||
.max(3, 'Max retries must be at most 3')
|
||||
.optional(),
|
||||
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
||||
defaultQuery: z
|
||||
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
||||
.optional(),
|
||||
})
|
||||
export const HonchoConfigSchema = z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
environment: z.enum(['local', 'production']).optional(),
|
||||
baseURL: z.url('Base URL must be a valid URL').optional(),
|
||||
workspaceId: WorkspaceIdSchema.optional(),
|
||||
timeout: z
|
||||
.number()
|
||||
.positive('Timeout must be a positive number')
|
||||
.optional(),
|
||||
maxRetries: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0, 'Max retries must be a non-negative integer')
|
||||
.max(3, 'Max retries must be at most 3')
|
||||
.optional(),
|
||||
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
||||
defaultQuery: z
|
||||
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for peer metadata.
|
||||
|
|
@ -49,9 +53,11 @@ export const PeerMetadataSchema = z.record(z.string(), z.unknown())
|
|||
/**
|
||||
* Schema for peer configuration.
|
||||
*/
|
||||
export const PeerConfigSchema = z.object({
|
||||
observeMe: z.boolean().nullable().optional(),
|
||||
})
|
||||
export const PeerConfigSchema = z
|
||||
.object({
|
||||
observeMe: z.boolean().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for peer ID validation.
|
||||
|
|
@ -65,6 +71,11 @@ export const PeerIdSchema = z
|
|||
)
|
||||
.max(100, 'Peer ID can be at most 100 characters')
|
||||
|
||||
/**
|
||||
* Strict helper: peer ID as object.
|
||||
*/
|
||||
const PeerIdObjectSchema = z.object({ id: PeerIdSchema })
|
||||
|
||||
/**
|
||||
* Schema for session metadata.
|
||||
*/
|
||||
|
|
@ -78,48 +89,58 @@ export const SessionMetadataSchema = z.record(z.string(), z.unknown())
|
|||
* Schema for reasoning configuration.
|
||||
* Used in workspace, session, and message configuration.
|
||||
*/
|
||||
export const ReasoningConfigSchema = z.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
customInstructions: z.string().nullable().optional(),
|
||||
})
|
||||
export const ReasoningConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
customInstructions: z.string().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for peer card configuration.
|
||||
* Used in workspace and session configuration.
|
||||
*/
|
||||
export const PeerCardConfigSchema = z.object({
|
||||
use: z.boolean().nullable().optional(),
|
||||
create: z.boolean().nullable().optional(),
|
||||
})
|
||||
export const PeerCardConfigSchema = z
|
||||
.object({
|
||||
use: z.boolean().nullable().optional(),
|
||||
create: z.boolean().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for summary configuration.
|
||||
* Used in workspace and session configuration.
|
||||
*/
|
||||
export const SummaryConfigSchema = z.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
messagesPerShortSummary: z.number().int().min(10).nullable().optional(),
|
||||
messagesPerLongSummary: z.number().int().min(20).nullable().optional(),
|
||||
})
|
||||
export const SummaryConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
messagesPerShortSummary: z.number().int().min(10).nullable().optional(),
|
||||
messagesPerLongSummary: z.number().int().min(20).nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for dream configuration.
|
||||
* Used in workspace and session configuration.
|
||||
*/
|
||||
export const DreamConfigSchema = z.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
})
|
||||
export const DreamConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for session configuration.
|
||||
* Includes reasoning, peer card, summary, and dream settings.
|
||||
*/
|
||||
export const SessionConfigSchema = z.object({
|
||||
reasoning: ReasoningConfigSchema.nullable().optional(),
|
||||
peerCard: PeerCardConfigSchema.nullable().optional(),
|
||||
summary: SummaryConfigSchema.nullable().optional(),
|
||||
dream: DreamConfigSchema.nullable().optional(),
|
||||
})
|
||||
export const SessionConfigSchema = z
|
||||
.object({
|
||||
reasoning: ReasoningConfigSchema.nullable().optional(),
|
||||
peerCard: PeerCardConfigSchema.nullable().optional(),
|
||||
summary: SummaryConfigSchema.nullable().optional(),
|
||||
dream: DreamConfigSchema.nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for session ID validation.
|
||||
|
|
@ -133,13 +154,20 @@ export const SessionIdSchema = z
|
|||
)
|
||||
.max(100, 'Session ID can be at most 100 characters')
|
||||
|
||||
/**
|
||||
* Strict helper: session ID as object.
|
||||
*/
|
||||
const SessionIdObjectSchema = z.object({ id: SessionIdSchema })
|
||||
|
||||
/**
|
||||
* Schema for session peer configuration.
|
||||
*/
|
||||
export const SessionPeerConfigSchema = z.object({
|
||||
observeMe: z.boolean().nullable().optional(),
|
||||
observeOthers: z.boolean().nullable().optional(),
|
||||
})
|
||||
export const SessionPeerConfigSchema = z
|
||||
.object({
|
||||
observeMe: z.boolean().nullable().optional(),
|
||||
observeOthers: z.boolean().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for message content.
|
||||
|
|
@ -166,19 +194,22 @@ export const MessageConfigurationSchema = z
|
|||
.object({
|
||||
reasoning: ReasoningConfigSchema.nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
.nullable()
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message input.
|
||||
*/
|
||||
export const MessageInputSchema = z.object({
|
||||
peerId: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: MessageConfigurationSchema,
|
||||
createdAt: z.string().nullable().optional(),
|
||||
})
|
||||
export const MessageInputSchema = z
|
||||
.object({
|
||||
peerId: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: MessageConfigurationSchema,
|
||||
createdAt: z.string().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for search query validation.
|
||||
|
|
@ -191,78 +222,125 @@ export const SearchQuerySchema = z
|
|||
'Search query cannot be only whitespace'
|
||||
)
|
||||
|
||||
/**
|
||||
* Schema for content-like search query objects.
|
||||
* Accepts SDK Message instances and other objects with a valid content field.
|
||||
*/
|
||||
export const SearchQueryObjectSchema = z
|
||||
.object({
|
||||
content: SearchQuerySchema,
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Schema for search query inputs that can be normalized to a string.
|
||||
*/
|
||||
export const SearchQueryLikeSchema = z.union([
|
||||
SearchQuerySchema,
|
||||
SearchQueryObjectSchema,
|
||||
])
|
||||
|
||||
/**
|
||||
* Normalize a supported search query input to plain text.
|
||||
*/
|
||||
export function normalizeSearchQuery(searchQuery: unknown): string | undefined {
|
||||
if (searchQuery === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const validatedSearchQuery = SearchQueryLikeSchema.parse(searchQuery)
|
||||
return typeof validatedSearchQuery === 'string'
|
||||
? validatedSearchQuery
|
||||
: validatedSearchQuery.content
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for filter objects.
|
||||
*/
|
||||
export const FilterSchema = z.record(z.string(), z.unknown()).optional()
|
||||
|
||||
/**
|
||||
* Schema for chat query parameters.
|
||||
* Normalize list-method input so legacy raw filters and the new options object
|
||||
* shape are both accepted.
|
||||
*
|
||||
* Discriminates on the `filters` key: if the input has a `filters` property or
|
||||
* any of the pagination-only keys (`page`, `size`, `reverse`) it is treated as
|
||||
* the new options object. Otherwise it is treated as a legacy raw filter.
|
||||
*/
|
||||
export const ChatQuerySchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
target: z
|
||||
.union([PeerIdSchema, z.object({ id: PeerIdSchema })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
session: z
|
||||
.union([SessionIdSchema, z.object({ id: SessionIdSchema })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
reasoningLevel: z
|
||||
.enum(['minimal', 'low', 'medium', 'high', 'max'])
|
||||
.optional(),
|
||||
})
|
||||
export function normalizeListOptions<T extends { filters?: Filters }>(
|
||||
input: Filters | T | undefined,
|
||||
optionKeys: string[]
|
||||
): T {
|
||||
if (input === undefined) {
|
||||
return {} as T
|
||||
}
|
||||
|
||||
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
||||
return { filters: input as Filters } as T
|
||||
}
|
||||
|
||||
// Pagination-only keys can never appear in a raw filter object
|
||||
const paginationKeys = optionKeys.filter((k) => k !== 'filters')
|
||||
const hasFiltersKey = 'filters' in input
|
||||
const hasPaginationKey = paginationKeys.some((key) => key in input)
|
||||
|
||||
if (hasFiltersKey || hasPaginationKey) {
|
||||
return input as T
|
||||
}
|
||||
|
||||
return { filters: input as Filters } as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for validating Message API responses (snake_case).
|
||||
* Schema for chat query parameters.
|
||||
*/
|
||||
const MessageResponseSchema: z.ZodType<MessageResponse> = z.object({
|
||||
id: z.string(),
|
||||
content: z.string(),
|
||||
created_at: z.string(),
|
||||
peer_id: PeerIdSchema,
|
||||
session_id: SessionIdSchema,
|
||||
token_count: z.number(),
|
||||
workspace_id: WorkspaceIdSchema,
|
||||
metadata: z.record(z.string(), z.unknown()),
|
||||
}) as z.ZodType<MessageResponse>
|
||||
export const ChatQuerySchema = z
|
||||
.object({
|
||||
query: SearchQuerySchema,
|
||||
target: z
|
||||
.union([PeerIdSchema, PeerIdObjectSchema])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
session: z
|
||||
.union([SessionIdSchema, SessionIdObjectSchema])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
reasoningLevel: z
|
||||
.enum(['minimal', 'low', 'medium', 'high', 'max'])
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for representation options.
|
||||
*/
|
||||
export const RepresentationOptionsSchema = z.object({
|
||||
searchQuery: z
|
||||
.string()
|
||||
.min(1, 'searchQuery must be a non-empty string')
|
||||
.refine(
|
||||
(query: string) => query.trim().length > 0,
|
||||
'searchQuery cannot be only whitespace'
|
||||
)
|
||||
.optional(),
|
||||
searchTopK: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'searchTopK must be at least 1')
|
||||
.max(100, 'searchTopK must be at most 100')
|
||||
.optional(),
|
||||
searchMaxDistance: z
|
||||
.number()
|
||||
.min(0.0, 'searchMaxDistance must be at least 0.0')
|
||||
.max(1.0, 'searchMaxDistance must be at most 1.0')
|
||||
.optional(),
|
||||
includeMostFrequent: z.boolean().optional(),
|
||||
maxConclusions: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'maxConclusions must be at least 1')
|
||||
.max(100, 'maxConclusions must be at most 100')
|
||||
.optional(),
|
||||
})
|
||||
export const RepresentationOptionsSchema = z
|
||||
.object({
|
||||
searchQuery: SearchQueryLikeSchema.optional(),
|
||||
searchTopK: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'searchTopK must be at least 1')
|
||||
.max(100, 'searchTopK must be at most 100')
|
||||
.optional(),
|
||||
searchMaxDistance: z
|
||||
.number()
|
||||
.min(0.0, 'searchMaxDistance must be at least 0.0')
|
||||
.max(1.0, 'searchMaxDistance must be at most 1.0')
|
||||
.optional(),
|
||||
includeMostFrequent: z.boolean().optional(),
|
||||
maxConclusions: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'maxConclusions must be at least 1')
|
||||
.max(100, 'maxConclusions must be at most 100')
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for context retrieval parameters.
|
||||
|
|
@ -271,23 +349,18 @@ export const ContextParamsSchema = z
|
|||
.object({
|
||||
summary: z.boolean().optional(),
|
||||
tokens: z.int('Token limit must be an integer').optional(),
|
||||
searchQuery: z
|
||||
.union([
|
||||
z.string().min(1, 'Search query must be a non-empty string'),
|
||||
MessageResponseSchema,
|
||||
])
|
||||
.optional(),
|
||||
peerTarget: PeerIdSchema.optional(),
|
||||
peerPerspective: PeerIdSchema.optional(),
|
||||
limitToSession: z.boolean().optional(),
|
||||
representationOptions: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.searchQuery && !data.peerTarget) {
|
||||
if (data.representationOptions?.searchQuery && !data.peerTarget) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'peerTarget is required when searchQuery is provided',
|
||||
path: ['searchQuery'],
|
||||
path: ['representationOptions', 'searchQuery'],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -303,74 +376,72 @@ export const ContextParamsSchema = z
|
|||
/**
|
||||
* Schema for deriver status options.
|
||||
*/
|
||||
export const QueueStatusOptionsSchema = z.object({
|
||||
observer: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]).optional(),
|
||||
sender: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]).optional(),
|
||||
session: z
|
||||
.union([SessionIdSchema, z.object({ id: SessionIdSchema })])
|
||||
.optional(),
|
||||
timeout: z.number().positive('Timeout must be a positive number').optional(),
|
||||
})
|
||||
export const QueueStatusOptionsSchema = z
|
||||
.object({
|
||||
observer: z.union([PeerIdSchema, PeerIdObjectSchema]).optional(),
|
||||
sender: z.union([PeerIdSchema, PeerIdObjectSchema]).optional(),
|
||||
session: z.union([SessionIdSchema, SessionIdObjectSchema]).optional(),
|
||||
timeout: z
|
||||
.number()
|
||||
.positive('Timeout must be a positive number')
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for file upload parameters.
|
||||
* Supports File objects (browser), Buffer, Uint8Array, and custom uploadable objects.
|
||||
* Supports Blob/File objects and custom uploadable objects with binary content.
|
||||
*/
|
||||
export const FileUploadSchema = z.object({
|
||||
file: z.union([
|
||||
// Browser File object
|
||||
z.instanceof(File),
|
||||
// Node.js Buffer
|
||||
z.instanceof(Buffer),
|
||||
// Uint8Array
|
||||
z.instanceof(Uint8Array),
|
||||
// Custom uploadable object with filename, content, and content_type
|
||||
z.object({
|
||||
filename: z.string().min(1, 'Filename must be a non-empty string'),
|
||||
content: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array)]),
|
||||
content_type: z
|
||||
.string()
|
||||
.min(1, 'Content type must be a non-empty string'),
|
||||
}),
|
||||
// Fallback for any other uploadable type
|
||||
z
|
||||
.any()
|
||||
.refine(
|
||||
(val) => val !== null && val !== undefined,
|
||||
'File must not be null or undefined'
|
||||
),
|
||||
]),
|
||||
peer: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]),
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: z.record(z.string(), z.unknown()).optional(),
|
||||
createdAt: z.string().nullable().optional(),
|
||||
})
|
||||
export const FileUploadSchema = z
|
||||
.object({
|
||||
file: z.union([
|
||||
// Browser/File API objects
|
||||
z.instanceof(Blob),
|
||||
// Custom uploadable object with filename, content, and content_type
|
||||
z
|
||||
.object({
|
||||
filename: z.string().min(1, 'Filename must be a non-empty string'),
|
||||
content: z.instanceof(Uint8Array),
|
||||
content_type: z
|
||||
.string()
|
||||
.min(1, 'Content type must be a non-empty string'),
|
||||
})
|
||||
.strict(),
|
||||
]),
|
||||
peer: z.union([PeerIdSchema, PeerIdObjectSchema]),
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: MessageConfigurationSchema,
|
||||
createdAt: z.string().nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for get representation parameters.
|
||||
*/
|
||||
export const GetRepresentationParamsSchema = z.object({
|
||||
peer: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]),
|
||||
target: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]).optional(),
|
||||
options: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
export const GetRepresentationParamsSchema = z
|
||||
.object({
|
||||
peer: z.union([PeerIdSchema, PeerIdObjectSchema]),
|
||||
target: z.union([PeerIdSchema, PeerIdObjectSchema]).optional(),
|
||||
options: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for peer get representation parameters.
|
||||
*/
|
||||
export const PeerGetRepresentationParamsSchema = z.object({
|
||||
session: z
|
||||
.union([SessionIdSchema, z.object({ id: SessionIdSchema })])
|
||||
.optional(),
|
||||
target: z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]).optional(),
|
||||
options: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
export const PeerGetRepresentationParamsSchema = z
|
||||
.object({
|
||||
session: z.union([SessionIdSchema, SessionIdObjectSchema]).optional(),
|
||||
target: z.union([PeerIdSchema, PeerIdObjectSchema]).optional(),
|
||||
options: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for peer card target parameter.
|
||||
*/
|
||||
export const CardTargetSchema = z
|
||||
.union([PeerIdSchema, z.object({ id: PeerIdSchema })])
|
||||
.union([PeerIdSchema, PeerIdObjectSchema])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
|
|
@ -386,19 +457,19 @@ export const PeerCardContentSchema = z.array(z.string())
|
|||
*/
|
||||
export const PeerAdditionSchema = z.union([
|
||||
PeerIdSchema,
|
||||
z.object({ id: PeerIdSchema }),
|
||||
PeerIdObjectSchema,
|
||||
z.array(
|
||||
z.union([
|
||||
PeerIdSchema,
|
||||
z.object({ id: PeerIdSchema }),
|
||||
PeerIdObjectSchema,
|
||||
z.tuple([
|
||||
z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]),
|
||||
z.union([PeerIdSchema, PeerIdObjectSchema]),
|
||||
SessionPeerConfigSchema,
|
||||
]),
|
||||
])
|
||||
),
|
||||
z.tuple([
|
||||
z.union([PeerIdSchema, z.object({ id: PeerIdSchema })]),
|
||||
z.union([PeerIdSchema, PeerIdObjectSchema]),
|
||||
SessionPeerConfigSchema,
|
||||
]),
|
||||
])
|
||||
|
|
@ -798,8 +869,8 @@ export const PeerAdditionToApiSchema = PeerAdditionSchema.transform(
|
|||
*/
|
||||
export const PeerRemovalSchema = z.union([
|
||||
PeerIdSchema,
|
||||
z.object({ id: PeerIdSchema }),
|
||||
z.array(z.union([PeerIdSchema, z.object({ id: PeerIdSchema })])),
|
||||
PeerIdObjectSchema,
|
||||
z.array(z.union([PeerIdSchema, PeerIdObjectSchema])),
|
||||
])
|
||||
|
||||
/**
|
||||
|
|
@ -835,12 +906,14 @@ export const WorkspaceMetadataSchema = z.record(z.string(), z.unknown())
|
|||
* Schema for workspace configuration.
|
||||
* Includes reasoning, peer card, summary, and dream settings.
|
||||
*/
|
||||
export const WorkspaceConfigSchema = z.object({
|
||||
reasoning: ReasoningConfigSchema.nullable().optional(),
|
||||
peerCard: PeerCardConfigSchema.nullable().optional(),
|
||||
summary: SummaryConfigSchema.nullable().optional(),
|
||||
dream: DreamConfigSchema.nullable().optional(),
|
||||
})
|
||||
export const WorkspaceConfigSchema = z
|
||||
.object({
|
||||
reasoning: ReasoningConfigSchema.nullable().optional(),
|
||||
peerCard: PeerCardConfigSchema.nullable().optional(),
|
||||
summary: SummaryConfigSchema.nullable().optional(),
|
||||
dream: DreamConfigSchema.nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Schema for limit.
|
||||
|
|
@ -854,21 +927,23 @@ export const LimitSchema = z
|
|||
/**
|
||||
* Schema for conclusion query parameters.
|
||||
*/
|
||||
export const ConclusionQueryParamsSchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
top_k: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'top_k must be at least 1')
|
||||
.max(100, 'top_k must be at most 100')
|
||||
.optional(),
|
||||
distance: z
|
||||
.number()
|
||||
.min(0.0, 'distance must be at least 0.0')
|
||||
.max(1.0, 'distance must be at most 1.0')
|
||||
.optional(),
|
||||
filters: FilterSchema,
|
||||
})
|
||||
export const ConclusionQueryParamsSchema = z
|
||||
.object({
|
||||
query: SearchQuerySchema,
|
||||
top_k: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'top_k must be at least 1')
|
||||
.max(100, 'top_k must be at most 100')
|
||||
.optional(),
|
||||
distance: z
|
||||
.number()
|
||||
.min(0.0, 'distance must be at least 0.0')
|
||||
.max(1.0, 'distance must be at most 1.0')
|
||||
.optional(),
|
||||
filters: FilterSchema,
|
||||
})
|
||||
.strict()
|
||||
|
||||
/**
|
||||
* Type exports for use throughout the SDK.
|
||||
|
|
@ -883,6 +958,7 @@ export type MessageInput = z.infer<typeof MessageInputSchema>
|
|||
export type Filters = z.infer<typeof FilterSchema>
|
||||
export type ChatQuery = z.infer<typeof ChatQuerySchema>
|
||||
export type ContextParams = z.infer<typeof ContextParamsSchema>
|
||||
export type SearchQueryLike = z.infer<typeof SearchQueryLikeSchema>
|
||||
export type QueueStatusOptions = z.infer<typeof QueueStatusOptionsSchema>
|
||||
export type FileUpload = z.infer<typeof FileUploadSchema>
|
||||
export type GetRepresentationParams = z.infer<
|
||||
|
|
|
|||
|
|
@ -36,6 +36,62 @@ async def test_session_metadata(client_fixture: tuple[Honcho, str]):
|
|||
assert metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_fetch_methods_refresh_cached_status_fields(
|
||||
client_fixture: tuple[Honcho, str],
|
||||
):
|
||||
"""
|
||||
Tests that fetch-style session methods populate created_at and is_active.
|
||||
"""
|
||||
honcho_client, client_type = client_fixture
|
||||
session_id = "test-session-refresh-cache"
|
||||
|
||||
if client_type == "async":
|
||||
await honcho_client.aio.session(id=session_id)
|
||||
session = Session(session_id, honcho_client)
|
||||
|
||||
assert session.created_at is None
|
||||
assert session.is_active is None
|
||||
|
||||
metadata = await session.aio.get_metadata()
|
||||
assert metadata == {}
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
|
||||
session = Session(session_id, honcho_client)
|
||||
await session.aio.refresh()
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
|
||||
session = Session(session_id, honcho_client)
|
||||
configuration = await session.aio.get_configuration()
|
||||
assert configuration is not None
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
else:
|
||||
honcho_client.session(id=session_id)
|
||||
session = Session(session_id, honcho_client)
|
||||
|
||||
assert session.created_at is None
|
||||
assert session.is_active is None
|
||||
|
||||
metadata = session.get_metadata()
|
||||
assert metadata == {}
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
|
||||
session = Session(session_id, honcho_client)
|
||||
session.refresh()
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
|
||||
session = Session(session_id, honcho_client)
|
||||
configuration = session.get_configuration()
|
||||
assert configuration is not None
|
||||
assert session.created_at is not None
|
||||
assert session.is_active is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_peer_management(
|
||||
client_fixture: tuple[Honcho, str],
|
||||
|
|
|
|||
Loading…
Reference in New Issue