Merge branch 'main' into vineeth/dev-1034
This commit is contained in:
commit
4784269e7e
|
|
@ -8,8 +8,6 @@
|
|||
# Application Settings
|
||||
# =============================================================================
|
||||
LOG_LEVEL=INFO
|
||||
FASTAPI_HOST=0.0.0.0
|
||||
FASTAPI_PORT=8000
|
||||
# SESSION_PEERS_LIMIT=10
|
||||
# GET_CONTEXT_MAX_TOKENS=100000
|
||||
|
||||
|
|
@ -86,8 +84,6 @@ LLM_ANTHROPIC_API_KEY=your-anthropic-api-key-here
|
|||
# MAX_OUTPUT_TOKENS=2500
|
||||
# only applied when using Anthropic as provider
|
||||
# THINKING_BUDGET_TOKENS=1024
|
||||
# DERIVER_DEDUCTIVE_OBSERVATIONS_COUNT=6
|
||||
# DERIVER_EXPLICIT_OBSERVATIONS_COUNT=10
|
||||
|
||||
# =============================================================================
|
||||
# Dialectic Settings
|
||||
|
|
|
|||
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -5,6 +5,29 @@ 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.2.0] - 2025-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- Arbitrary filters now available on all search endpoints
|
||||
- Search combines full-text and semantic using reciprocal rank fusion
|
||||
- Webhook support (currently only supports queue_empty and test events, more to come)
|
||||
- Small test harness and custom test format for evaluating Honcho output quality
|
||||
- Added MCP server and documentation for it
|
||||
|
||||
### Changed
|
||||
|
||||
- Search has 10 results by default, max 100 results
|
||||
- Queue structure generalized to handle more event types
|
||||
- Summarizer now exhaustive by default and tuned for performance
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolve race condition for peers that leave a session while sending messages
|
||||
- Added explicit rollback to solve integrity error in queue
|
||||
- Re-introduced Sentry tracing to deriver
|
||||
- Better integrity logic in get_or_create API methods
|
||||
|
||||
## [2.1.2] - 2025-07-30
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
|
|||
- **Peers**: Create, list, update, chat (dialectic), messages, representation
|
||||
- **Sessions**: Create, list, update, delete, clone, manage peers, get context
|
||||
- **Messages**: Create (batch up to 100), list, get, update
|
||||
- **Keys**: Create scoped JWT tokens
|
||||
- **Keys**: Create scoped JWTs
|
||||
|
||||
### Key Features
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ src/
|
|||
2. **Flexible Theory of Mind**: Pluggable ToM implementations (conversational, single_prompt, long_term)
|
||||
3. **Background Processing**: Async queue system for expensive operations
|
||||
4. **Provider Abstraction**: Model client supports multiple LLM providers
|
||||
5. **Scoped Authentication**: JWT tokens can be scoped to workspace, peer, or session level
|
||||
5. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level
|
||||
6. **Batch Operations**: Support for bulk message creation (up to 100 messages)
|
||||
7. **Session History**: Two-tier summarization (short every 20 messages, long every 60)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🫡 Honcho
|
||||
|
||||

|
||||

|
||||
[](https://discord.gg/plasticlabs)
|
||||
[](https://arxiv.org/abs/2310.06983)
|
||||

|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
# Application-level settings
|
||||
[app]
|
||||
LOG_LEVEL = "INFO"
|
||||
FASTAPI_HOST = "0.0.0.0"
|
||||
FASTAPI_PORT = 8000
|
||||
SESSION_PEERS_LIMIT = 10
|
||||
GET_CONTEXT_MAX_TOKENS = 100000
|
||||
EMBED_MESSAGES = true
|
||||
|
|
@ -66,8 +64,6 @@ PROVIDER = "google"
|
|||
MODEL = "gemini-2.0-flash-lite"
|
||||
MAX_OUTPUT_TOKENS = 2500
|
||||
THINKING_BUDGET_TOKENS = 1024 # only applied when using Anthropic
|
||||
DEDUCTIVE_OBSERVATIONS_COUNT = 6
|
||||
EXPLICIT_OBSERVATIONS_COUNT = 10
|
||||
|
||||
# Dialectic settings
|
||||
[dialectic]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,28 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
## Version Compatibility
|
||||
|
||||
### Honcho API v2.1.2 (Current)
|
||||
### Honcho API v2.2.0 (Current)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v1.3.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@1.3.0
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v1.3.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==1.3.0
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Honcho API v2.1.2
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
|
|
@ -35,7 +56,8 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v2.1.2 (Current) | v1.2.1 | v1.2.2 |
|
||||
| v2.2.0 (Current) | v1.3.0 | v1.3.0 |
|
||||
| v2.1.2 | v1.2.1 | v1.2.2 |
|
||||
| v2.1.1 | v1.2.1 | v1.2.2 |
|
||||
| v2.1.0 | v1.2.1 | v1.2.2 |
|
||||
| v2.0.5 | v1.1.0 | v1.1.0 |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,31 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
### Honcho API and SDK Changelogs
|
||||
<Tabs>
|
||||
<Tab title="Honcho API">
|
||||
<Update label="v2.1.2 (Current)">
|
||||
<Update label="v2.2.0 (Current)">
|
||||
### Added
|
||||
|
||||
- Arbitrary filters now available on all search endpoints
|
||||
- Search combines full-text and semantic using reciprocal rank fusion
|
||||
- Webhook support (currently only supports queue_empty and test events, more to come)
|
||||
- Small test harness and custom test format for evaluating Honcho output quality
|
||||
- Added MCP server and documentation for it
|
||||
|
||||
### Changed
|
||||
|
||||
- Search has 10 results by default, max 100 results
|
||||
- Queue structure generalized to handle more event types
|
||||
- Summarizer now exhaustive by default and tuned for performance
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolve race condition for peers that leave a session while sending messages
|
||||
- Added explicit rollback to solve integrity error in queue
|
||||
- Re-introduced Sentry tracing to deriver
|
||||
- Better integrity logic in get_or_create API methods
|
||||
</Update>
|
||||
|
||||
|
||||
<Update label="v2.1.2">
|
||||
### Fixed
|
||||
|
||||
- Summarizer module to ignore empty summaries and pass appropriate one to get_context
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"navigation": {
|
||||
"versions": [
|
||||
{
|
||||
"version": "v2.1.2",
|
||||
"version": "v2.2.0",
|
||||
"api": {
|
||||
"openapi": [
|
||||
"openapi.documented.yml"
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ def get_session(user_id, location_id, create=False):
|
|||
|
||||
# Query for *active* sessions with both user_id and location_id
|
||||
sessions_iter = honcho.apps.users.sessions.list(
|
||||
app_id=app.id, user_id=user_id, reverse=True, filter={"is_active": True}
|
||||
app_id=app.id, user_id=user_id, reverse=True, filters={"is_active": True}
|
||||
)
|
||||
sessions = list(session for session in sessions_iter)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"apps"
|
||||
],
|
||||
"summary": "Get App",
|
||||
"description": "Get an App by ID.\n\nIf app_id is provided as a query parameter, it uses that (must match JWT app_id).\nOtherwise, it uses the app_id from the JWT token.",
|
||||
"description": "Get an App by ID.\n\nIf app_id is provided as a query parameter, it uses that (must match JWT app_id).\nOtherwise, it uses the app_id from the JWT.",
|
||||
"operationId": "get_app_v1_apps_get",
|
||||
"security": [
|
||||
{
|
||||
|
|
@ -54,10 +54,10 @@
|
|||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "App ID to retrieve. If not provided, uses JWT token",
|
||||
"description": "App ID to retrieve. If not provided, uses JWT",
|
||||
"title": "App Id"
|
||||
},
|
||||
"description": "App ID to retrieve. If not provided, uses JWT token"
|
||||
"description": "App ID to retrieve. If not provided, uses JWT"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
@ -535,7 +535,7 @@
|
|||
"users"
|
||||
],
|
||||
"summary": "Get User",
|
||||
"description": "Get a User by ID\n\nIf user_id is provided as a query parameter, it uses that (must match JWT app_id).\nOtherwise, it uses the user_id from the JWT token.",
|
||||
"description": "Get a User by ID\n\nIf user_id is provided as a query parameter, it uses that (must match JWT app_id).\nOtherwise, it uses the user_id from the JWT.",
|
||||
"operationId": "get_user_v1_apps__app_id__users_get",
|
||||
"security": [
|
||||
{
|
||||
|
|
@ -568,10 +568,10 @@
|
|||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "User ID to retrieve. If not provided, users JWT token",
|
||||
"description": "User ID to retrieve. If not provided, uses JWT",
|
||||
"title": "User Id"
|
||||
},
|
||||
"description": "User ID to retrieve. If not provided, users JWT token"
|
||||
"description": "User ID to retrieve. If not provided, uses JWT"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
@ -959,7 +959,7 @@
|
|||
"sessions"
|
||||
],
|
||||
"summary": "Get Session",
|
||||
"description": "Get a specific session for a user.\n\nIf session_id is provided as a query parameter, it uses that (must match JWT session_id).\nOtherwise, it uses the session_id from the JWT token.",
|
||||
"description": "Get a specific session for a user.\n\nIf session_id is provided as a query parameter, it uses that (must match JWT session_id).\nOtherwise, it uses the session_id from the JWT.",
|
||||
"operationId": "get_session_v1_apps__app_id__users__user_id__sessions_get",
|
||||
"security": [
|
||||
{
|
||||
|
|
@ -1003,10 +1003,10 @@
|
|||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Session ID to retrieve. If not provided, uses JWT token",
|
||||
"description": "Session ID to retrieve. If not provided, uses JWT",
|
||||
"title": "Session Id"
|
||||
},
|
||||
"description": "Session ID to retrieve. If not provided, uses JWT token"
|
||||
"description": "Session ID to retrieve. If not provided, uses JWT"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
@ -2577,7 +2577,7 @@
|
|||
"collections"
|
||||
],
|
||||
"summary": "Get Collection",
|
||||
"description": "Get a specific collection for a user.\n\nIf collection_id is provided as a query parameter, it uses that (must match JWT collection_id).\nOtherwise, it uses the collection_id from the JWT token.",
|
||||
"description": "Get a specific collection for a user.\n\nIf collection_id is provided as a query parameter, it uses that (must match JWT collection_id).\nOtherwise, it uses the collection_id from the JWT.",
|
||||
"operationId": "get_collection_v1_apps__app_id__users__user_id__collections_get",
|
||||
"security": [
|
||||
{
|
||||
|
|
@ -2621,10 +2621,10 @@
|
|||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Collection ID to retrieve. If not provided, uses JWT token",
|
||||
"description": "Collection ID to retrieve. If not provided, uses JWT",
|
||||
"title": "Collection Id"
|
||||
},
|
||||
"description": "Collection ID to retrieve. If not provided, uses JWT token"
|
||||
"description": "Collection ID to retrieve. If not provided, uses JWT"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
|||
|
|
@ -111,8 +111,6 @@ The application will use the production connection URI while keeping the pool si
|
|||
```bash
|
||||
# Logging and server settings
|
||||
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
|
||||
FASTAPI_HOST=0.0.0.0
|
||||
FASTAPI_PORT=8000
|
||||
SESSION_PEERS_LIMIT=10
|
||||
GET_CONTEXT_MAX_TOKENS=100000
|
||||
|
||||
|
|
@ -256,10 +254,6 @@ DERIVER_MODEL=gemini-2.0-flash-lite
|
|||
DERIVER_WORKERS=1
|
||||
DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5
|
||||
DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0
|
||||
|
||||
# Observation retrieval counts
|
||||
DERIVER_DEDUCTIVE_OBSERVATIONS_COUNT=6
|
||||
DERIVER_EXPLICIT_OBSERVATIONS_COUNT=10
|
||||
```
|
||||
|
||||
**Summary Generation:**
|
||||
|
|
@ -310,8 +304,6 @@ SENTRY_PROFILES_SAMPLE_RATE=0.1
|
|||
```toml
|
||||
[app]
|
||||
LOG_LEVEL = "DEBUG"
|
||||
FASTAPI_HOST = "127.0.0.1"
|
||||
FASTAPI_PORT = 8000
|
||||
SESSION_PEERS_LIMIT = 10
|
||||
EMBED_MESSAGES = false
|
||||
|
||||
|
|
@ -357,8 +349,6 @@ ANTHROPIC_API_KEY=your-dev-anthropic-key
|
|||
```toml
|
||||
[app]
|
||||
LOG_LEVEL = "WARNING"
|
||||
FASTAPI_HOST = "0.0.0.0"
|
||||
FASTAPI_PORT = 8000
|
||||
SESSION_PEERS_LIMIT = 10
|
||||
EMBED_MESSAGES = true
|
||||
|
||||
|
|
|
|||
|
|
@ -295,8 +295,7 @@ const client = new Honcho({
|
|||
- Check that the keys have sufficient credits/quota
|
||||
|
||||
**Port Already in Use**
|
||||
- Change the port in your configuration: `FASTAPI_PORT=8001`
|
||||
- Or stop other services using port 8000
|
||||
- Pass a different port to FastAPI or stop other services using port 8000
|
||||
|
||||
**Docker Issues**
|
||||
- Ensure Docker is running
|
||||
|
|
|
|||
|
|
@ -211,9 +211,11 @@ backchannel directly with Honcho, via MCP or a direct API call.
|
|||
Developers should frame the Dialectic as talking to an expert on the Peer rather than addressing the Peer itself, meaning:
|
||||
|
||||
```python
|
||||
alice.chat("What is alice's mood like") # ✅ Correct
|
||||
alice.chat("What is the user's mood today?") # ✅ Ideal
|
||||
|
||||
alice.chat("What is your mood like") # ❌ Wrong
|
||||
alice.chat("What is alice's mood today?") # ✅ Works -- but make sure to consider what peer "Alice" has been saying in their messages about name/identity.
|
||||
|
||||
alice.chat("What is your mood today?") # ❌ Likely to fail -- the dialectic agent may conflate itself and the user.
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
title: 'Configuration'
|
||||
description: 'Customizing how Honcho handles peers and sessions'
|
||||
icon: 'wrench'
|
||||
---
|
||||
|
||||
Entities in Honcho can sometimes be configured to change the behavior of the deriver, which is responsible for generating and storing facts, summaries, and user representations.
|
||||
|
||||
These configurations can be set at the peer, session, and session-peer level (AKA the state of a peer within a specific session).
|
||||
|
||||
### Peer Configuration
|
||||
|
||||
By default, all peers are "observed" by Honcho. This means that Honcho will derive facts from messages sent by the peer and generate a representation of them. In most cases, this is why you use Honcho! However, sometimes an application requires a peer that should not be observed: for example, an assistant or game NPC that your program will never need to ask questions about.
|
||||
|
||||
You may therefore disable observation of a peer by setting the `observe_me` flag in their configuration to `false`.
|
||||
|
||||
If the peer has a session-level configuration, it will override this configuration. If the flag is not set, or is set to `true`, the peer will be observed.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
# Initialize client
|
||||
honcho = Honcho()
|
||||
|
||||
# Create peer with configuration
|
||||
peer = honcho.peer("my-peer", config={"observe_me": False})
|
||||
|
||||
# Change peer's configuration
|
||||
peer.set_peer_config({"observe_me": True})
|
||||
|
||||
# Note: creating the same peer again will also replace the configuration
|
||||
peer = honcho.peer("my-peer", config={"observe_me": False})
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Create peer with configuration
|
||||
const peer = await honcho.peer("my-peer", { config: { observe_me: false } });
|
||||
|
||||
// Change peer's configuration
|
||||
await peer.setPeerConfig({ observe_me: true });
|
||||
|
||||
// Note: creating the same peer again will also replace the configuration
|
||||
await honcho.peer("my-peer", { config: { observe_me: false } });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session Configuration
|
||||
|
||||
By default, all sessions have the deriver enabled, much like peers. You may create a session that escapes the deriver's watchful eye by setting the `deriver_disabled` flag to `true`. You can update the flag by calling `get_or_create` on the session with a new configuration.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
# Initialize client
|
||||
honcho = Honcho()
|
||||
|
||||
# Create session with configuration
|
||||
session = honcho.session("my-session", config={"deriver_disabled": True})
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Create session with configuration
|
||||
const session = await honcho.session("my-session", { config: { deriver_disabled: true } });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session-Peer Configuration
|
||||
|
||||
Configuration at the session-peer level is the most common use case for configuration flags. You will often want to arrange a session such that certain peers observe others in order to form "local representations" of them. There are two flags that can be set at the session-peer level:
|
||||
|
||||
- `observe_me`: Whether this peer should *be observed* by others in the session. By default, this is `true`. This overrides the peer-level `observe_me` flag.
|
||||
|
||||
- `observe_others`: Whether this peer should produce local representations of others in the session. By default, this is `false`. Other peers will only be observed if their `observe_me` flag is `true`.
|
||||
|
||||
You can combine these flags across multiple peers to arrange any possible permutation of directional observation. Note that in the default case, no local representations are produced. To produce local representations, you must set the `observe_others` flag to `true` for at least one peer in the session and at least one other peer must have their `observe_me` flag set to `true`.
|
||||
|
||||
Many applications will work best without local representations, preferring to chat with Honcho's top-down representation of each peer. Only enable local representations via the `observe_others` flag if you are doing advanced reasoning on user perspectives.
|
||||
|
||||
<img src="/images/local-vs-global-reps.png" alt="Peer Representations" />
|
||||
|
||||
You can dynamically change the configuration of a session-peer by calling `set_peer_config` on the session with the peer and the configuration you want to set.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
# Initialize client
|
||||
honcho = Honcho()
|
||||
|
||||
# Create session
|
||||
session = honcho.session("my-session")
|
||||
|
||||
# Create peers
|
||||
alice = honcho.peer("alice")
|
||||
bob = honcho.peer("bob")
|
||||
|
||||
# Add peers to session with default configuration
|
||||
session.add_peers([alice, bob])
|
||||
|
||||
# Add another peer to the session with a custom configuration
|
||||
charlie = honcho.peer("charlie")
|
||||
session.add_peers([charlie, {"observe_me": False, "observe_others": True}])
|
||||
|
||||
# Set session-peer configuration
|
||||
session.set_peer_config(alice, {"observe_others": True})
|
||||
session.set_peer_config(bob, {"observe_me": False})
|
||||
|
||||
# Get session-peer configuration
|
||||
charlie_config = session.get_peer_config(charlie)
|
||||
print(charlie_config)
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Create session
|
||||
const session = await honcho.session("my-session");
|
||||
|
||||
// Create peers
|
||||
const alice = await honcho.peer("alice");
|
||||
const bob = await honcho.peer("bob");
|
||||
|
||||
// Add peers to session
|
||||
await session.addPeers([alice, bob]);
|
||||
|
||||
// Add another peer to the session with a custom configuration
|
||||
const charlie = await honcho.peer("charlie");
|
||||
await session.addPeers([charlie, { observe_me: false, observe_others: true }]);
|
||||
|
||||
// Set session-peer configuration
|
||||
await session.setPeerConfig(alice, { observe_others: true });
|
||||
await session.setPeerConfig(bob, { observe_me: false });
|
||||
|
||||
// Get session-peer configuration
|
||||
const charlieConfig = await session.getPeerConfig(charlie);
|
||||
console.log(charlieConfig);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
|
@ -19,9 +19,17 @@ To help developers understand when a Peer's representation is fully up to date,
|
|||
- If a Session is specified, the queue status reflects pending work for the Peer's working representation in that Session.
|
||||
|
||||
### Search
|
||||
Honcho supports full-text search across message content in different scopes.
|
||||
- You can search within a specific Session, Peer, or across all messages in a Workspace.
|
||||
- Search results are ordered by relevance, making it easy to quickly retrieve important past messages.
|
||||
Honcho implements a powerful search endpoint that allows you to search for messages across a workspace, session, or peer with complex [filters](/v2/guides/using-filters).
|
||||
|
||||
The search process combines full-text and semantic search using reciprocal rank fusion. By default, all messages ingested into Honcho have embeddings generated and stored in the database, enabling semantic search -- if this feature is disabled, the search process will only use full-text search.
|
||||
|
||||
Results are returned in the form of a list of Message objects, and you may choose how many results to return. The default is 10 results, with a maximum of 100.
|
||||
|
||||
In the SDK, search is available on `Workspace`, `Session`, and `Peer` objects, and an optional `filters` parameter may be used to apply a narrower search scope such as a time range or developer-defined metadata attached to messages.
|
||||
|
||||
Note that results are not ordered by recency, only relevance. Results can be sorted by timestamp or a filter on the `created_at` field can limit results to recent messages.
|
||||
|
||||
[Look here for examples of how to use search in the SDK](/v2/guides/search).
|
||||
|
||||
### Scoped API Keys
|
||||
Builders can create scoped API keys to control access to different resources within Honcho.
|
||||
|
|
|
|||
|
|
@ -67,4 +67,4 @@ cognitive functions.
|
|||
continually generating & updating internal world models to anticipate sensory input, rather than
|
||||
passively receiving it--closely linked to Bayesian brain hypotheses, which hold that the brain
|
||||
interprets the world probabilistically, weighing prior knowledge against new evidence to minimize
|
||||
uncertainty.
|
||||
uncertainty.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Almost all agents require, in addition to personalization and memory, a way to q
|
|||
|
||||
### Creating Summaries
|
||||
|
||||
Honcho already has an asynchronous task queue for the purpose of deriving facts from messages. This is the ideal place to create summaries where they won't add latency to a message. Currently, Honcho has two configurable summary types:
|
||||
Honcho already has an asynchronous task queue for the purpose of deriving facts from messages. This is the ideal place to create summaries where they won't add latency to a message. Currently, Honcho has two configurable summary types:
|
||||
|
||||
* Short summaries: by default, enqueued every 20 messages and given a token limit of 1000
|
||||
* Long summaries: by default, enqueued every 60 messages and given a token limit of 4000
|
||||
|
|
@ -46,4 +46,4 @@ The above scenarios indicate where summarization is not possible -- therefore, t
|
|||
|
||||
Sometimes, gaps in context aren't an issue. In these cases, it's best to pass a reasonable token limit depending on your needs. Other cases demand exhaustive context -- don't pass a token limit and just let Honcho retrieve the ideal combination of summary and recent messages. Finally, if you don't care about the conversation at large and just want the last few messages, set `summary` to false and `tokens` to some multiple of your desired message count. Note that context messages are not paginated, so there's a hard limit on the number of messages that can be retrieved (currently 100,000 tokens).
|
||||
|
||||
As a final note, remember that summaries are generated asynchronously and therefore may not be available immediately. If you batch-save a large number of messages, assume that summaries will not be available until those messages are processed, which can take seconds to minutes depending on the number of messages and the configured LLM provider. Exhaustive `get_context` calls performed during this time will likely just return the messages in the session.
|
||||
As a final note, remember that summaries are generated asynchronously and therefore may not be available immediately. If you batch-save a large number of messages, assume that summaries will not be available until those messages are processed, which can take seconds to minutes depending on the number of messages and the configured LLM provider. Exhaustive `get_context` calls performed during this time will likely just return the messages in the session.
|
||||
|
|
|
|||
|
|
@ -143,15 +143,17 @@ print(response)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Ask what Bob is like
|
||||
const response = await alice.chat("Tell me about Bob's interests and habits");
|
||||
console.log(response);
|
||||
(async () => {
|
||||
// Ask what Bob is like
|
||||
const response = await alice.chat("Tell me about Bob's interests and habits");
|
||||
console.log(response);
|
||||
|
||||
// Returns rich context like:
|
||||
// "Bob is health-conscious and has been working on getting back in shape.
|
||||
// He regularly goes to the gym, particularly in the evenings, and finds
|
||||
// exercise helps him relax. He's encouraging about fitness and willing
|
||||
// to share advice about workout routines."
|
||||
// Returns rich context like:
|
||||
// "Bob is health-conscious and has been working on getting back in shape.
|
||||
// He regularly goes to the gym, particularly in the evenings, and finds
|
||||
// exercise helps him relax. He's encouraging about fitness and willing
|
||||
// to share advice about workout routines."
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -574,8 +574,8 @@ session.add_messages([
|
|||
])
|
||||
|
||||
# Filter messages by metadata
|
||||
finance_messages = session.get_messages(filter={"metadata": {"topic": "finance"}})
|
||||
action_items = session.get_messages(filter={"metadata": {"action_item": True}})
|
||||
finance_messages = session.get_messages(filters={"metadata": {"topic": "finance"}})
|
||||
action_items = session.get_messages(filters={"metadata": {"action_item": True}})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
@ -597,10 +597,10 @@ await session.addMessages([
|
|||
|
||||
// Filter messages by metadata
|
||||
const financeMessages = await session.getMessages({
|
||||
filter: { metadata: { topic: "finance" } }
|
||||
filters: { metadata: { topic: "finance" } }
|
||||
});
|
||||
const actionItems = await session.getMessages({
|
||||
filter: { metadata: { action_item: true } }
|
||||
filters: { metadata: { action_item: true } }
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ session = honcho.session("demo-session")
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import Honcho from '@honcho-ai/sdk';
|
||||
import { Honcho } from '@honcho-ai/sdk';
|
||||
|
||||
// use the default workspace
|
||||
const honcho = new Honcho();
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ async def dialectic(ctx, query: str):
|
|||
session = honcho_client.session(id=str(ctx.channel.id))
|
||||
|
||||
response = peer.chat(
|
||||
queries=query,
|
||||
query=query,
|
||||
session_id=session.id,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ Files are processed in memory and not stored on disk. Only the extracted text co
|
|||
|
||||
## Basic Usage
|
||||
|
||||
### Upload a Single File
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
|
@ -59,45 +57,20 @@ print(f"Created {len(messages)} messages from the PDF")
|
|||
import { Honcho } from "@honcho-ai/sdk";
|
||||
import fs from "fs";
|
||||
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Create session and peer
|
||||
const session = honcho.session("research-session");
|
||||
const user = honcho.peer("researcher");
|
||||
// Create session and peer
|
||||
const session = await honcho.session("research-session");
|
||||
const user = await honcho.peer("researcher");
|
||||
|
||||
// Upload a PDF to a session
|
||||
const fileStream = fs.createReadStream("research_paper.pdf");
|
||||
const messages = await session.uploadFile({
|
||||
file: fileStream,
|
||||
peerId: user.id,
|
||||
});
|
||||
// Upload a PDF to a session
|
||||
const fileStream = fs.createReadStream("research_paper.pdf");
|
||||
const messages = await session.uploadFile(fileStream, user.id);
|
||||
|
||||
console.log(`Created ${messages.length} messages from the PDF`);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Upload to Peer's Global Representation
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Upload files directly to a peer's global representation
|
||||
with open("personal_notes.pdf", "rb") as file:
|
||||
messages = user.upload_file(
|
||||
file=file,
|
||||
)
|
||||
|
||||
print(f"Added {len(messages)} messages to {user.id}'s global representation")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Upload files directly to a peer's global representation
|
||||
const fileStream = fs.createReadStream("personal_notes.pdf");
|
||||
const messages = await user.uploadFile({
|
||||
file: fileStream,
|
||||
});
|
||||
|
||||
console.log(`Added ${messages.length} messages to ${user.id}'s global representation`);
|
||||
console.log(`Created ${messages.length} messages from the PDF`);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -108,7 +81,7 @@ The upload methods accept the following parameters:
|
|||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `file` | File | Yes | File to upload |
|
||||
| `peer_id` | String | Session only | ID of the peer creating the messages |
|
||||
| `peer_id` | String | Yes | ID of the peer creating the messages |
|
||||
|
||||
## File Processing Details
|
||||
|
||||
|
|
@ -160,17 +133,19 @@ messages = context.to_openai(assistant=assistant)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Query what was learned from the uploaded documents
|
||||
const response = await user.chat("What are the key findings from the research papers I uploaded?");
|
||||
console.log(response);
|
||||
(async () => {
|
||||
// Query what was learned from the uploaded documents
|
||||
const response = await user.chat("What are the key findings from the research papers I uploaded?");
|
||||
console.log(response);
|
||||
|
||||
// Ask about specific documents
|
||||
const response2 = await user.chat("What does the quarterly report say about revenue growth?");
|
||||
console.log(response2);
|
||||
// Ask about specific documents
|
||||
const response2 = await user.chat("What does the quarterly report say about revenue growth?");
|
||||
console.log(response2);
|
||||
|
||||
// Get context from the uploaded documents for LLM integration
|
||||
const context = await session.getContext({ tokens: 3000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
// Get context from the uploaded documents for LLM integration
|
||||
const context = await session.getContext({ tokens: 3000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -262,52 +237,51 @@ print("Document Analysis:", analysis)
|
|||
import { Honcho } from "@honcho-ai/sdk";
|
||||
import fs from "fs";
|
||||
|
||||
// Initialize
|
||||
const honcho = new Honcho({});
|
||||
const session = honcho.session("document-analysis");
|
||||
const user = honcho.peer("analyst");
|
||||
const assistant = honcho.peer("analysis-bot");
|
||||
(async () => {
|
||||
// Initialize
|
||||
const honcho = new Honcho({});
|
||||
const session = await honcho.session("document-analysis");
|
||||
const user = await honcho.peer("analyst");
|
||||
const assistant = await honcho.peer("analysis-bot");
|
||||
|
||||
async function uploadDocument(filePath: string, description: string) {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const messages = await session.uploadFile({
|
||||
file: fileStream,
|
||||
peerId: user.id,
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
async function uploadDocument(filePath: string, description: string) {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const messages = await session.uploadFile(fileStream, user.id);
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function analyzeDocuments() {
|
||||
const context = await session.getContext({ tokens: 4000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
// Add analysis request
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: "Please analyze all the documents I've uploaded and provide a comprehensive summary of the key findings, trends, and recommendations."
|
||||
});
|
||||
async function analyzeDocuments() {
|
||||
const context = await session.getContext({ tokens: 4000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
// Add analysis request
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: "Please analyze all the documents I've uploaded and provide a comprehensive summary of the key findings, trends, and recommendations."
|
||||
});
|
||||
|
||||
// Call OpenAI (or your preferred LLM)
|
||||
// const response = await openai.chat.completions.create({ model: "gpt-4", messages });
|
||||
// return response.choices[0].message.content;
|
||||
// Call OpenAI (or your preferred LLM)
|
||||
// const response = await openai.chat.completions.create({ model: "gpt-4", messages });
|
||||
// return response.choices[0].message.content;
|
||||
|
||||
return "Analysis would be generated here";
|
||||
}
|
||||
return "Analysis would be generated here";
|
||||
}
|
||||
|
||||
// Upload multiple documents
|
||||
const documents = [
|
||||
["quarterly_report.pdf", "Q3 2024 Quarterly Financial Report"],
|
||||
["market_research.pdf", "Market Analysis and Competitive Landscape"],
|
||||
["product_roadmap.pdf", "Product Development Roadmap 2024-2025"]
|
||||
];
|
||||
// Upload multiple documents
|
||||
const documents = [
|
||||
["quarterly_report.pdf", "Q3 2024 Quarterly Financial Report"],
|
||||
["market_research.pdf", "Market Analysis and Competitive Landscape"],
|
||||
["product_roadmap.pdf", "Product Development Roadmap 2024-2025"]
|
||||
];
|
||||
|
||||
for (const [filePath, description] of documents) {
|
||||
const messages = await uploadDocument(filePath, description);
|
||||
console.log(`Uploaded ${filePath}: ${messages.length} messages created`);
|
||||
}
|
||||
for (const [filePath, description] of documents) {
|
||||
const messages = await uploadDocument(filePath, description);
|
||||
console.log(`Uploaded ${filePath}: ${messages.length} messages created`);
|
||||
}
|
||||
|
||||
// Get AI analysis
|
||||
const analysis = await analyzeDocuments();
|
||||
console.log("Document Analysis:", analysis);
|
||||
// Get AI analysis
|
||||
const analysis = await analyzeDocuments();
|
||||
console.log("Document Analysis:", analysis);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,14 @@ context = session.get_context()
|
|||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
// Initialize client and create session
|
||||
const honcho = new Honcho({});
|
||||
const session = honcho.session("conversation-1");
|
||||
(async () => {
|
||||
// Initialize client and create session
|
||||
const honcho = new Honcho({});
|
||||
const session = await honcho.session("conversation-1");
|
||||
|
||||
// Get basic context (not very useful before adding any messages!)
|
||||
const context = await session.getContext();
|
||||
// Get basic context (not very useful before adding any messages!)
|
||||
const context = await session.getContext();
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -54,11 +56,13 @@ context = session.get_context(tokens=3000)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Limit context to 1500 tokens
|
||||
const context = await session.getContext({ tokens: 1500 });
|
||||
(async () => {
|
||||
// Limit context to 1500 tokens
|
||||
const context = await session.getContext({ tokens: 1500 });
|
||||
|
||||
// Limit context to 3000 tokens for larger conversations
|
||||
const context = await session.getContext({ tokens: 3000 });
|
||||
// Limit context to 3000 tokens for larger conversations
|
||||
const context = await session.getContext({ tokens: 3000 });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -76,14 +80,16 @@ context = session.get_context(summary=False, tokens=2000)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get context with summary enabled -- will contain both summary and messages
|
||||
const context = await session.getContext({ summary: true });
|
||||
(async () => {
|
||||
// Get context with summary enabled -- will contain both summary and messages
|
||||
const context = await session.getContext({ summary: true });
|
||||
|
||||
// Combine summary=False with token limits to get more messages
|
||||
const context = await session.getContext({
|
||||
summary: false,
|
||||
tokens: 2000
|
||||
});
|
||||
// Combine summary=False with token limits to get more messages
|
||||
const context = await session.getContext({
|
||||
summary: false,
|
||||
tokens: 2000
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -120,26 +126,28 @@ print(openai_messages)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create peers
|
||||
const alice = honcho.peer("alice");
|
||||
const assistant = honcho.peer("assistant");
|
||||
(async () => {
|
||||
// Create peers
|
||||
const alice = await honcho.peer("alice");
|
||||
const assistant = await honcho.peer("assistant");
|
||||
|
||||
// Add some conversation
|
||||
await session.addMessages([
|
||||
alice.message("What's the weather like today?"),
|
||||
assistant.message("It's sunny and 75°F outside!")
|
||||
]);
|
||||
// Add some conversation
|
||||
await session.addMessages([
|
||||
alice.message("What's the weather like today?"),
|
||||
assistant.message("It's sunny and 75°F outside!")
|
||||
]);
|
||||
|
||||
// Get context and convert to OpenAI format
|
||||
const context = await session.getContext();
|
||||
const openaiMessages = context.toOpenAI(assistant);
|
||||
// Get context and convert to OpenAI format
|
||||
const context = await session.getContext();
|
||||
const openaiMessages = context.toOpenAI(assistant);
|
||||
|
||||
// The messages are now ready for OpenAI API
|
||||
console.log(openaiMessages);
|
||||
// [
|
||||
// {"role": "user", "content": "What's the weather like today?"},
|
||||
// {"role": "assistant", "content": "It's sunny and 75°F outside!"}
|
||||
// ]
|
||||
// The messages are now ready for OpenAI API
|
||||
console.log(openaiMessages);
|
||||
// [
|
||||
// {"role": "user", "content": "What's the weather like today?"},
|
||||
// {"role": "assistant", "content": "It's sunny and 75°F outside!"}
|
||||
// ]
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -158,12 +166,14 @@ print(anthropic_messages)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get context and convert to Anthropic format
|
||||
const context = await session.getContext();
|
||||
const anthropicMessages = context.toAnthropic(assistant);
|
||||
(async () => {
|
||||
// Get context and convert to Anthropic format
|
||||
const context = await session.getContext();
|
||||
const anthropicMessages = context.toAnthropic(assistant);
|
||||
|
||||
// Ready for Anthropic API
|
||||
console.log(anthropicMessages);
|
||||
// Ready for Anthropic API
|
||||
console.log(anthropicMessages);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -217,39 +227,41 @@ session.add_messages([
|
|||
import OpenAI from 'openai';
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
// Initialize clients
|
||||
const honcho = new Honcho({});
|
||||
const openai = new OpenAI();
|
||||
(async () => {
|
||||
// Initialize clients
|
||||
const honcho = new Honcho({});
|
||||
const openai = new OpenAI();
|
||||
|
||||
// Set up conversation
|
||||
const session = honcho.session("support-chat");
|
||||
const user = honcho.peer("user-123");
|
||||
const assistant = honcho.peer("support-bot");
|
||||
// Set up conversation
|
||||
const session = honcho.session("support-chat");
|
||||
const user = honcho.peer("user-123");
|
||||
const assistant = honcho.peer("support-bot");
|
||||
|
||||
// Add conversation history
|
||||
await session.addMessages([
|
||||
user.message("I'm having trouble with my account login"),
|
||||
assistant.message("I can help you with that. What error message are you seeing?"),
|
||||
user.message("It says 'Invalid credentials' but I'm sure my password is correct")
|
||||
]);
|
||||
// Add conversation history
|
||||
await session.addMessages([
|
||||
user.message("I'm having trouble with my account login"),
|
||||
assistant.message("I can help you with that. What error message are you seeing?"),
|
||||
user.message("It says 'Invalid credentials' but I'm sure my password is correct")
|
||||
]);
|
||||
|
||||
// Get context for LLM
|
||||
const messages = await session.getContext({ tokens: 2000 }).toOpenAI(assistant);
|
||||
// Get context for LLM
|
||||
const messages = await session.getContext({ tokens: 2000 }).toOpenAI(assistant);
|
||||
|
||||
// Add new user message and get AI response
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: [
|
||||
...messages,
|
||||
{ role: "user", content: "Can you reset my password?" }
|
||||
]
|
||||
});
|
||||
// Add new user message and get AI response
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: [
|
||||
...messages,
|
||||
{ role: "user", content: "Can you reset my password?" }
|
||||
]
|
||||
});
|
||||
|
||||
// Add AI response back to session
|
||||
await session.addMessages([
|
||||
user.message("Can you reset my password?"),
|
||||
assistant.message(response.choices[0].message.content)
|
||||
]);
|
||||
// Add AI response back to session
|
||||
await session.addMessages([
|
||||
user.message("Can you reset my password?"),
|
||||
assistant.message(response.choices[0].message.content)
|
||||
]);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -294,44 +306,46 @@ chat_loop()
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
async function chatLoop() {
|
||||
const session = honcho.session("chat-session");
|
||||
const user = honcho.peer("user");
|
||||
const assistant = honcho.peer("ai-assistant");
|
||||
(async () => {
|
||||
async function chatLoop() {
|
||||
const session = await honcho.session("chat-session");
|
||||
const user = await honcho.peer("user");
|
||||
const assistant = await honcho.peer("ai-assistant");
|
||||
|
||||
// This would be replaced with actual user input handling in a real app
|
||||
const userInputs = [
|
||||
"Hello, how are you?",
|
||||
"What's the weather like?",
|
||||
"Tell me a joke"
|
||||
];
|
||||
// This would be replaced with actual user input handling in a real app
|
||||
const userInputs = [
|
||||
"Hello, how are you?",
|
||||
"What's the weather like?",
|
||||
"Tell me a joke"
|
||||
];
|
||||
|
||||
for (const userInput of userInputs) {
|
||||
console.log(`You: ${userInput}`);
|
||||
for (const userInput of userInputs) {
|
||||
console.log(`You: ${userInput}`);
|
||||
|
||||
// Add user message to session
|
||||
await session.addMessages([user.message(userInput)]);
|
||||
// Add user message to session
|
||||
await session.addMessages([user.message(userInput)]);
|
||||
|
||||
// Get conversation context
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
// Get conversation context
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
|
||||
// Get AI response
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: messages
|
||||
});
|
||||
// Get AI response
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: messages
|
||||
});
|
||||
|
||||
const aiResponse = response.choices[0].message.content;
|
||||
console.log(`Assistant: ${aiResponse}`);
|
||||
const aiResponse = response.choices[0].message.content;
|
||||
console.log(`Assistant: ${aiResponse}`);
|
||||
|
||||
// Add AI response to session
|
||||
await session.addMessages([assistant.message(aiResponse)]);
|
||||
}
|
||||
}
|
||||
// Add AI response to session
|
||||
await session.addMessages([assistant.message(aiResponse)]);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the chat loop
|
||||
await chatLoop();
|
||||
// Start the chat loop
|
||||
await chatLoop();
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -355,18 +369,20 @@ print(f"Context contains {len(messages)} formatted messages")
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// For long conversations, use summary mode
|
||||
const longSession = honcho.session("long-conversation");
|
||||
(async () => {
|
||||
// For long conversations, use summary mode
|
||||
const longSession = await honcho.session("long-conversation");
|
||||
|
||||
// Get summarized context to fit within token limits
|
||||
const context = await longSession.getContext({
|
||||
summary: true,
|
||||
tokens: 1500
|
||||
});
|
||||
const messages = context.toOpenAI(assistant);
|
||||
// Get summarized context to fit within token limits
|
||||
const context = await longSession.getContext({
|
||||
summary: true,
|
||||
tokens: 1500
|
||||
});
|
||||
const messages = context.toOpenAI(assistant);
|
||||
|
||||
// This will include a summary of older messages and recent full messages
|
||||
console.log(`Context contains ${messages.length} formatted messages`);
|
||||
// This will include a summary of older messages and recent full messages
|
||||
console.log(`Context contains ${messages.length} formatted messages`);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -390,18 +406,20 @@ moderator_context = session.get_context().to_openai(assistant=moderator)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create different assistant peers
|
||||
const chatbot = honcho.peer("chatbot");
|
||||
const analyzer = honcho.peer("data-analyzer");
|
||||
const moderator = honcho.peer("moderator");
|
||||
(async () => {
|
||||
// Create different assistant peers
|
||||
const chatbot = await honcho.peer("chatbot");
|
||||
const analyzer = await honcho.peer("data-analyzer");
|
||||
const moderator = await honcho.peer("moderator");
|
||||
|
||||
// Get context formatted for each assistant type
|
||||
const context = await session.getContext();
|
||||
const chatbotContext = context.toOpenAI(chatbot);
|
||||
const analyzerContext = context.toOpenAI(analyzer);
|
||||
const moderatorContext = context.toOpenAI(moderator);
|
||||
// Get context formatted for each assistant type
|
||||
const context = await session.getContext();
|
||||
const chatbotContext = context.toOpenAI(chatbot);
|
||||
const analyzerContext = context.toOpenAI(analyzer);
|
||||
const moderatorContext = context.toOpenAI(moderator);
|
||||
|
||||
// Each context will format the conversation from that assistant's perspective
|
||||
// Each context will format the conversation from that assistant's perspective
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -422,12 +440,14 @@ context = session.get_context(summary=True, tokens=2000)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Good: Set reasonable token limits based on your model
|
||||
const context = await session.getContext({ tokens: 3000 }); // For GPT-4
|
||||
const context = await session.getContext({ tokens: 1500 }); // For smaller models
|
||||
(async () => {
|
||||
// Good: Set reasonable token limits based on your model
|
||||
const context = await session.getContext({ tokens: 3000 }); // For GPT-4
|
||||
const context = await session.getContext({ tokens: 1500 }); // For smaller models
|
||||
|
||||
// Good: Use summaries for very long conversations
|
||||
const context = await session.getContext({ summary: true, tokens: 2000 });
|
||||
// Good: Use summaries for very long conversations
|
||||
const context = await session.getContext({ summary: true, tokens: 2000 });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -446,12 +466,14 @@ anthropic_messages = context.to_anthropic(assistant=assistant)
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Cache context for multiple LLM calls within the same request
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const openaiMessages = context.toOpenAI(assistant);
|
||||
const anthropicMessages = context.toAnthropic(assistant);
|
||||
(async () => {
|
||||
// Cache context for multiple LLM calls within the same request
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const openaiMessages = context.toOpenAI(assistant);
|
||||
const anthropicMessages = context.toAnthropic(assistant);
|
||||
|
||||
// Use the same context object for multiple format conversions
|
||||
// Use the same context object for multiple format conversions
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -477,20 +499,22 @@ except Exception as e:
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
try {
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
(async () => {
|
||||
try {
|
||||
const context = await session.getContext({ tokens: 2000 });
|
||||
const messages = context.toOpenAI(assistant);
|
||||
|
||||
// Use messages with LLM API
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: messages
|
||||
});
|
||||
// Use messages with LLM API
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: messages
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error getting context: ${error}`);
|
||||
// Handle error appropriately
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting context: ${error}`);
|
||||
// Handle error appropriately
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,6 @@ icon: 'magnifying-glass'
|
|||
|
||||
Honcho's search functionality allows you to find relevant messages and conversations across different scopes - from entire workspaces down to specific peers or sessions.
|
||||
|
||||
## How Search Works
|
||||
|
||||
Search in Honcho is implemented with a two-tier approach:
|
||||
1. **Primary**: PostgreSQL English language full-text search index for intelligent matching
|
||||
2. **Fallback**: Simple string matching for broader coverage
|
||||
|
||||
All search results are returned in a paginated format, making it easy to handle large result sets efficiently.
|
||||
|
||||
## Search Scopes
|
||||
|
||||
### Workspace Search
|
||||
|
|
@ -38,16 +30,18 @@ for result in results:
|
|||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Search across entire workspace
|
||||
const results = await honcho.search("budget planning");
|
||||
// Search across entire workspace
|
||||
const results = await honcho.search("budget planning");
|
||||
|
||||
// Iterate through all results
|
||||
for await (const result of results) {
|
||||
console.log(`Found: ${result}`);
|
||||
}
|
||||
// Iterate through all results
|
||||
for (const result of results) {
|
||||
console.log(`Found: ${result}`);
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -69,16 +63,18 @@ for result in results:
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create or get a session
|
||||
const session = honcho.session("team-meeting-jan");
|
||||
(async () => {
|
||||
// Create or get a session
|
||||
const session = await honcho.session("team-meeting-jan");
|
||||
|
||||
// Search within this session only
|
||||
const results = await session.search("action items");
|
||||
// Search within this session only
|
||||
const results = await session.search("action items");
|
||||
|
||||
// Process results
|
||||
for await (const result of results) {
|
||||
console.log(`Session result: ${result}`);
|
||||
}
|
||||
// Process results
|
||||
for (const result of results) {
|
||||
console.log(`Session result: ${result}`);
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -100,160 +96,90 @@ for result in results:
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create or get a peer
|
||||
const alice = honcho.peer("alice");
|
||||
import { Message } from "@honcho-ai/sdk";
|
||||
|
||||
// Search across all of Alice's messages and interactions
|
||||
const results = await alice.search("programming");
|
||||
(async () => {
|
||||
// Create or get a peer
|
||||
const alice = await honcho.peer("alice");
|
||||
|
||||
// View results
|
||||
for await (const result of results) {
|
||||
console.log(`Alice's content: ${result}`);
|
||||
}
|
||||
// Search across all of Alice's messages and interactions
|
||||
const results: Message[] = await alice.search("programming");
|
||||
|
||||
// View results
|
||||
for (const result of results) {
|
||||
console.log(`Alice's content: ${result.content}`);
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Working with Search Results
|
||||
## Filters and Limits
|
||||
|
||||
### Basic Result Processing
|
||||
### Get a specific number of results
|
||||
|
||||
You can specify the number of results you want to return by passing the `limit` parameter to the search method. The default is 10 results, with a maximum of 100.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Search returns a paginated iterator
|
||||
results = honcho.search("customer feedback")
|
||||
|
||||
# Simple iteration processes all results automatically
|
||||
for result in results:
|
||||
# Each result contains the matched content and context
|
||||
print(f"Match: {result}")
|
||||
|
||||
# Check if there are any results
|
||||
results = honcho.search("nonexistent topic")
|
||||
result_list = list(results)
|
||||
if not result_list:
|
||||
print("No results found")
|
||||
results = honcho.search("budget planning", limit=20)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Search returns a paginated Page object
|
||||
const results = await honcho.search("customer feedback");
|
||||
|
||||
// Iterate through all results
|
||||
for await (const result of results) {
|
||||
// Each result contains the matched content and context
|
||||
console.log(`Match: ${result}`);
|
||||
}
|
||||
|
||||
// Check if there are any results
|
||||
const emptyResults = await honcho.search("nonexistent topic");
|
||||
const resultData = await emptyResults.data();
|
||||
if (resultData.length === 0) {
|
||||
console.log("No results found");
|
||||
}
|
||||
(async () => {
|
||||
const results = await honcho.search("budget planning", { limit: 20 });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Manual Pagination
|
||||
### Get messages from a Peer in a specific Session
|
||||
|
||||
Combine Peer-level search with a `session_id` filter to get messages from a Peer in a specific Session.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# For manual pagination control, you can work with pages directly
|
||||
results = honcho.search("project updates")
|
||||
|
||||
# The iterator handles pagination automatically, but you can also
|
||||
# work with individual batches if needed
|
||||
count = 0
|
||||
for result in results:
|
||||
count += 1
|
||||
print(f"Result {count}: {result}")
|
||||
|
||||
# Stop after first 10 results
|
||||
if count >= 10:
|
||||
break
|
||||
my_peer = honcho.peer("my-peer")
|
||||
my_session = honcho.session("team-meeting-jan")
|
||||
results = my_peer.search("budget planning", filters={"session_id": my_session.id})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Manual pagination with TypeScript
|
||||
let currentPage = await honcho.search("project updates");
|
||||
|
||||
while (currentPage) {
|
||||
const data = await currentPage.data();
|
||||
console.log(`Processing ${data.length} results`);
|
||||
|
||||
for (const result of data) {
|
||||
console.log(`Result: ${result}`);
|
||||
}
|
||||
|
||||
// Get next page
|
||||
currentPage = await currentPage.nextPage();
|
||||
}
|
||||
(async () => {
|
||||
const my_peer = await honcho.peer("my-peer");
|
||||
const my_session = await honcho.session("team-meeting-jan");
|
||||
const results = await my_peer.search("budget planning", { filters: { session_id: my_session.id } });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Search with Context Building
|
||||
### Filter results by time range
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Use search results to build context for LLM interactions
|
||||
def build_context_from_search(query: str, session_id: str):
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Search for relevant past discussions
|
||||
search_results = list(session.search(query))
|
||||
|
||||
if search_results:
|
||||
# Use search results to inform context
|
||||
context_summary = f"Found {len(search_results)} relevant past discussions about '{query}'"
|
||||
|
||||
# Get normal session context
|
||||
session_context = session.get_context(tokens=1500)
|
||||
|
||||
return {
|
||||
"search_summary": context_summary,
|
||||
"session_context": session_context,
|
||||
"search_results": search_results[:3] # Top 3 results
|
||||
}
|
||||
|
||||
return {"message": "No relevant past discussions found"}
|
||||
|
||||
# Build context for a new question
|
||||
context_data = build_context_from_search("user authentication", "support-session-1")
|
||||
print(f"Context: {context_data.get('search_summary', 'No context')}")
|
||||
results = honcho.search("budget planning", filters={"created_at": {"gte": "2024-01-01", "lte": "2024-01-31"}})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Use search results to build context for LLM interactions
|
||||
async function buildContextFromSearch(query: string, sessionId: string) {
|
||||
const session = honcho.session(sessionId);
|
||||
|
||||
// Search for relevant past discussions
|
||||
const searchResults = await session.search(query);
|
||||
const searchData = await searchResults.data();
|
||||
|
||||
if (searchData.length > 0) {
|
||||
// Use search results to inform context
|
||||
const contextSummary = `Found ${searchData.length} relevant past discussions about '${query}'`;
|
||||
|
||||
// Get normal session context
|
||||
const sessionContext = await session.getContext({ tokens: 1500 });
|
||||
|
||||
return {
|
||||
searchSummary: contextSummary,
|
||||
sessionContext: sessionContext,
|
||||
searchResults: searchData.slice(0, 3) // Top 3 results
|
||||
};
|
||||
}
|
||||
|
||||
return { message: "No relevant past discussions found" };
|
||||
}
|
||||
|
||||
// Build context for a new question
|
||||
const contextData = await buildContextFromSearch("user authentication", "support-session-1");
|
||||
console.log(`Context: ${contextData.searchSummary || contextData.message}`);
|
||||
(async () => {
|
||||
const results = await honcho.search("budget planning", { filters: { created_at: { gte: "2024-01-01", lte: "2024-01-31" } } });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Best Practices
|
||||
### Filter results by metadata
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
results = honcho.search("budget planning", filters={"metadata": {"key": "value"}})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
(async () => {
|
||||
const results = await honcho.search("budget planning", { filters: { metadata: { key: "value" } } });
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Best Practices
|
||||
|
||||
### Handle Empty Results Gracefully
|
||||
|
||||
|
|
@ -272,18 +198,21 @@ else:
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Always check for empty results
|
||||
const results = await honcho.search("very specific query");
|
||||
const resultData = await results.data();
|
||||
import { Message } from "@honcho-ai/sdk";
|
||||
|
||||
if (resultData.length > 0) {
|
||||
console.log(`Found ${resultData.length} results`);
|
||||
for (const result of resultData) {
|
||||
console.log(`- ${result}`);
|
||||
}
|
||||
} else {
|
||||
console.log("No results found - try a broader search");
|
||||
}
|
||||
(async () => {
|
||||
// Always check for empty results
|
||||
const results: Message[] = await honcho.search("very specific query");
|
||||
|
||||
if (results.length > 0) {
|
||||
console.log(`Found ${results.length} results`);
|
||||
for (const result of results) {
|
||||
console.log(`- ${result.content}`);
|
||||
}
|
||||
} else {
|
||||
console.log("No results found - try a broader search");
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,23 +48,25 @@ session.add_messages([
|
|||
```typescript TypeScript
|
||||
import { Honcho } from '@honcho-ai/sdk';
|
||||
|
||||
// Initialize client (using the default workspace)
|
||||
const honcho = new Honcho();
|
||||
(async () => {
|
||||
// Initialize client (using the default workspace)
|
||||
const honcho = new Honcho();
|
||||
|
||||
// Create or get peers
|
||||
const user = honcho.peer('demo-user');
|
||||
const assistant = honcho.peer('assistant');
|
||||
// Create or get peers
|
||||
const user = await honcho.peer('demo-user');
|
||||
const assistant = await honcho.peer('assistant');
|
||||
|
||||
// Create a new session
|
||||
const session = honcho.session('demo-session');
|
||||
// Create a new session
|
||||
const session = await honcho.session('demo-session');
|
||||
|
||||
// Add peers to the session
|
||||
await session.addPeers([user, assistant]);
|
||||
// Add peers to the session
|
||||
await session.addPeers([user, assistant]);
|
||||
|
||||
// Store some messages for context (optional)
|
||||
await session.addMessages([
|
||||
user.message("Hello, I'm testing the streaming functionality")
|
||||
]);
|
||||
// Store some messages for context (optional)
|
||||
await session.addMessages([
|
||||
user.message("Hello, I'm testing the streaming functionality")
|
||||
]);
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -83,15 +85,17 @@ for chunk in response_stream.iter_text():
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Basic streaming example
|
||||
const responseStream = await user.chat("What can you tell me about this user?", {
|
||||
stream: true
|
||||
});
|
||||
(async () => {
|
||||
// Basic streaming example
|
||||
const responseStream = await user.chat("What can you tell me about this user?", {
|
||||
stream: true
|
||||
});
|
||||
|
||||
// Process the stream
|
||||
for await (const chunk of responseStream.iter_text()) {
|
||||
process.stdout.write(chunk); // Write to console without newlines
|
||||
}
|
||||
// Process the stream
|
||||
for await (const chunk of responseStream.iter_text()) {
|
||||
process.stdout.write(chunk); // Write to console without newlines
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -171,63 +175,65 @@ if __name__ == "__main__":
|
|||
```typescript TypeScript
|
||||
import { Honcho } from '@honcho-ai/sdk';
|
||||
|
||||
async function restaurantRecommendationChat() {
|
||||
// Initialize client
|
||||
const honcho = new Honcho();
|
||||
(async () => {
|
||||
async function restaurantRecommendationChat() {
|
||||
// Initialize client
|
||||
const honcho = new Honcho();
|
||||
|
||||
// Create peers
|
||||
const user = honcho.peer('food-lover');
|
||||
const assistant = honcho.peer('restaurant-assistant');
|
||||
// Create peers
|
||||
const user = await honcho.peer('food-lover');
|
||||
const assistant = await honcho.peer('restaurant-assistant');
|
||||
|
||||
// Create session
|
||||
const session = honcho.session('food-preferences-session');
|
||||
// Create session
|
||||
const session = await honcho.session('food-preferences-session');
|
||||
|
||||
// Add peers to session
|
||||
await session.addPeers([user, assistant]);
|
||||
// Add peers to session
|
||||
await session.addPeers([user, assistant]);
|
||||
|
||||
// Store multiple user messages about food preferences
|
||||
const userMessages = [
|
||||
"I absolutely love spicy Thai food, especially curries with coconut milk.",
|
||||
"Italian cuisine is another favorite - fresh pasta and wood-fired pizza are my weakness!",
|
||||
"I try to eat vegetarian most of the time, but occasionally enjoy seafood.",
|
||||
"I can't handle overly sweet desserts, but love something with dark chocolate."
|
||||
];
|
||||
// Store multiple user messages about food preferences
|
||||
const userMessages = [
|
||||
"I absolutely love spicy Thai food, especially curries with coconut milk.",
|
||||
"Italian cuisine is another favorite - fresh pasta and wood-fired pizza are my weakness!",
|
||||
"I try to eat vegetarian most of the time, but occasionally enjoy seafood.",
|
||||
"I can't handle overly sweet desserts, but love something with dark chocolate."
|
||||
];
|
||||
|
||||
// Add the user's messages to the session
|
||||
const sessionMessages = userMessages.map(message => user.message(message));
|
||||
await session.addMessages(sessionMessages);
|
||||
// Add the user's messages to the session
|
||||
const sessionMessages = userMessages.map(message => user.message(message));
|
||||
await session.addMessages(sessionMessages);
|
||||
|
||||
// Print the user messages
|
||||
for (const message of userMessages) {
|
||||
console.log(`User: ${message}`);
|
||||
}
|
||||
// Print the user messages
|
||||
for (const message of userMessages) {
|
||||
console.log(`User: ${message}`);
|
||||
}
|
||||
|
||||
// Ask for restaurant recommendations based on preferences
|
||||
console.log("\nRequesting restaurant recommendations...");
|
||||
process.stdout.write("Assistant: ");
|
||||
let fullResponse = "";
|
||||
// Ask for restaurant recommendations based on preferences
|
||||
console.log("\nRequesting restaurant recommendations...");
|
||||
process.stdout.write("Assistant: ");
|
||||
let fullResponse = "";
|
||||
|
||||
// Stream the response using the user's peer to get recommendations
|
||||
const responseStream = await user.chat(
|
||||
"Based on this user's food preferences, recommend 3 restaurants they might enjoy in the Lower East Side.",
|
||||
{
|
||||
stream: true,
|
||||
sessionId: session.id
|
||||
// Stream the response using the user's peer to get recommendations
|
||||
const responseStream = await user.chat(
|
||||
"Based on this user's food preferences, recommend 3 restaurants they might enjoy in the Lower East Side.",
|
||||
{
|
||||
stream: true,
|
||||
sessionId: session.id
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of responseStream.iter_text()) {
|
||||
process.stdout.write(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
|
||||
// Store the assistant's complete response
|
||||
await session.addMessages([
|
||||
assistant.message(fullResponse)
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of responseStream.iter_text()) {
|
||||
process.stdout.write(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
|
||||
// Store the assistant's complete response
|
||||
await session.addMessages([
|
||||
assistant.message(fullResponse)
|
||||
]);
|
||||
}
|
||||
|
||||
restaurantRecommendationChat().catch(console.error);
|
||||
await restaurantRecommendationChat();
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ async def dialectic_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
session = honcho_client.session(id=str(update.effective_chat.id))
|
||||
|
||||
response = peer.chat(
|
||||
queries=query,
|
||||
query=query,
|
||||
session_id=session.id,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,15 +22,15 @@ from honcho import Honcho
|
|||
honcho = Honcho()
|
||||
|
||||
# Simple peer filter
|
||||
peers = honcho.get_peers(filter={"peer_id": "alice"})
|
||||
peers = honcho.get_peers(filters={"peer_id": "alice"})
|
||||
|
||||
# Simple session filter with metadata
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {"type": "support"}
|
||||
})
|
||||
|
||||
# Simple message filter
|
||||
messages = honcho.get_messages(filter={
|
||||
messages = honcho.get_messages(filters={
|
||||
"session_id": "support-chat-1",
|
||||
"peer_id": "alice"
|
||||
})
|
||||
|
|
@ -39,28 +39,30 @@ messages = honcho.get_messages(filter={
|
|||
```typescript TypeScript
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
(async () => {
|
||||
// Initialize client
|
||||
const honcho = new Honcho({});
|
||||
|
||||
// Simple peer filter
|
||||
const peers = await honcho.getPeers({
|
||||
filter: { peerId: "alice" }
|
||||
});
|
||||
// Simple peer filter
|
||||
const peers = await honcho.getPeers({
|
||||
filters: { peerId: "alice" }
|
||||
});
|
||||
|
||||
// Simple session filter with metadata
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: { type: "support" }
|
||||
}
|
||||
});
|
||||
// Simple session filter with metadata
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: { type: "support" }
|
||||
}
|
||||
});
|
||||
|
||||
// Simple message filter
|
||||
const messages = await honcho.getMessages({
|
||||
filter: {
|
||||
sessionId: "support-chat-1",
|
||||
peerId: "alice"
|
||||
}
|
||||
});
|
||||
// Simple message filter
|
||||
const messages = await honcho.getMessages({
|
||||
filters: {
|
||||
sessionId: "support-chat-1",
|
||||
peerId: "alice"
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ Use AND to require all conditions to be true:
|
|||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
messages = honcho.get_messages(filter={
|
||||
messages = honcho.get_messages(filters={
|
||||
"AND": [
|
||||
{"session_id": "chat-1"},
|
||||
{"created_at": {"gte": "2024-01-01"}}
|
||||
|
|
@ -83,14 +85,16 @@ messages = honcho.get_messages(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const messages = await honcho.getMessages({
|
||||
filter: {
|
||||
AND: [
|
||||
{ sessionId: "chat-1" },
|
||||
{ createdAt: { gte: "2024-01-01" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
const messages = await honcho.getMessages({
|
||||
filters: {
|
||||
AND: [
|
||||
{ sessionId: "chat-1" },
|
||||
{ createdAt: { gte: "2024-01-01" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -101,7 +105,7 @@ Use OR to match any of the specified conditions:
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find messages from either alice or bob
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"OR": [
|
||||
{"peer_id": "alice"},
|
||||
{"peer_id": "bob"}
|
||||
|
|
@ -109,7 +113,7 @@ messages = session.get_messages(filter={
|
|||
})
|
||||
|
||||
# Complex OR with metadata conditions
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"OR": [
|
||||
{"metadata": {"priority": "high"}},
|
||||
{"metadata": {"urgent": True}},
|
||||
|
|
@ -119,26 +123,28 @@ sessions = honcho.get_sessions(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find messages from either alice or bob
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
OR: [
|
||||
{ peerId: "alice" },
|
||||
{ peerId: "bob" }
|
||||
]
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find messages from either alice or bob
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
OR: [
|
||||
{ peerId: "alice" },
|
||||
{ peerId: "bob" }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Complex OR with metadata conditions
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
OR: [
|
||||
{ metadata: { priority: "high" } },
|
||||
{ metadata: { urgent: true } },
|
||||
{ metadata: { escalated: true } }
|
||||
]
|
||||
}
|
||||
});
|
||||
// Complex OR with metadata conditions
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
OR: [
|
||||
{ metadata: { priority: "high" } },
|
||||
{ metadata: { urgent: true } },
|
||||
{ metadata: { escalated: true } }
|
||||
]
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -149,14 +155,14 @@ Use NOT to exclude specific conditions:
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find all peers except alice
|
||||
peers = honcho.get_peers(filter={
|
||||
peers = honcho.get_peers(filters={
|
||||
"NOT": [
|
||||
{"peer_id": "alice"}
|
||||
]
|
||||
})
|
||||
|
||||
# Find sessions that are NOT completed
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"NOT": [
|
||||
{"metadata": {"status": "completed"}}
|
||||
]
|
||||
|
|
@ -164,23 +170,25 @@ sessions = honcho.get_sessions(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find all peers except alice
|
||||
const peers = await honcho.getPeers({
|
||||
filter: {
|
||||
NOT: [
|
||||
{ peerId: "alice" }
|
||||
]
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find all peers except alice
|
||||
const peers = await honcho.getPeers({
|
||||
filters: {
|
||||
NOT: [
|
||||
{ peerId: "alice" }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Find sessions that are NOT completed
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
NOT: [
|
||||
{ metadata: { status: "completed" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
// Find sessions that are NOT completed
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
NOT: [
|
||||
{ metadata: { status: "completed" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -191,7 +199,7 @@ Create sophisticated queries by combining different logical operators:
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find messages from alice OR bob, but NOT where message has archived set to true in metadata
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"AND": [
|
||||
{
|
||||
"OR": [
|
||||
|
|
@ -209,24 +217,26 @@ messages = session.get_messages(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find messages from alice OR bob, but NOT where message has archived set to true in metadata
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ peerId: "alice" },
|
||||
{ peerId: "bob" }
|
||||
]
|
||||
},
|
||||
{
|
||||
NOT: [
|
||||
{ metadata: { archived: true } }
|
||||
(async () => {
|
||||
// Find messages from alice OR bob, but NOT where message has archived set to true in metadata
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ peerId: "alice" },
|
||||
{ peerId: "bob" }
|
||||
]
|
||||
},
|
||||
{
|
||||
NOT: [
|
||||
{ metadata: { archived: true } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -239,12 +249,12 @@ Use comparison operators for range queries and advanced matching:
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find sessions created after a specific date
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"created_at": {"gte": "2024-01-01"}
|
||||
})
|
||||
|
||||
# Find messages within a date range
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"created_at": {
|
||||
"gte": "2024-01-01",
|
||||
"lte": "2024-12-31"
|
||||
|
|
@ -252,7 +262,7 @@ messages = session.get_messages(filter={
|
|||
})
|
||||
|
||||
# Metadata numeric comparisons
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {
|
||||
"score": {"gt": 8.5},
|
||||
"duration": {"lte": 3600}
|
||||
|
|
@ -261,32 +271,34 @@ sessions = honcho.get_sessions(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find sessions created after a specific date
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
createdAt: { gte: "2024-01-01" }
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find sessions created after a specific date
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
createdAt: { gte: "2024-01-01" }
|
||||
}
|
||||
});
|
||||
|
||||
// Find messages within a date range
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
createdAt: {
|
||||
gte: "2024-01-01",
|
||||
lte: "2024-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
// Find messages within a date range
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
createdAt: {
|
||||
gte: "2024-01-01",
|
||||
lte: "2024-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Metadata numeric comparisons
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: {
|
||||
score: { gt: 8.5 },
|
||||
duration: { lte: 3600 }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Metadata numeric comparisons
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: {
|
||||
score: { gt: 8.5 },
|
||||
duration: { lte: 3600 }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -295,19 +307,19 @@ const sessions = await honcho.getSessions({
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find messages from specific peers in a session
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"peer_id": {"in": ["alice", "bob", "charlie"]}
|
||||
})
|
||||
|
||||
# Find sessions with specific tags
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {
|
||||
"tag": {"in": ["important", "urgent", "follow-up"]}
|
||||
}
|
||||
})
|
||||
|
||||
# Not equal comparisons
|
||||
peers = honcho.get_peers(filter={
|
||||
peers = honcho.get_peers(filters={
|
||||
"metadata": {
|
||||
"status": {"ne": "inactive"}
|
||||
}
|
||||
|
|
@ -315,30 +327,32 @@ peers = honcho.get_peers(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find messages from specific peers in a session
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
peerId: { in: ["alice", "bob", "charlie"] }
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find messages from specific peers in a session
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
peerId: { in: ["alice", "bob", "charlie"] }
|
||||
}
|
||||
});
|
||||
|
||||
// Find sessions with specific tags
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: {
|
||||
tag: { in: ["important", "urgent", "follow-up"] }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Find sessions with specific tags
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: {
|
||||
tag: { in: ["important", "urgent", "follow-up"] }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Not equal comparisons
|
||||
const peers = await honcho.getPeers({
|
||||
filter: {
|
||||
metadata: {
|
||||
status: { ne: "inactive" }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Not equal comparisons
|
||||
const peers = await honcho.getPeers({
|
||||
filters: {
|
||||
metadata: {
|
||||
status: { ne: "inactive" }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -351,7 +365,7 @@ Metadata filtering is particularly powerful in Honcho, supporting nested conditi
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Simple metadata equality
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {
|
||||
"type": "customer_support",
|
||||
"priority": "high"
|
||||
|
|
@ -359,7 +373,7 @@ sessions = honcho.get_sessions(filter={
|
|||
})
|
||||
|
||||
# Nested metadata objects
|
||||
peers = honcho.get_peers(filter={
|
||||
peers = honcho.get_peers(filters={
|
||||
"metadata": {
|
||||
"profile": {
|
||||
"role": "admin",
|
||||
|
|
@ -370,27 +384,29 @@ peers = honcho.get_peers(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Simple metadata equality
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: {
|
||||
type: "customer_support",
|
||||
priority: "high"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Nested metadata objects
|
||||
const peers = await honcho.getPeers({
|
||||
filter: {
|
||||
metadata: {
|
||||
profile: {
|
||||
role: "admin",
|
||||
department: "engineering"
|
||||
(async () => {
|
||||
// Simple metadata equality
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: {
|
||||
type: "customer_support",
|
||||
priority: "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Nested metadata objects
|
||||
const peers = await honcho.getPeers({
|
||||
filters: {
|
||||
metadata: {
|
||||
profile: {
|
||||
role: "admin",
|
||||
department: "engineering"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -403,7 +419,7 @@ If you want to do advanced queries like these, make sure not to create metadata
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Metadata with comparison operators
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {
|
||||
"score": {"gte": 4.0, "lte": 5.0},
|
||||
"created_by": {"ne": "system"},
|
||||
|
|
@ -412,7 +428,7 @@ sessions = honcho.get_sessions(filter={
|
|||
})
|
||||
|
||||
# Complex metadata conditions
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"AND": [
|
||||
{"metadata": {"sentiment": {"in": ["positive", "neutral"]}}},
|
||||
{"metadata": {"confidence": {"gt": 0.8}}},
|
||||
|
|
@ -422,27 +438,29 @@ messages = session.get_messages(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Metadata with comparison operators
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: {
|
||||
score: { gte: 4.0, lte: 5.0 },
|
||||
createdBy: { ne: "system" },
|
||||
tags: { contains: "important" }
|
||||
}
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Metadata with comparison operators
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: {
|
||||
score: { gte: 4.0, lte: 5.0 },
|
||||
createdBy: { ne: "system" },
|
||||
tags: { contains: "important" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Complex metadata conditions
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
AND: [
|
||||
{ metadata: { sentiment: { in: ["positive", "neutral"] } } },
|
||||
{ metadata: { confidence: { gt: 0.8 } } },
|
||||
{ content: { icontains: "thank" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
// Complex metadata conditions
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
AND: [
|
||||
{ metadata: { sentiment: { in: ["positive", "neutral"] } } },
|
||||
{ metadata: { confidence: { gt: 0.8 } } },
|
||||
{ content: { icontains: "thank" } }
|
||||
]
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -453,17 +471,17 @@ Use wildcards (*) to match any value for a field:
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find all sessions with any peer_id (essentially all sessions)
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"peer_id": "*"
|
||||
})
|
||||
|
||||
# Wildcard in lists - matches everything
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"peer_id": {"in": ["alice", "bob", "*"]}
|
||||
})
|
||||
|
||||
# Metadata wildcards
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"metadata": {
|
||||
"type": "*", # Any type
|
||||
"status": "active" # But status must be active
|
||||
|
|
@ -472,29 +490,31 @@ sessions = honcho.get_sessions(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find all sessions with any peer_id (essentially all sessions)
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
peerId: "*"
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find all sessions with any peer_id (essentially all sessions)
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
peerId: "*"
|
||||
}
|
||||
});
|
||||
|
||||
// Wildcard in lists - matches everything
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
peerId: { in: ["alice", "bob", "*"] }
|
||||
}
|
||||
});
|
||||
// Wildcard in lists - matches everything
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
peerId: { in: ["alice", "bob", "*"] }
|
||||
}
|
||||
});
|
||||
|
||||
// Metadata wildcards
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
metadata: {
|
||||
type: "*", // Any type
|
||||
status: "active" // But status must be active
|
||||
}
|
||||
}
|
||||
});
|
||||
// Metadata wildcards
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
metadata: {
|
||||
type: "*", // Any type
|
||||
status: "active" // But status must be active
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -505,12 +525,12 @@ const sessions = await honcho.getSessions({
|
|||
<CodeGroup>
|
||||
```python Python
|
||||
# Find workspaces by name pattern
|
||||
workspaces = honcho.get_workspaces(filter={
|
||||
workspaces = honcho.get_workspaces(filters={
|
||||
"name": {"contains": "prod"}
|
||||
})
|
||||
|
||||
# Filter by metadata
|
||||
workspaces = honcho.get_workspaces(filter={
|
||||
workspaces = honcho.get_workspaces(filters={
|
||||
"metadata": {
|
||||
"environment": "production",
|
||||
"team": {"in": ["backend", "frontend", "devops"]}
|
||||
|
|
@ -519,22 +539,24 @@ workspaces = honcho.get_workspaces(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find workspaces by name pattern
|
||||
const workspaces = await honcho.getWorkspaces({
|
||||
filter: {
|
||||
name: { contains: "prod" }
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find workspaces by name pattern
|
||||
const workspaces = await honcho.getWorkspaces({
|
||||
filters: {
|
||||
name: { contains: "prod" }
|
||||
}
|
||||
});
|
||||
|
||||
// Filter by metadata
|
||||
const workspaces = await honcho.getWorkspaces({
|
||||
filter: {
|
||||
metadata: {
|
||||
environment: "production",
|
||||
team: { in: ["backend", "frontend", "devops"] }
|
||||
}
|
||||
}
|
||||
});
|
||||
// Filter by metadata
|
||||
const workspaces = await honcho.getWorkspaces({
|
||||
filters: {
|
||||
metadata: {
|
||||
environment: "production",
|
||||
team: { in: ["backend", "frontend", "devops"] }
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -546,7 +568,7 @@ const workspaces = await honcho.getWorkspaces({
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
week_ago = (datetime.now() - timedelta(days=7)).isoformat()
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"AND": [
|
||||
{"content": {"icontains": "error"}},
|
||||
{"created_at": {"gte": week_ago}},
|
||||
|
|
@ -555,7 +577,7 @@ messages = session.get_messages(filter={
|
|||
})
|
||||
|
||||
# Find messages in specific sessions with sentiment analysis
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"AND": [
|
||||
{"session_id": {"in": ["support-1", "support-2", "support-3"]}},
|
||||
{"metadata": {"sentiment": "negative"}},
|
||||
|
|
@ -565,28 +587,30 @@ messages = session.get_messages(filter={
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Find error messages from the last week
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
AND: [
|
||||
{ content: { icontains: "error" } },
|
||||
{ createdAt: { gte: weekAgo } },
|
||||
{ metadata: { level: { in: ["error", "critical"] } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
(async () => {
|
||||
// Find error messages from the last week
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
AND: [
|
||||
{ content: { icontains: "error" } },
|
||||
{ createdAt: { gte: weekAgo } },
|
||||
{ metadata: { level: { in: ["error", "critical"] } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Find messages in specific sessions with sentiment analysis
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
AND: [
|
||||
{ sessionId: { in: ["support-1", "support-2", "support-3"] } },
|
||||
{ metadata: { sentiment: "negative" } },
|
||||
{ metadata: { confidence: { gte: 0.7 } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
// Find messages in specific sessions with sentiment analysis
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
AND: [
|
||||
{ sessionId: { in: ["support-1", "support-2", "support-3"] } },
|
||||
{ metadata: { sentiment: "negative" } },
|
||||
{ metadata: { confidence: { gte: 0.7 } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
@ -600,7 +624,7 @@ from honcho.exceptions import FilterError
|
|||
|
||||
try:
|
||||
# Invalid filter - unsupported operator
|
||||
messages = session.get_messages(filter={
|
||||
messages = session.get_messages(filters={
|
||||
"created_at": {"invalid_operator": "2024-01-01"}
|
||||
})
|
||||
except FilterError as e:
|
||||
|
|
@ -609,7 +633,7 @@ except FilterError as e:
|
|||
|
||||
try:
|
||||
# Invalid column name
|
||||
sessions = honcho.get_sessions(filter={
|
||||
sessions = honcho.get_sessions(filters={
|
||||
"nonexistent_field": "value"
|
||||
})
|
||||
except FilterError as e:
|
||||
|
|
@ -617,30 +641,32 @@ except FilterError as e:
|
|||
```
|
||||
|
||||
```typescript TypeScript
|
||||
try {
|
||||
// Invalid filter - unsupported operator
|
||||
const messages = await session.getMessages({
|
||||
filter: {
|
||||
createdAt: { invalidOperator: "2024-01-01" }
|
||||
(async () => {
|
||||
try {
|
||||
// Invalid filter - unsupported operator
|
||||
const messages = await session.getMessages({
|
||||
filters: {
|
||||
createdAt: { invalidOperator: "2024-01-01" }
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("filters")) {
|
||||
console.error(`Filter error: ${error.message}`);
|
||||
// Handle the error appropriately
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("filter")) {
|
||||
console.error(`Filter error: ${error.message}`);
|
||||
// Handle the error appropriately
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Invalid column name
|
||||
const sessions = await honcho.getSessions({
|
||||
filter: {
|
||||
nonexistentField: "value"
|
||||
try {
|
||||
// Invalid column name
|
||||
const sessions = await honcho.getSessions({
|
||||
filters: {
|
||||
nonexistentField: "value"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Invalid field: ${error.message}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Invalid field: ${error.message}`);
|
||||
}
|
||||
})();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ paths:
|
|||
Get a Workspace by ID.
|
||||
|
||||
If workspace_id is provided as a query parameter, it uses that (must match JWT workspace_id).
|
||||
Otherwise, it uses the workspace_id from the JWT token.
|
||||
Otherwise, it uses the workspace_id from the JWT.
|
||||
operationId: get_or_create_workspace_v2_workspaces_post
|
||||
requestBody:
|
||||
content:
|
||||
|
|
@ -480,7 +480,7 @@ paths:
|
|||
Get a Peer by ID
|
||||
|
||||
If peer_id is provided as a query parameter, it uses that (must match JWT workspace_id).
|
||||
Otherwise, it uses the peer_id from the JWT token.
|
||||
Otherwise, it uses the peer_id from the JWT.
|
||||
operationId: get_or_create_peer_v2_workspaces__workspace_id__peers_post
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
|
|
@ -972,7 +972,7 @@ paths:
|
|||
Get a specific session in a workspace.
|
||||
|
||||
If session_id is provided as a query parameter, it verifies the session is in the workspace.
|
||||
Otherwise, it uses the session_id from the JWT token for verification.
|
||||
Otherwise, it uses the session_id from the JWT for verification.
|
||||
operationId: get_or_create_session_v2_workspaces__workspace_id__sessions_post
|
||||
security:
|
||||
- HTTPBearer: []
|
||||
|
|
@ -2114,7 +2114,7 @@ paths:
|
|||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
title: >-
|
||||
Response Create Messages With File V2 Workspaces Workspace Id Sessions Session Id
|
||||
Response Create Messages With File V2 Workspaces Workspace Id Sessions Session Id
|
||||
Messages Upload Post
|
||||
'422':
|
||||
description: Validation Error
|
||||
|
|
@ -2677,7 +2677,7 @@ components:
|
|||
title: MessageCreate
|
||||
MessageGet:
|
||||
properties:
|
||||
filter:
|
||||
filters:
|
||||
anyOf:
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
|
|
@ -2879,7 +2879,7 @@ components:
|
|||
title: PeerCreate
|
||||
PeerGet:
|
||||
properties:
|
||||
filter:
|
||||
filters:
|
||||
anyOf:
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
|
|
@ -3032,7 +3032,7 @@ components:
|
|||
title: SessionDeriverStatus
|
||||
SessionGet:
|
||||
properties:
|
||||
filter:
|
||||
filters:
|
||||
anyOf:
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
|
|
@ -3142,7 +3142,7 @@ components:
|
|||
title: WorkspaceCreate
|
||||
WorkspaceGet:
|
||||
properties:
|
||||
filter:
|
||||
filters:
|
||||
anyOf:
|
||||
- additionalProperties: true
|
||||
type: object
|
||||
|
|
@ -3169,4 +3169,4 @@ components:
|
|||
securitySchemes:
|
||||
HTTPBearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
scheme: bearer
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ dist/
|
|||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
## Quickstart: Use the Hosted MCP Server
|
||||
|
||||
Go to https://app.honcho.dev and get an API key. Then go to Claude Desktop and navigate to custom MCP servers.
|
||||
Go to <https://app.honcho.dev> and get an API key. Then go to Claude Desktop and navigate to custom MCP servers.
|
||||
|
||||
If you don't have node/bun installed you will need to do that. You can also use npm if you already have that installed. If not, Claude Desktop or Claude Code can help!
|
||||
|
||||
Add Honcho to your Claude desktop config. You must provide a username for Honcho to refer to you as -- preferably what you want Claude to actually call you.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
|
|
@ -62,18 +63,22 @@ You may customize your assistant name and/or workspace ID. Both are optional.
|
|||
## Available Tools
|
||||
|
||||
### start_conversation
|
||||
|
||||
Start a new conversation session with Honcho. This initializes a session for tracking conversation history and context.
|
||||
|
||||
**Returns:** A session ID that you must store and use for all subsequent interactions in this conversation.
|
||||
|
||||
### add_turn
|
||||
|
||||
Add a conversation turn (user and assistant messages) to the current session. This stores the conversation in Honcho for context tracking.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to add the turn to
|
||||
- `messages`: Array of message objects with `role` ("user" or "assistant") and `content`
|
||||
|
||||
**Example usage:**
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "session-uuid",
|
||||
|
|
@ -83,7 +88,7 @@ Add a conversation turn (user and assistant messages) to the current session. Th
|
|||
"content": "Hello, how are you?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"role": "assistant",
|
||||
"content": "I'm doing well, thank you!"
|
||||
}
|
||||
]
|
||||
|
|
@ -91,153 +96,195 @@ Add a conversation turn (user and assistant messages) to the current session. Th
|
|||
```
|
||||
|
||||
### get_personalization_insights
|
||||
|
||||
Get personalization insights from Honcho based on conversation history. This queries the user's conversation context to provide personalized responses.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session for context
|
||||
- `query`: The question about the user's preferences, habits, etc.
|
||||
|
||||
**Example queries:**
|
||||
|
||||
- "What does this message reveal about the user's communication preferences?"
|
||||
- "How formal or casual should I be with the user based on our history?"
|
||||
- "What emotional state might the user be in right now?"
|
||||
|
||||
### search_workspace
|
||||
|
||||
Search for messages across the entire workspace.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `query`: The search query to use
|
||||
|
||||
### get_workspace_metadata
|
||||
|
||||
Get metadata for the current workspace.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
### set_workspace_metadata
|
||||
|
||||
Set metadata for the current workspace.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `metadata`: A dictionary of metadata to associate with the workspace
|
||||
|
||||
### create_peer
|
||||
|
||||
Create or get a peer with the specified ID and optional configuration.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `peer_id`: Unique identifier for the peer
|
||||
- `config`: Optional configuration dictionary for the peer
|
||||
|
||||
### get_peer_metadata
|
||||
|
||||
Get metadata for a specific peer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `peer_id`: The ID of the peer to get metadata for
|
||||
|
||||
### set_peer_metadata
|
||||
|
||||
Set metadata for a specific peer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `peer_id`: The ID of the peer to set metadata for
|
||||
- `metadata`: A dictionary of metadata to associate with the peer
|
||||
|
||||
### search_peer_messages
|
||||
|
||||
Search for messages sent by a peer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `peer_id`: The ID of the peer to search messages for
|
||||
- `query`: The search query to use
|
||||
|
||||
### chat
|
||||
|
||||
Query a peer's representation with natural language questions.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `peer_id`: The ID of the peer to query
|
||||
- `query`: The natural language question to ask
|
||||
- `target_peer_id`: Optional target peer ID for local representation queries
|
||||
- `session_id`: Optional session ID to scope the query to a specific session
|
||||
|
||||
### list_peers
|
||||
|
||||
Get all peers in the current workspace.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
### create_session
|
||||
|
||||
Create or get a session with the specified ID and optional configuration.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: Unique identifier for the session
|
||||
- `config`: Optional configuration dictionary for the session
|
||||
|
||||
### get_session_metadata
|
||||
|
||||
Get metadata for a specific session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to get metadata for
|
||||
|
||||
### set_session_metadata
|
||||
|
||||
Set metadata for a specific session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to set metadata for
|
||||
- `metadata`: A dictionary of metadata to associate with the session
|
||||
|
||||
### add_peers_to_session
|
||||
|
||||
Add peers to a session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to add peers to
|
||||
- `peer_ids`: List of peer IDs to add to the session
|
||||
|
||||
### remove_peers_from_session
|
||||
|
||||
Remove peers from a session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to remove peers from
|
||||
- `peer_ids`: List of peer IDs to remove from the session
|
||||
|
||||
### get_session_peers
|
||||
|
||||
Get all peer IDs in a session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to get peers from
|
||||
|
||||
### add_messages_to_session
|
||||
|
||||
Add messages to a session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to add messages to
|
||||
- `messages`: List of message dictionaries with `peer_id`, `content`, and optional `metadata`
|
||||
|
||||
### get_session_messages
|
||||
|
||||
Get messages from a session with optional filtering.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to get messages from
|
||||
- `filters`: Optional dictionary of filter criteria
|
||||
|
||||
### get_session_context
|
||||
|
||||
Get optimized context for a session within a token limit.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to get context for
|
||||
- `summary`: Whether to include summary information (default: true)
|
||||
- `tokens`: Maximum number of tokens to include in the context
|
||||
|
||||
### search_session_messages
|
||||
|
||||
Search for messages in a specific session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session to search messages in
|
||||
- `query`: The search query to use
|
||||
|
||||
### get_working_representation
|
||||
|
||||
Get the current working representation of a peer in a session.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `session_id`: The ID of the session
|
||||
- `peer_id`: The ID of the peer to get the working representation of
|
||||
- `target_peer_id`: Optional target peer ID to get the representation of what peer_id knows about target_peer_id
|
||||
|
||||
### list_sessions
|
||||
|
||||
Get all sessions in the current workspace.
|
||||
|
||||
**Parameters:** None
|
||||
|
|
@ -249,11 +296,13 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
|
|||
### Deploy MCP Worker
|
||||
|
||||
1. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
bun i
|
||||
```
|
||||
|
||||
2. **Login to Cloudflare (if not already done):**
|
||||
|
||||
```bash
|
||||
bun wrangler login
|
||||
```
|
||||
|
|
@ -263,11 +312,13 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
|
|||
- Update the worker names in the `[env.production]` and `[env.staging]` sections
|
||||
|
||||
4. **Test locally:**
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
5. **Deploy to production:**
|
||||
|
||||
```bash
|
||||
bun run deploy
|
||||
```
|
||||
|
|
@ -277,13 +328,14 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
|
|||
You can customize the behavior using HTTP headers:
|
||||
|
||||
**Available Configuration:**
|
||||
|
||||
- `apiKey`: Your Honcho API key
|
||||
- `baseUrl`: Custom Honcho API base URL (default: https://api.honcho.dev)
|
||||
- `baseUrl`: Custom Honcho API base URL (default: <https://api.honcho.dev>)
|
||||
- `workspaceId`: Workspace ID (default: "default")
|
||||
- `userName`: User identifier (default: "User")
|
||||
- `assistantName`: Assistant identifier (default: "Assistant")
|
||||
|
||||
#### Using HTTP Headers:
|
||||
#### Using HTTP Headers
|
||||
|
||||
Pass configuration to mcp-remote via custom headers:
|
||||
|
||||
|
|
@ -297,6 +349,7 @@ bunx mcp-remote https://YOUR_WORKER_NAME.YOUR_SUBDOMAIN.workers.dev \
|
|||
```
|
||||
|
||||
**Supported Custom Headers:**
|
||||
|
||||
- `Authorization: Bearer YOUR_API_KEY` - Your Honcho API key
|
||||
- `X-Honcho-Base-URL` - Custom Honcho API base URL
|
||||
- `X-Honcho-Workspace-ID` - Workspace identifier
|
||||
|
|
@ -326,6 +379,7 @@ The server provides proper JSON-RPC 2.0 error responses:
|
|||
- `-32603`: Internal error
|
||||
|
||||
Common issues:
|
||||
|
||||
- **Missing API key**: Ensure you provide a valid Honcho API key via header or URL parameter
|
||||
- **Invalid tool parameters**: Check that required parameters are provided and properly formatted
|
||||
- **Network errors**: Verify the worker is deployed and accessible
|
||||
- **Network errors**: Verify the worker is deployed and accessible
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ For subsequent messages in the same conversation:
|
|||
session_id: "session_abc123"
|
||||
messages: [
|
||||
{
|
||||
"role": "user",
|
||||
"role": "user",
|
||||
"content": "Thanks for listening. Can you help me prioritize my tasks?"
|
||||
},
|
||||
{
|
||||
|
|
@ -153,4 +153,4 @@ Ask theory-of-mind questions that reveal:
|
|||
3. **Use `get_personalization_insights` strategically for better responses**
|
||||
4. **Ask thoughtful theory-of-mind questions**
|
||||
5. **Never expose technical details to the user**
|
||||
6. **The system maintains context automatically between sessions**
|
||||
6. **The system maintains context automatically between sessions**
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@
|
|||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1341,4 +1341,4 @@ export default {
|
|||
return createErrorResponse(requestData.id ?? null, -32603, errorMessage);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ compatibility_flags = ["nodejs_compat"]
|
|||
name = "honcho-mcp"
|
||||
|
||||
[env.staging]
|
||||
name = "honcho-mcp-staging"
|
||||
name = "honcho-mcp-staging"
|
||||
|
||||
[observability.logs]
|
||||
enabled = true
|
||||
enabled = true
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ def upgrade() -> None:
|
|||
# Step 2: Get unique workspace-peer combinations that have orphaned messages
|
||||
workspace_peer_result = conn.execute(
|
||||
sa.text(f"""
|
||||
SELECT DISTINCT workspace_name, peer_name
|
||||
FROM {schema}.messages
|
||||
SELECT DISTINCT workspace_name, peer_name
|
||||
FROM {schema}.messages
|
||||
WHERE session_name IS NULL
|
||||
""")
|
||||
)
|
||||
|
|
@ -109,9 +109,9 @@ def upgrade() -> None:
|
|||
# Step 4: Assign orphaned messages for this peer to the default session
|
||||
op.execute(
|
||||
sa.text(f"""
|
||||
UPDATE {schema}.messages
|
||||
UPDATE {schema}.messages
|
||||
SET session_name = '{default_session_name}'
|
||||
WHERE workspace_name = '{workspace_name}'
|
||||
WHERE workspace_name = '{workspace_name}'
|
||||
AND peer_name = '{peer_name}'
|
||||
AND session_name IS NULL
|
||||
""")
|
||||
|
|
@ -120,9 +120,9 @@ def upgrade() -> None:
|
|||
# Step 4.5: Handle orphaned message embeddings for this peer
|
||||
op.execute(
|
||||
sa.text(f"""
|
||||
UPDATE {schema}.message_embeddings
|
||||
UPDATE {schema}.message_embeddings
|
||||
SET session_name = '{default_session_name}'
|
||||
WHERE workspace_name = '{workspace_name}'
|
||||
WHERE workspace_name = '{workspace_name}'
|
||||
AND peer_name = '{peer_name}'
|
||||
AND session_name IS NULL
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
"""add webhooks table
|
||||
|
||||
Revision ID: 88b0fb10906f
|
||||
Revises: 05486ce795d5
|
||||
Create Date: 2025-07-25 16:12:11.015327
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from migrations.utils import (
|
||||
column_exists,
|
||||
constraint_exists,
|
||||
index_exists,
|
||||
table_exists,
|
||||
)
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "88b0fb10906f"
|
||||
down_revision: str | None = "05486ce795d5"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Add webhook_endpoints table
|
||||
op.create_table(
|
||||
"webhook_endpoints",
|
||||
sa.Column(
|
||||
"id",
|
||||
sa.TEXT(),
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"workspace_name",
|
||||
sa.TEXT(),
|
||||
sa.ForeignKey("workspaces.name"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("url", sa.TEXT(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.CheckConstraint("length(url) <= 2048", name="webhook_endpoint_url_length"),
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
op.create_index(
|
||||
op.f("idx_webhook_endpoints_workspace_lookup"),
|
||||
"webhook_endpoints",
|
||||
["workspace_name"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# 2. Add columns to queue table
|
||||
op.add_column(
|
||||
"queue",
|
||||
sa.Column("task_type", sa.TEXT(), nullable=True),
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"queue",
|
||||
sa.Column("work_unit_key", sa.Text(), nullable=True),
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# 2.5 Backfill task_type and work_unit_key for existing queue items (batched)
|
||||
op.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
DO $$
|
||||
DECLARE
|
||||
rows_updated INT;
|
||||
BEGIN
|
||||
LOOP
|
||||
UPDATE {schema}.queue
|
||||
SET
|
||||
task_type = COALESCE(payload->>'task_type', 'representation'),
|
||||
work_unit_key =
|
||||
COALESCE(payload->>'task_type', 'representation') || ':' ||
|
||||
COALESCE(payload->>'workspace_name', 'None') || ':' ||
|
||||
COALESCE(payload->>'session_name', 'None') || ':' ||
|
||||
COALESCE(payload->>'sender_name', 'None') || ':' ||
|
||||
COALESCE(payload->>'target_name', 'None')
|
||||
WHERE id IN (
|
||||
SELECT id FROM {schema}.queue
|
||||
WHERE task_type IS NULL OR work_unit_key IS NULL
|
||||
LIMIT 1000
|
||||
);
|
||||
|
||||
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
||||
EXIT WHEN rows_updated = 0;
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Make both columns non-nullable
|
||||
op.alter_column("queue", "task_type", nullable=False, schema=schema)
|
||||
op.alter_column("queue", "work_unit_key", nullable=False, schema=schema)
|
||||
|
||||
# 3. Alter active queue sessions table
|
||||
op.add_column(
|
||||
"active_queue_sessions",
|
||||
sa.Column("work_unit_key", sa.Text(), index=True),
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# Add unique constraint for work_unit_key
|
||||
op.create_unique_constraint(
|
||||
"unique_work_unit_key",
|
||||
"active_queue_sessions",
|
||||
["work_unit_key"],
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
|
||||
if constraint_exists(
|
||||
"active_queue_sessions", "unique_active_queue_session", "unique", inspector
|
||||
):
|
||||
op.drop_constraint(
|
||||
"unique_active_queue_session", "active_queue_sessions", schema=schema
|
||||
)
|
||||
|
||||
if column_exists("active_queue_sessions", "session_id", inspector):
|
||||
op.drop_column("active_queue_sessions", "session_id", schema=schema)
|
||||
|
||||
if column_exists("active_queue_sessions", "sender_name", inspector):
|
||||
op.drop_column("active_queue_sessions", "sender_name", schema=schema)
|
||||
|
||||
if column_exists("active_queue_sessions", "target_name", inspector):
|
||||
op.drop_column("active_queue_sessions", "target_name", schema=schema)
|
||||
|
||||
if column_exists("active_queue_sessions", "task_type", inspector):
|
||||
op.drop_column("active_queue_sessions", "task_type", schema=schema)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
|
||||
if table_exists("webhook_endpoints", inspector):
|
||||
if index_exists(
|
||||
"webhook_endpoints", "idx_webhook_endpoints_workspace_lookup", inspector
|
||||
):
|
||||
op.drop_index(
|
||||
op.f("idx_webhook_endpoints_workspace_lookup"),
|
||||
table_name="webhook_endpoints",
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
op.drop_table("webhook_endpoints", schema=schema)
|
||||
|
||||
if column_exists("queue", "task_type", inspector):
|
||||
op.drop_column("queue", "task_type", schema=schema)
|
||||
|
||||
if column_exists("queue", "work_unit_key", inspector):
|
||||
op.drop_column("queue", "work_unit_key", schema=schema)
|
||||
|
||||
# Drop unique constraint first if it exists
|
||||
if constraint_exists(
|
||||
"active_queue_sessions", "unique_work_unit_key", "unique", inspector
|
||||
):
|
||||
op.drop_constraint(
|
||||
"unique_work_unit_key", "active_queue_sessions", schema=schema
|
||||
)
|
||||
|
||||
if column_exists("active_queue_sessions", "work_unit_key", inspector):
|
||||
op.drop_column("active_queue_sessions", "work_unit_key", schema=schema)
|
||||
|
||||
if column_exists("active_queue_sessions", "work_unit_data", inspector):
|
||||
op.drop_column("active_queue_sessions", "work_unit_data", schema=schema)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
@ -28,11 +28,12 @@ dependencies = [
|
|||
"pydantic-settings>=2.10.1",
|
||||
"google-generativeai>=0.8.5",
|
||||
"pdfplumber>=0.11.7",
|
||||
"typing-extensions>=4.11.0",
|
||||
]
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"honcho-core>=1.2.0",
|
||||
"honcho-ai>=1.2.1",
|
||||
"honcho-core==1.3.0",
|
||||
"honcho-ai==1.3.0",
|
||||
"pytest>=8.2.2",
|
||||
"sqlalchemy-utils>=0.41.2",
|
||||
"pytest-asyncio>=0.23.7",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ def main():
|
|||
print(f"Generated JWT secret: {secret}")
|
||||
print("\nAdd this to your .env file as:")
|
||||
print(f"AUTH_JWT_SECRET={secret}")
|
||||
print(f"or as WEBHOOK_SECRET={secret}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -168,10 +168,13 @@ TYPESCRIPT_VERSION=
|
|||
for section in sections:
|
||||
if line.strip() == f"### {section}":
|
||||
# If we have a previous section, decide whether to keep it
|
||||
if current_section is not None and section_start_idx != -1:
|
||||
if section_has_content:
|
||||
# Keep the section
|
||||
cleaned_lines.extend(lines[section_start_idx:i])
|
||||
if (
|
||||
current_section is not None
|
||||
and section_start_idx != -1
|
||||
and section_has_content
|
||||
):
|
||||
# Keep the section
|
||||
cleaned_lines.extend(lines[section_start_idx:i])
|
||||
# Start tracking new section
|
||||
current_section = section
|
||||
section_start_idx = i
|
||||
|
|
@ -179,15 +182,21 @@ TYPESCRIPT_VERSION=
|
|||
is_section_header = True
|
||||
break
|
||||
|
||||
if not is_section_header and current_section is not None:
|
||||
# Check if this line has content (not empty and not just whitespace)
|
||||
if line.strip() and not line.strip().startswith("#"):
|
||||
section_has_content = True
|
||||
if (
|
||||
not is_section_header
|
||||
and current_section is not None
|
||||
and line.strip()
|
||||
and not line.strip().startswith("#")
|
||||
):
|
||||
section_has_content = True
|
||||
|
||||
# Handle the last section
|
||||
if current_section is not None and section_start_idx != -1:
|
||||
if section_has_content:
|
||||
cleaned_lines.extend(lines[section_start_idx:])
|
||||
if (
|
||||
current_section is not None
|
||||
and section_start_idx != -1
|
||||
and section_has_content
|
||||
):
|
||||
cleaned_lines.extend(lines[section_start_idx:])
|
||||
|
||||
# If no sections were found, return original
|
||||
if not cleaned_lines and "###" not in changelog:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,22 @@ 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/).
|
||||
|
||||
## [1.3.0] - 2025-08-04
|
||||
|
||||
### Added
|
||||
|
||||
- Added get_peer_config to sessions module
|
||||
|
||||
### Changed
|
||||
|
||||
- Summaries are now included in `toOpenAI` and `toAnthropic` functions
|
||||
- `SessionContext.__len__` now counts the summary in its total
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added missing metadata inputs in many places
|
||||
- Better documentation all over
|
||||
|
||||
## [1.2.2] - 2025-07-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
description = "Official DX Optimized Python SDK for Honcho"
|
||||
dynamic = ["readme"]
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -8,7 +8,7 @@ authors = [
|
|||
{ name = "Plastic Labs", email = "hello@plasticlabs.ai" },
|
||||
]
|
||||
dependencies = [
|
||||
"honcho-core>=1.2.0",
|
||||
"honcho-core==1.3.0",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -27,10 +27,14 @@ class AsyncHoncho(BaseModel):
|
|||
from environment variables or explicit parameters. This is the primary entry
|
||||
point for interacting with the Honcho conversational memory platform asynchronously.
|
||||
|
||||
For advanced usage, the underlying honcho_core client can be accessed via the
|
||||
`core` property to use functionality not exposed through this SDK.
|
||||
|
||||
Attributes:
|
||||
api_key: API key for authentication
|
||||
base_url: Base URL for the Honcho API
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow") # pyright: ignore
|
||||
|
|
@ -42,6 +46,26 @@ class AsyncHoncho(BaseModel):
|
|||
)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def core(self) -> AsyncHonchoCore:
|
||||
"""
|
||||
Access the underlying honcho_core client. The honcho_core client is the raw Stainless-generated client,
|
||||
allowing users to access functionality that is not exposed through this SDK.
|
||||
|
||||
Returns:
|
||||
The underlying AsyncHonchoCore client instance
|
||||
|
||||
Example:
|
||||
```python
|
||||
from honcho import AsyncHoncho
|
||||
|
||||
client = AsyncHoncho()
|
||||
|
||||
workspace = await client.core.workspaces.get_or_create(id="custom-workspace-id")
|
||||
```
|
||||
"""
|
||||
return self._client
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -138,6 +162,10 @@ class AsyncHoncho(BaseModel):
|
|||
..., min_length=1, description="Unique identifier for the peer"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this peer. If set, will get/create peer immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
|
||||
|
|
@ -147,14 +175,16 @@ class AsyncHoncho(BaseModel):
|
|||
Get or create a peer with the given ID.
|
||||
|
||||
Creates an AsyncPeer object that can be used to interact with the specified peer.
|
||||
This method does not make an API call - the peer is created lazily when
|
||||
its methods are first used.
|
||||
This method does not make an API call unless `config` or `metadata` is
|
||||
provided.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for the peer within the workspace. Should be a
|
||||
stable identifier that can be used consistently across sessions
|
||||
config:
|
||||
Optional configuration to set for this peer. If set, will get/create peer immediately with flags.
|
||||
stable identifier that can be used consistently across sessions
|
||||
metadata: Optional metadata dictionary to associate with this peer.
|
||||
If set, will get/create peer immediately with metadata.
|
||||
config: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
|
||||
Returns:
|
||||
An AsyncPeer object that can be used to send messages, join sessions, and
|
||||
|
|
@ -163,14 +193,14 @@ class AsyncHoncho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the peer ID is empty or invalid
|
||||
"""
|
||||
if config:
|
||||
if config or metadata:
|
||||
return await AsyncPeer.create(
|
||||
id, self.workspace_id, self._client, config=config
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
return AsyncPeer(id, self.workspace_id, self._client)
|
||||
|
||||
async def get_peers(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncPeer]:
|
||||
"""
|
||||
Get all peers in the current workspace.
|
||||
|
|
@ -180,11 +210,10 @@ class AsyncHoncho(BaseModel):
|
|||
inner client Peer objects to SDK AsyncPeer objects as they are consumed.
|
||||
|
||||
Returns:
|
||||
An AsyncPage of AsyncPeer objects representing all peers in the workspace.
|
||||
The page preserves pagination functionality while transforming objects
|
||||
An AsyncPage of AsyncPeer objects representing all peers in the workspace
|
||||
"""
|
||||
peers_page = await self._client.workspaces.peers.list(
|
||||
workspace_id=self.workspace_id, filter=filter
|
||||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return AsyncPage(
|
||||
peers_page, lambda peer: AsyncPeer(peer.id, self.workspace_id, self._client)
|
||||
|
|
@ -197,6 +226,10 @@ class AsyncHoncho(BaseModel):
|
|||
..., min_length=1, description="Unique identifier for the session"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this session. If set, will get/create session immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
|
||||
|
|
@ -206,15 +239,17 @@ class AsyncHoncho(BaseModel):
|
|||
Get or create a session with the given ID.
|
||||
|
||||
Creates an AsyncSession object that can be used to manage conversations between
|
||||
multiple peers. This method does not make an API call - the session is
|
||||
created lazily when its methods are first used.
|
||||
multiple peers. This method does not make an API call unless `config` or
|
||||
`metadata` is provided.
|
||||
|
||||
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
|
||||
config:
|
||||
Optional configuration to set for this session. If set, will get/create session immediately with flags.
|
||||
stable identifier that can be used consistently to reference the
|
||||
same conversation
|
||||
metadata: Optional metadata dictionary to associate with this session.
|
||||
If set, will get/create session immediately with metadata.
|
||||
config: Optional configuration to set for this session.
|
||||
If set, will get/create session immediately with flags.
|
||||
Returns:
|
||||
An AsyncSession object that can be used to add peers, send messages, and
|
||||
manage conversation context
|
||||
|
|
@ -222,14 +257,14 @@ class AsyncHoncho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the session ID is empty or invalid
|
||||
"""
|
||||
if config:
|
||||
if config or metadata:
|
||||
return await AsyncSession.create(
|
||||
id, self.workspace_id, self._client, config=config
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
return AsyncSession(id, self.workspace_id, self._client)
|
||||
|
||||
async def get_sessions(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncSession]:
|
||||
"""
|
||||
Get all sessions in the current workspace.
|
||||
|
|
@ -242,7 +277,7 @@ class AsyncHoncho(BaseModel):
|
|||
Returns an empty page if no sessions exist
|
||||
"""
|
||||
sessions_page = await self._client.workspaces.sessions.list(
|
||||
workspace_id=self.workspace_id, filter=filter
|
||||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return AsyncPage(
|
||||
sessions_page,
|
||||
|
|
@ -282,7 +317,7 @@ class AsyncHoncho(BaseModel):
|
|||
await self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
|
||||
async def get_workspaces(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> list[str]:
|
||||
"""
|
||||
Get all workspace IDs from the Honcho instance.
|
||||
|
|
@ -294,7 +329,7 @@ class AsyncHoncho(BaseModel):
|
|||
A list of workspace ID strings. Returns an empty list if no workspaces
|
||||
are accessible or none exist
|
||||
"""
|
||||
workspaces_page = await self._client.workspaces.list(filter=filter)
|
||||
workspaces_page = await self._client.workspaces.list(filters=filters)
|
||||
workspace_ids: list[str] = []
|
||||
async for workspace in workspaces_page:
|
||||
workspace_ids.append(workspace.id)
|
||||
|
|
@ -304,7 +339,13 @@ class AsyncHoncho(BaseModel):
|
|||
async def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> AsyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search for messages in the current workspace.
|
||||
|
||||
|
|
@ -312,15 +353,19 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
An AsyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = await self._client.workspaces.search(
|
||||
self.workspace_id, body=query
|
||||
return await self._client.workspaces.search(
|
||||
self.workspace_id,
|
||||
query=query,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
)
|
||||
return AsyncPage(messages_page)
|
||||
|
||||
@validate_call
|
||||
async def get_deriver_status(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
|
@ -65,28 +66,35 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id: str,
|
||||
client: AsyncHonchoCore,
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> AsyncPeer:
|
||||
"""
|
||||
Create a new AsyncPeer with optional configuration.
|
||||
|
||||
Provided metadata and configuration will overwrite any existing data in those
|
||||
locations if given.
|
||||
|
||||
Args:
|
||||
peer_id: Unique identifier for this peer within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent AsyncHoncho client instance
|
||||
metadata: Optional metadata dictionary to associate with this peer.
|
||||
If set, will get/create peer immediately with metadata.
|
||||
config: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
If set, will get/create peer immediately with flags.
|
||||
|
||||
Returns:
|
||||
A new AsyncPeer instance
|
||||
"""
|
||||
peer = cls(peer_id, workspace_id, client)
|
||||
|
||||
if config:
|
||||
if config or metadata:
|
||||
await client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
)
|
||||
|
||||
return peer
|
||||
|
|
@ -133,7 +141,7 @@ class AsyncPeer(BaseModel):
|
|||
return response.content
|
||||
|
||||
async def get_sessions(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> AsyncPage[AsyncSession]:
|
||||
"""
|
||||
Get all sessions this peer is a member of.
|
||||
|
|
@ -150,7 +158,7 @@ class AsyncPeer(BaseModel):
|
|||
sessions_page = await self._client.workspaces.peers.sessions.list(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
filter=filter,
|
||||
filters=filters,
|
||||
)
|
||||
return AsyncPage(
|
||||
sessions_page,
|
||||
|
|
@ -224,11 +232,58 @@ class AsyncPeer(BaseModel):
|
|||
metadata=metadata,
|
||||
)
|
||||
|
||||
async def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
Makes an API call to retrieve configuration associated with this peer.
|
||||
Configuration currently includes one optional flag, `observe_me`.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
peer = await self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
async def set_peer_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for this peer. Currently the only supported config
|
||||
value is the `observe_me` flag, which controls whether derivation tasks
|
||||
should be created for this peer's global representation. Default is True.
|
||||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.peers.update(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
async def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> AsyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search across all messages in the workspace with this peer as author.
|
||||
|
||||
|
|
@ -236,15 +291,20 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
An AsyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = await self._client.workspaces.peers.search(
|
||||
self.id, workspace_id=self.workspace_id, query=query
|
||||
return await self._client.workspaces.peers.search(
|
||||
self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
)
|
||||
return AsyncPage(messages_page)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
|
|
@ -17,13 +16,6 @@ if TYPE_CHECKING:
|
|||
from .peer import AsyncPeer
|
||||
|
||||
|
||||
try:
|
||||
env_val = os.getenv("HONCHO_DEFAULT_CONTEXT_TOKENS")
|
||||
_default_context_tokens = int(env_val) if env_val else None
|
||||
except (ValueError, TypeError):
|
||||
_default_context_tokens = None
|
||||
|
||||
|
||||
class SessionPeerConfig(BaseModel):
|
||||
observe_others: bool | None = Field(
|
||||
None,
|
||||
|
|
@ -90,28 +82,35 @@ class AsyncSession(BaseModel):
|
|||
workspace_id: str,
|
||||
client: AsyncHonchoCore,
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> AsyncSession:
|
||||
"""
|
||||
Create a new AsyncSession with optional configuration.
|
||||
|
||||
Provided metadata and configuration will overwrite any existing data in those
|
||||
locations if given.
|
||||
|
||||
Args:
|
||||
session_id: Unique identifier for this session within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent AsyncHoncho client instance
|
||||
config:
|
||||
Optional configuration to set for this session. If set, will get/create session immediately with flags.
|
||||
metadata: Optional metadata dictionary to associate with this session.
|
||||
If set, will get/create session immediately with metadata.
|
||||
config: Optional configuration to set for this session.
|
||||
If set, will get/create session immediately with flags.
|
||||
|
||||
Returns:
|
||||
A new AsyncSession instance
|
||||
"""
|
||||
session = cls(session_id, workspace_id, client)
|
||||
|
||||
if config:
|
||||
if config or metadata:
|
||||
await client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
)
|
||||
|
||||
return session
|
||||
|
|
@ -137,13 +136,13 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Args:
|
||||
peers: Peers to add to the session. Can be:
|
||||
- str: Single peer ID
|
||||
- AsyncPeer: Single AsyncPeer object
|
||||
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
|
||||
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
- str: Single peer ID
|
||||
- AsyncPeer: Single AsyncPeer object
|
||||
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
|
||||
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
"""
|
||||
if not isinstance(peers, list):
|
||||
peers = [peers]
|
||||
|
|
@ -186,13 +185,13 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Args:
|
||||
peers: Peers to set for the session. Can be:
|
||||
- str: Single peer ID
|
||||
- AsyncPeer: Single AsyncPeer object
|
||||
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
|
||||
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
- str: Single peer ID
|
||||
- AsyncPeer: Single AsyncPeer object
|
||||
- List[Union[AsyncPeer, str]]: List of AsyncPeer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[AsyncPeer, SessionPeerConfig]: Single AsyncPeer object and SessionPeerConfig
|
||||
- List[tuple[Union[AsyncPeer, str], SessionPeerConfig]]: List of AsyncPeer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
"""
|
||||
if not isinstance(peers, list):
|
||||
peers = [peers]
|
||||
|
|
@ -359,7 +358,7 @@ class AsyncSession(BaseModel):
|
|||
messages_page = await self._client.workspaces.sessions.messages.list(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
filter=filters,
|
||||
filters=filters,
|
||||
)
|
||||
return AsyncPage(messages_page)
|
||||
|
||||
|
|
@ -422,24 +421,22 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Args:
|
||||
summary: Whether to include summary information
|
||||
tokens: Maximum number of tokens to include in the context.
|
||||
Defaults to HONCHO_default_context_tokens environment
|
||||
variable if it exists.
|
||||
tokens: Maximum number of tokens to include in the context. Will default
|
||||
to Honcho server configuration if not provided.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history
|
||||
that maximizes conversational context while respecting the token limit
|
||||
A SessionContext object containing the optimized message history and
|
||||
summary, if available, that maximizes conversational context while
|
||||
respecting the token limit
|
||||
|
||||
Note:
|
||||
Token counting is performed using tiktoken. For models using different
|
||||
tokenizers, you may need to adjust the token limit accordingly.
|
||||
"""
|
||||
if not tokens:
|
||||
tokens = _default_context_tokens
|
||||
context = await self._client.workspaces.sessions.get_context(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
tokens=tokens,
|
||||
tokens=tokens if tokens is not None else NOT_GIVEN,
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
|
|
@ -451,7 +448,13 @@ class AsyncSession(BaseModel):
|
|||
async def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> AsyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search for messages in this session.
|
||||
|
||||
|
|
@ -459,15 +462,20 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
An AsyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = await self._client.workspaces.sessions.search(
|
||||
self.id, workspace_id=self.workspace_id, query=query
|
||||
return await self._client.workspaces.sessions.search(
|
||||
self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
)
|
||||
return AsyncPage(messages_page)
|
||||
|
||||
@validate_call
|
||||
async def upload_file(
|
||||
|
|
@ -529,7 +537,7 @@ class AsyncSession(BaseModel):
|
|||
Args:
|
||||
peer: Peer to get the working representation of.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
queries what `peer` knows about the `target`.
|
||||
queries what `peer` knows about the `target`.
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,14 @@ class Honcho(BaseModel):
|
|||
from environment variables or explicit parameters. This is the primary entry
|
||||
point for interacting with the Honcho conversational memory platform.
|
||||
|
||||
For advanced usage, the underlying honcho_core client can be accessed via the
|
||||
`core` property to use functionality not exposed through this SDK.
|
||||
|
||||
Attributes:
|
||||
api_key: API key for authentication
|
||||
base_url: Base URL for the Honcho API
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow") # pyright: ignore
|
||||
|
|
@ -40,6 +44,26 @@ class Honcho(BaseModel):
|
|||
)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def core(self) -> HonchoCore:
|
||||
"""
|
||||
Access the underlying honcho_core client. The honcho_core client is the raw Stainless-generated client,
|
||||
allowing users to access functionality that is not exposed through this SDK.
|
||||
|
||||
Returns:
|
||||
The underlying HonchoCore client instance
|
||||
|
||||
Example:
|
||||
```python
|
||||
from honcho import Honcho
|
||||
|
||||
client = Honcho()
|
||||
|
||||
workspace = client.core.workspaces.get_or_create(id="custom-workspace-id")
|
||||
```
|
||||
"""
|
||||
return self._client
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -126,6 +150,10 @@ class Honcho(BaseModel):
|
|||
..., min_length=1, description="Unique identifier for the peer"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this peer. If set, will get/create peer immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
|
||||
|
|
@ -135,14 +163,16 @@ class Honcho(BaseModel):
|
|||
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 - the peer is created lazily when
|
||||
its methods are first used.
|
||||
This method does not make an API call unless `config` or `metadata` is
|
||||
provided.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for the peer within the workspace. Should be a
|
||||
stable identifier that can be used consistently across sessions
|
||||
config:
|
||||
Optional configuration to set for this peer. If set, will get/create peer immediately with flags.
|
||||
stable identifier that can be used consistently across sessions.
|
||||
metadata: Optional metadata dictionary to associate with this peer.
|
||||
If set, will get/create peer immediately with metadata.
|
||||
config: 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
|
||||
|
|
@ -151,9 +181,11 @@ class Honcho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the peer ID is empty or invalid
|
||||
"""
|
||||
return Peer(id, self.workspace_id, self._client, config=config)
|
||||
return Peer(
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
|
||||
def get_peers(self, filter: dict[str, object] | None = None) -> SyncPage[Peer]:
|
||||
def get_peers(self, filters: dict[str, object] | None = None) -> SyncPage[Peer]:
|
||||
"""
|
||||
Get all peers in the current workspace.
|
||||
|
||||
|
|
@ -162,11 +194,10 @@ class Honcho(BaseModel):
|
|||
inner client Peer objects to SDK Peer objects as they are consumed.
|
||||
|
||||
Returns:
|
||||
A SyncPage of Peer objects representing all peers in the workspace.
|
||||
The page preserves pagination functionality while transforming objects
|
||||
A SyncPage of Peer objects representing all peers in the workspace
|
||||
"""
|
||||
peers_page = self._client.workspaces.peers.list(
|
||||
workspace_id=self.workspace_id, filter=filter
|
||||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return SyncPage(
|
||||
peers_page, lambda peer: Peer(peer.id, self.workspace_id, self._client)
|
||||
|
|
@ -179,6 +210,10 @@ class Honcho(BaseModel):
|
|||
..., min_length=1, description="Unique identifier for the session"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this session. If set, will get/create session immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
|
||||
|
|
@ -188,15 +223,17 @@ class Honcho(BaseModel):
|
|||
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 - the session is
|
||||
created lazily when its methods are first used.
|
||||
multiple peers. This method does not make an API call unless `config` or
|
||||
`metadata` is provided.
|
||||
|
||||
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
|
||||
config:
|
||||
Optional configuration to set for this session. If set, will get/create session immediately with flags.
|
||||
stable identifier that can be used consistently to reference the
|
||||
same conversation
|
||||
metadata: Optional metadata dictionary to associate with this session.
|
||||
If set, will get/create session immediately with metadata.
|
||||
config: 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
|
||||
|
|
@ -204,10 +241,12 @@ class Honcho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the session ID is empty or invalid
|
||||
"""
|
||||
return Session(id, self.workspace_id, self._client, config=config)
|
||||
return Session(
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
|
||||
def get_sessions(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> SyncPage[Session]:
|
||||
"""
|
||||
Get all sessions in the current workspace.
|
||||
|
|
@ -220,7 +259,7 @@ class Honcho(BaseModel):
|
|||
Returns an empty page if no sessions exist
|
||||
"""
|
||||
sessions_page = self._client.workspaces.sessions.list(
|
||||
workspace_id=self.workspace_id, filter=filter
|
||||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return SyncPage(
|
||||
sessions_page,
|
||||
|
|
@ -259,7 +298,7 @@ class Honcho(BaseModel):
|
|||
"""
|
||||
self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
|
||||
def get_workspaces(self, filter: dict[str, object] | None = None) -> list[str]:
|
||||
def get_workspaces(self, filters: dict[str, object] | None = None) -> list[str]:
|
||||
"""
|
||||
Get all workspace IDs from the Honcho instance.
|
||||
|
||||
|
|
@ -270,14 +309,20 @@ class Honcho(BaseModel):
|
|||
A list of workspace ID strings. Returns an empty list if no workspaces
|
||||
are accessible or none exist
|
||||
"""
|
||||
workspaces = self._client.workspaces.list(filter=filter)
|
||||
workspaces = self._client.workspaces.list(filters=filters)
|
||||
return [workspace.id for workspace in workspaces]
|
||||
|
||||
@validate_call
|
||||
def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> SyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search for messages in the current workspace.
|
||||
|
||||
|
|
@ -285,13 +330,16 @@ class Honcho(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
A SyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = self._client.workspaces.search(self.workspace_id, body=query)
|
||||
return SyncPage(messages_page)
|
||||
return self._client.workspaces.search(
|
||||
self.workspace_id, query=query, filters=filters, limit=limit
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def get_deriver_status(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import NOT_GIVEN
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
|
@ -47,6 +48,10 @@ class Peer(BaseModel):
|
|||
..., description="Reference to the parent Honcho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this peer. If set, will get/create peer immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this peer. If set, will get/create peer immediately with flags.",
|
||||
|
|
@ -55,21 +60,27 @@ class Peer(BaseModel):
|
|||
"""
|
||||
Initialize a new Peer.
|
||||
|
||||
Provided metadata and configuration will overwrite any existing data in those
|
||||
locations if given.
|
||||
|
||||
Args:
|
||||
peer_id: Unique identifier for this peer within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent Honcho client instance
|
||||
metadata: Optional metadata dictionary to associate with this peer.
|
||||
If set, will get/create peer immediately with metadata.
|
||||
config: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
If set, will get/create peer immediately with flags.
|
||||
"""
|
||||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
self._client = client
|
||||
|
||||
if config:
|
||||
if config or metadata:
|
||||
self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
)
|
||||
|
||||
def chat(
|
||||
|
|
@ -113,7 +124,7 @@ class Peer(BaseModel):
|
|||
return response.content
|
||||
|
||||
def get_sessions(
|
||||
self, filter: dict[str, object] | None = None
|
||||
self, filters: dict[str, object] | None = None
|
||||
) -> SyncPage[Session]:
|
||||
"""
|
||||
Get all sessions this peer is a member of.
|
||||
|
|
@ -130,7 +141,7 @@ class Peer(BaseModel):
|
|||
sessions_page = self._client.workspaces.peers.sessions.list(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
filter=filter,
|
||||
filters=filters,
|
||||
)
|
||||
return SyncPage(
|
||||
sessions_page,
|
||||
|
|
@ -196,7 +207,7 @@ class Peer(BaseModel):
|
|||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this peer.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.peers.update(
|
||||
peer_id=self.id,
|
||||
|
|
@ -204,11 +215,58 @@ class Peer(BaseModel):
|
|||
metadata=metadata,
|
||||
)
|
||||
|
||||
def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
Makes an API call to retrieve configuration associated with this peer.
|
||||
Configuration currently includes one optional flag, `observe_me`.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
peer = self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
def set_peer_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for this peer. Currently the only supported config
|
||||
value is the `observe_me` flag, which controls whether derivation tasks
|
||||
should be created for this peer's global representation. Default is True.
|
||||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.peers.update(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> SyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search across all messages in the workspace with this peer as author.
|
||||
|
||||
|
|
@ -216,15 +274,20 @@ class Peer(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
A SyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = self._client.workspaces.peers.search(
|
||||
self.id, workspace_id=self.workspace_id, query=query
|
||||
return self._client.workspaces.peers.search(
|
||||
self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
)
|
||||
return SyncPage(messages_page)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
|
|
@ -17,13 +16,6 @@ if TYPE_CHECKING:
|
|||
from .peer import Peer
|
||||
|
||||
|
||||
try:
|
||||
env_val = os.getenv("HONCHO_DEFAULT_CONTEXT_TOKENS")
|
||||
_default_context_tokens = int(env_val) if env_val else None
|
||||
except (ValueError, TypeError):
|
||||
_default_context_tokens = None
|
||||
|
||||
|
||||
class SessionPeerConfig(BaseModel):
|
||||
observe_others: bool | None = Field(
|
||||
None,
|
||||
|
|
@ -69,6 +61,10 @@ class Session(BaseModel):
|
|||
..., description="Reference to the parent Honcho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with this session. If set, will get/create session immediately with metadata.",
|
||||
),
|
||||
config: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional configuration to set for this session. If set, will get/create session immediately with flags.",
|
||||
|
|
@ -77,12 +73,17 @@ class Session(BaseModel):
|
|||
"""
|
||||
Initialize a new Session.
|
||||
|
||||
Provided metadata and configuration will overwrite any existing data in those
|
||||
locations if given.
|
||||
|
||||
Args:
|
||||
session_id: Unique identifier for this session within the workspace
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
client: Reference to the parent Honcho client instance
|
||||
config:
|
||||
Optional configuration to set for this session. If set, will get/create session immediately with flags.
|
||||
metadata: Optional metadata dictionary to associate with this session.
|
||||
If set, will get/create session immediately with metadata.
|
||||
config: Optional configuration to set for this session.
|
||||
If set, will get/create session immediately with flags.
|
||||
"""
|
||||
super().__init__(
|
||||
id=session_id,
|
||||
|
|
@ -90,11 +91,12 @@ class Session(BaseModel):
|
|||
)
|
||||
self._client = client
|
||||
|
||||
if config:
|
||||
if config or metadata:
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config,
|
||||
configuration=config if config is not None else NOT_GIVEN,
|
||||
metadata=metadata if metadata is not None else NOT_GIVEN,
|
||||
)
|
||||
|
||||
def add_peers(
|
||||
|
|
@ -118,13 +120,13 @@ class Session(BaseModel):
|
|||
|
||||
Args:
|
||||
peers: Peers to add to the session. Can be:
|
||||
- str: Single peer ID
|
||||
- Peer: Single Peer object
|
||||
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
|
||||
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
- str: Single peer ID
|
||||
- Peer: Single Peer object
|
||||
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
|
||||
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
"""
|
||||
if not isinstance(peers, list):
|
||||
peers = [peers]
|
||||
|
|
@ -167,13 +169,13 @@ class Session(BaseModel):
|
|||
|
||||
Args:
|
||||
peers: Peers to set for the session. Can be:
|
||||
- str: Single peer ID
|
||||
- Peer: Single Peer object
|
||||
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
|
||||
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
- str: Single peer ID
|
||||
- Peer: Single Peer object
|
||||
- List[Union[Peer, str]]: List of Peer objects and/or peer IDs
|
||||
- tuple[str, SessionPeerConfig]: Single peer ID and SessionPeerConfig
|
||||
- tuple[Peer, SessionPeerConfig]: Single Peer object and SessionPeerConfig
|
||||
- List[tuple[Union[Peer, str], SessionPeerConfig]]: List of Peer objects and/or peer IDs and SessionPeerConfig
|
||||
- Mixed lists with peers and tuples/lists containing peer+config combinations
|
||||
"""
|
||||
if not isinstance(peers, list):
|
||||
peers = [peers]
|
||||
|
|
@ -335,7 +337,7 @@ class Session(BaseModel):
|
|||
messages_page = self._client.workspaces.sessions.messages.list(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
filter=filters,
|
||||
filters=filters,
|
||||
)
|
||||
return SyncPage(messages_page)
|
||||
|
||||
|
|
@ -400,23 +402,22 @@ class Session(BaseModel):
|
|||
|
||||
Args:
|
||||
summary: Whether to include summary information
|
||||
tokens: Maximum number of tokens to include in the context.
|
||||
Defaults to HONCHO_default_context_tokens env var
|
||||
tokens: Maximum number of tokens to include in the context. Will default
|
||||
to Honcho server configuration if not provided.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history
|
||||
that maximizes conversational context while respecting the token limit
|
||||
A SessionContext object containing the optimized message history and
|
||||
summary, if available, that maximizes conversational context while
|
||||
respecting the token limit
|
||||
|
||||
Note:
|
||||
Token counting is performed using tiktoken. For models using different
|
||||
tokenizers, you may need to adjust the token limit accordingly.
|
||||
"""
|
||||
if not tokens:
|
||||
tokens = _default_context_tokens
|
||||
context = self._client.workspaces.sessions.get_context(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
tokens=tokens,
|
||||
tokens=tokens if tokens is not None else NOT_GIVEN,
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
|
|
@ -428,7 +429,13 @@ class Session(BaseModel):
|
|||
def search(
|
||||
self,
|
||||
query: str = Field(..., min_length=1, description="The search query to use"),
|
||||
) -> SyncPage[Message]:
|
||||
filters: dict[str, object] | None = Field(
|
||||
None, description="Filters to scope the search"
|
||||
),
|
||||
limit: int = Field(
|
||||
default=10, ge=1, le=100, description="Number of results to return"
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Search for messages in this session.
|
||||
|
||||
|
|
@ -436,15 +443,20 @@ class Session(BaseModel):
|
|||
|
||||
Args:
|
||||
query: The search query to use
|
||||
filters: Filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
limit: Number of results to return (1-100, default: 10)
|
||||
|
||||
Returns:
|
||||
A SyncPage of Message objects representing the search results.
|
||||
Returns an empty page if no messages are found.
|
||||
A list of Message objects representing the search results.
|
||||
Returns an empty list if no messages are found.
|
||||
"""
|
||||
messages_page = self._client.workspaces.sessions.search(
|
||||
self.id, workspace_id=self.workspace_id, query=query
|
||||
return self._client.workspaces.sessions.search(
|
||||
self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
)
|
||||
return SyncPage(messages_page)
|
||||
|
||||
@validate_call
|
||||
def upload_file(
|
||||
|
|
@ -506,7 +518,7 @@ class Session(BaseModel):
|
|||
Args:
|
||||
peer: Peer to get the working representation of.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
queries what `peer` knows about the `target`.
|
||||
queries what `peer` knows about the `target`.
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
|
|
|
|||
|
|
@ -61,18 +61,18 @@ class SessionContext(BaseModel):
|
|||
self,
|
||||
*,
|
||||
assistant: str | Peer,
|
||||
) -> list[dict[str, object]]:
|
||||
) -> list[dict[str, str]]:
|
||||
"""
|
||||
Convert the context to OpenAI-compatible message format.
|
||||
|
||||
Transforms the message history into the format expected by OpenAI's
|
||||
Chat Completions API, with proper role assignments based on the
|
||||
Transforms the message history and summary into the format expected by
|
||||
OpenAI's Chat Completions API, with proper role assignments based on the
|
||||
assistant's identity.
|
||||
|
||||
Args:
|
||||
assistant: The assistant peer (Peer object or peer ID string) to use
|
||||
for determining message roles. Messages from this peer will
|
||||
be marked as "assistant", others as "user"
|
||||
for determining message roles. Messages from this peer will be marked
|
||||
as "assistant", others as "user"
|
||||
|
||||
Returns:
|
||||
A list of dictionaries in OpenAI format, where each dictionary contains
|
||||
|
|
@ -80,26 +80,30 @@ class SessionContext(BaseModel):
|
|||
"""
|
||||
|
||||
assistant_id = assistant if isinstance(assistant, str) else assistant.id
|
||||
return [
|
||||
summary_message = {
|
||||
"role": "system",
|
||||
"content": f"<summary>{self.summary}</summary>",
|
||||
}
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant" if message.peer_id == assistant_id else "user",
|
||||
"name": message.peer_id,
|
||||
"content": message.content,
|
||||
}
|
||||
for message in self.messages
|
||||
]
|
||||
return [summary_message, *messages] if self.summary else messages
|
||||
|
||||
def to_anthropic(
|
||||
self,
|
||||
*,
|
||||
assistant: str | Peer,
|
||||
) -> list[dict[str, object]]:
|
||||
) -> list[dict[str, str]]:
|
||||
"""
|
||||
Convert the context to Anthropic-compatible message format.
|
||||
|
||||
Transforms the message history into the format expected by Anthropic's
|
||||
Claude API. TODO: Anthropic requires messages to alternate between
|
||||
user and assistant roles, so this method may need to handle role
|
||||
consolidation or filtering in the future.
|
||||
Claude API, with proper role assignments based on the assistant's identity.
|
||||
|
||||
Args:
|
||||
assistant: The assistant peer (Peer object or peer ID string) to use
|
||||
|
|
@ -116,13 +120,23 @@ class SessionContext(BaseModel):
|
|||
"""
|
||||
|
||||
assistant_id = assistant if isinstance(assistant, str) else assistant.id
|
||||
return [
|
||||
summary_message = {
|
||||
"role": "user",
|
||||
"content": f"<summary>{self.summary}</summary>",
|
||||
}
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant" if message.peer_id == assistant_id else "user",
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
}
|
||||
if message.peer_id == assistant_id
|
||||
else {
|
||||
"role": "user",
|
||||
"content": f"{message.peer_id}: {message.content}",
|
||||
}
|
||||
for message in self.messages
|
||||
]
|
||||
return [summary_message, *messages] if self.summary else messages
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
|
|
@ -131,7 +145,7 @@ class SessionContext(BaseModel):
|
|||
Returns:
|
||||
The number of messages in this context
|
||||
"""
|
||||
return len(self.messages)
|
||||
return len(self.messages) + (1 if self.summary else 0)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
|
|
@ -140,4 +154,4 @@ class SessionContext(BaseModel):
|
|||
Returns:
|
||||
A string representation suitable for debugging
|
||||
"""
|
||||
return f"SessionContext(messages={len(self.messages)})"
|
||||
return f"SessionContext(messages={len(self.messages)}, summary={self.summary})"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
|
|
@ -112,7 +112,7 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.2.0" },
|
||||
{ name = "honcho-core", specifier = "==1.3.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
]
|
||||
|
|
@ -122,7 +122,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
|||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
|
|
@ -135,9 +135,9 @@ dependencies = [
|
|||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/60/e870902c5d247b5a0fb38401d6f3730e11eaf038f5a15c68afc4465e332f/honcho_core-1.2.0.tar.gz", hash = "sha256:1f16fd9ecd236bfc4c30ecc33354baf4bdd9a4206e84f92ec785ecd61b25d193", size = 122450, upload-time = "2025-07-16T19:59:06.362Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/49/1810de2351be1fdff330ffadcc5eef709889b2f698b0340966cae2c1eef9/honcho_core-1.3.0.tar.gz", hash = "sha256:276a73e8d523f7d22f06746922fda97e4a11fbbef4a51a0b43573bc817089cec", size = 123333, upload-time = "2025-08-06T16:42:10.941Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/99/f435c093ea2067d7da50545cfbe2037e28370541ba161f5c8df6e33030b0/honcho_core-1.2.0-py3-none-any.whl", hash = "sha256:d9260e1a2a1254c26aeec464f8bca7ebb6c2fa9b5ae568a9344b5458467d78ab", size = 110721, upload-time = "2025-07-16T19:59:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/3f/3beb750ed65bb1b604c585ae1c51a54f8b34ea3002f4525f93fe86549531/honcho_core-1.3.0-py3-none-any.whl", hash = "sha256:c7c0bf77b61162e6c65a580327fc054785aef794923abc35acd3dbd8287245db", size = 112895, upload-time = "2025-08-06T16:42:08.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@ 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/).
|
||||
|
||||
## [1.3.0] - 2025-08-04
|
||||
|
||||
### Added
|
||||
|
||||
- Zod validation
|
||||
- Added getPeerConfig to Session object
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved parameters out of random `opts` dictionaries in many places
|
||||
- Peer and Session objects now use inner client like python SDK
|
||||
|
||||
### Fixed
|
||||
|
||||
- Enabled missing `metadata` options in many places
|
||||
- Proper default behavior for SessionPeerConfig
|
||||
|
||||
## [1.2.1] - 2025-07-21
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -23,10 +23,13 @@ export default class MockHonchoCore {
|
|||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
list: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
setConfig: jest.fn(),
|
||||
},
|
||||
messages: {
|
||||
create: jest.fn(),
|
||||
list: jest.fn(),
|
||||
upload: jest.fn(),
|
||||
},
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
|
@ -37,6 +40,7 @@ export default class MockHonchoCore {
|
|||
update: jest.fn(),
|
||||
list: jest.fn(),
|
||||
search: jest.fn(),
|
||||
deriverStatus: jest.fn(),
|
||||
};
|
||||
|
||||
constructor(options?: any) {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@ jest.mock('@honcho-ai/core', () => {
|
|||
workspaces: {
|
||||
peers: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
},
|
||||
sessions: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
},
|
||||
getOrCreate: jest.fn().mockResolvedValue({ id: 'test-workspace', metadata: {} }),
|
||||
update: jest.fn(),
|
||||
list: jest.fn(),
|
||||
search: jest.fn(),
|
||||
deriverStatus: jest.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
@ -89,21 +92,33 @@ describe('Honcho Client', () => {
|
|||
});
|
||||
|
||||
describe('peer', () => {
|
||||
it('should create a new Peer instance', () => {
|
||||
const peer = honcho.peer('test-peer');
|
||||
it('should create a new Peer instance', async () => {
|
||||
const peer = await honcho.peer('test-peer');
|
||||
|
||||
expect(peer).toBeInstanceOf(Peer);
|
||||
expect(peer.id).toBe('test-peer');
|
||||
});
|
||||
|
||||
it('should throw error for empty peer ID', () => {
|
||||
expect(() => honcho.peer('')).toThrow('Peer ID must be a non-empty string');
|
||||
it('should create peer with metadata and config', async () => {
|
||||
const metadata = { name: 'Test Peer' };
|
||||
const config = { observe_me: false };
|
||||
|
||||
await honcho.peer('test-peer', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'test-peer', metadata: metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for non-string peer ID', () => {
|
||||
expect(() => honcho.peer(null as any)).toThrow('Peer ID must be a non-empty string');
|
||||
expect(() => honcho.peer(undefined as any)).toThrow('Peer ID must be a non-empty string');
|
||||
expect(() => honcho.peer(123 as any)).toThrow('Peer ID must be a non-empty string');
|
||||
it('should throw error for empty peer ID', async () => {
|
||||
await expect(honcho.peer('')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for non-string peer ID', async () => {
|
||||
await expect(honcho.peer(null as any)).rejects.toThrow();
|
||||
await expect(honcho.peer(undefined as any)).rejects.toThrow();
|
||||
await expect(honcho.peer(123 as any)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -123,7 +138,7 @@ describe('Honcho Client', () => {
|
|||
const peersPage = await honcho.getPeers();
|
||||
|
||||
expect(peersPage).toBeInstanceOf(Page);
|
||||
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace', { filter: undefined });
|
||||
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace', { filters: undefined });
|
||||
});
|
||||
|
||||
it('should handle empty peers list', async () => {
|
||||
|
|
@ -138,32 +153,44 @@ describe('Honcho Client', () => {
|
|||
const peersPage = await honcho.getPeers();
|
||||
|
||||
expect(peersPage).toBeInstanceOf(Page);
|
||||
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace', { filter: undefined });
|
||||
expect(mockClient.workspaces.peers.list).toHaveBeenCalledWith('test-workspace', { filters: undefined });
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.list.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(honcho.getPeers()).rejects.toThrow('API Error');
|
||||
await expect(honcho.getPeers()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('session', () => {
|
||||
it('should create a new Session instance', () => {
|
||||
const session = honcho.session('test-session');
|
||||
it('should create a new Session instance', async () => {
|
||||
const session = await honcho.session('test-session');
|
||||
|
||||
expect(session).toBeInstanceOf(Session);
|
||||
expect(session.id).toBe('test-session');
|
||||
});
|
||||
|
||||
it('should throw error for empty session ID', () => {
|
||||
expect(() => honcho.session('')).toThrow('Session ID must be a non-empty string');
|
||||
it('should create session with metadata and config', async () => {
|
||||
const metadata = { name: 'Test Session' };
|
||||
const config = { anonymous: true };
|
||||
|
||||
await honcho.session('test-session', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'test-session', metadata: metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for non-string session ID', () => {
|
||||
expect(() => honcho.session(null as any)).toThrow('Session ID must be a non-empty string');
|
||||
expect(() => honcho.session(undefined as any)).toThrow('Session ID must be a non-empty string');
|
||||
expect(() => honcho.session(123 as any)).toThrow('Session ID must be a non-empty string');
|
||||
it('should throw error for empty session ID', async () => {
|
||||
await expect(honcho.session('')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for non-string session ID', async () => {
|
||||
await expect(honcho.session(null as any)).rejects.toThrow();
|
||||
await expect(honcho.session(undefined as any)).rejects.toThrow();
|
||||
await expect(honcho.session(123 as any)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -183,7 +210,7 @@ describe('Honcho Client', () => {
|
|||
const sessionsPage = await honcho.getSessions();
|
||||
|
||||
expect(sessionsPage).toBeInstanceOf(Page);
|
||||
expect(mockClient.workspaces.sessions.list).toHaveBeenCalledWith('test-workspace', { filter: undefined });
|
||||
expect(mockClient.workspaces.sessions.list).toHaveBeenCalledWith('test-workspace', { filters: undefined });
|
||||
});
|
||||
|
||||
it('should handle empty sessions list', async () => {
|
||||
|
|
@ -203,7 +230,7 @@ describe('Honcho Client', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.list.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(honcho.getSessions()).rejects.toThrow('API Error');
|
||||
await expect(honcho.getSessions()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -236,7 +263,7 @@ describe('Honcho Client', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.getOrCreate.mockRejectedValue(new Error('Workspace not found'));
|
||||
|
||||
await expect(honcho.getMetadata()).rejects.toThrow('Workspace not found');
|
||||
await expect(honcho.getMetadata()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -276,7 +303,7 @@ describe('Honcho Client', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
await expect(honcho.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
|
||||
await expect(honcho.setMetadata({ key: 'value' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -313,73 +340,145 @@ describe('Honcho Client', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.list.mockRejectedValue(new Error('Failed to list workspaces'));
|
||||
|
||||
await expect(honcho.getWorkspaces()).rejects.toThrow('Failed to list workspaces');
|
||||
await expect(honcho.getWorkspaces()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should search for messages and return Page', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults = [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
|
||||
];
|
||||
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await honcho.search('hello');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', { body: 'hello' });
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', { query: 'hello', limit: undefined });
|
||||
});
|
||||
|
||||
it('should handle empty search results', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [],
|
||||
total: 0,
|
||||
size: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults: any[] = [];
|
||||
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await honcho.search('nonexistent');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error for empty query', async () => {
|
||||
await expect(honcho.search('')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(honcho.search(' ')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(honcho.search('')).rejects.toThrow();
|
||||
await expect(honcho.search(' ')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for non-string query', async () => {
|
||||
await expect(honcho.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(honcho.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(honcho.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(honcho.search(null as any)).rejects.toThrow();
|
||||
await expect(honcho.search(undefined as any)).rejects.toThrow();
|
||||
await expect(honcho.search(123 as any)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle complex search queries', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [],
|
||||
total: 0,
|
||||
size: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults: any[] = [];
|
||||
mockClient.workspaces.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const complexQuery = 'complex query with "quotes" and special characters!@#$%';
|
||||
await honcho.search(complexQuery);
|
||||
|
||||
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', { body: complexQuery });
|
||||
expect(mockClient.workspaces.search).toHaveBeenCalledWith('test-workspace', { query: complexQuery, limit: undefined });
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.search.mockRejectedValue(new Error('Search failed'));
|
||||
|
||||
await expect(honcho.search('test')).rejects.toThrow('Search failed');
|
||||
await expect(honcho.search('test')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeriverStatus', () => {
|
||||
it('should return deriver status without options', async () => {
|
||||
const mockStatus = {
|
||||
total_work_units: 10,
|
||||
completed_work_units: 5,
|
||||
in_progress_work_units: 3,
|
||||
pending_work_units: 2,
|
||||
sessions: { 'session1': { status: 'active' } },
|
||||
};
|
||||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus);
|
||||
|
||||
const status = await honcho.getDeriverStatus();
|
||||
|
||||
expect(status).toEqual({
|
||||
totalWorkUnits: 10,
|
||||
completedWorkUnits: 5,
|
||||
inProgressWorkUnits: 3,
|
||||
pendingWorkUnits: 2,
|
||||
sessions: { 'session1': { status: 'active' } },
|
||||
});
|
||||
expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith('test-workspace', {});
|
||||
});
|
||||
|
||||
it('should return deriver status with options', async () => {
|
||||
const mockStatus = {
|
||||
total_work_units: 5,
|
||||
completed_work_units: 3,
|
||||
in_progress_work_units: 1,
|
||||
pending_work_units: 1,
|
||||
};
|
||||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus);
|
||||
|
||||
const status = await honcho.getDeriverStatus({
|
||||
observerId: 'observer1',
|
||||
senderId: 'sender1',
|
||||
sessionId: 'session1',
|
||||
});
|
||||
|
||||
expect(status).toEqual({
|
||||
totalWorkUnits: 5,
|
||||
completedWorkUnits: 3,
|
||||
inProgressWorkUnits: 1,
|
||||
pendingWorkUnits: 1,
|
||||
sessions: undefined,
|
||||
});
|
||||
expect(mockClient.workspaces.deriverStatus).toHaveBeenCalledWith('test-workspace', {
|
||||
observer_id: 'observer1',
|
||||
sender_id: 'sender1',
|
||||
session_id: 'session1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollDeriverStatus', () => {
|
||||
it('should poll until processing is complete', async () => {
|
||||
const mockStatusComplete = {
|
||||
total_work_units: 5,
|
||||
completed_work_units: 5,
|
||||
in_progress_work_units: 0,
|
||||
pending_work_units: 0,
|
||||
};
|
||||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusComplete);
|
||||
|
||||
const status = await honcho.pollDeriverStatus();
|
||||
|
||||
expect(status).toEqual({
|
||||
totalWorkUnits: 5,
|
||||
completedWorkUnits: 5,
|
||||
inProgressWorkUnits: 0,
|
||||
pendingWorkUnits: 0,
|
||||
sessions: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should timeout if processing takes too long', async () => {
|
||||
const mockStatusPending = {
|
||||
total_work_units: 5,
|
||||
completed_work_units: 2,
|
||||
in_progress_work_units: 2,
|
||||
pending_work_units: 1,
|
||||
};
|
||||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatusPending);
|
||||
|
||||
await expect(honcho.pollDeriverStatus({ timeoutMs: 100 })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
const mockPeerData = { id: 'assistant', metadata: { role: 'ai' } };
|
||||
const mockSessionData = { id: 'chat-session', metadata: { topic: 'general' } };
|
||||
const mockMessages = [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'user' },
|
||||
{ id: 'msg2', content: 'Hi there!', peer_name: 'assistant' },
|
||||
{ id: 'msg1', content: 'Hello', peer_id: 'user' },
|
||||
{ id: 'msg2', content: 'Hi there!', peer_id: 'assistant' },
|
||||
];
|
||||
const mockContextData = { messages: mockMessages, summary: 'Friendly greeting' };
|
||||
|
||||
|
|
@ -71,8 +71,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: 'AI response' });
|
||||
|
||||
// Step 1: Create peers
|
||||
const user = honcho.peer('user');
|
||||
const assistant = honcho.peer('assistant');
|
||||
const user = await honcho.peer('user');
|
||||
const assistant = await honcho.peer('assistant');
|
||||
|
||||
expect(user).toBeInstanceOf(Peer);
|
||||
expect(assistant).toBeInstanceOf(Peer);
|
||||
|
|
@ -80,7 +80,7 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
expect(assistant.id).toBe('assistant');
|
||||
|
||||
// Step 2: Create session
|
||||
const session = honcho.session('chat-session');
|
||||
const session = await honcho.session('chat-session');
|
||||
expect(session).toBeInstanceOf(Session);
|
||||
expect(session.id).toBe('chat-session');
|
||||
|
||||
|
|
@ -90,8 +90,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
'integration-test-workspace',
|
||||
'chat-session',
|
||||
{
|
||||
'user': { observe_me: true, observe_others: false },
|
||||
'assistant': { observe_me: true, observe_others: false }
|
||||
'user': {},
|
||||
'assistant': {}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -123,12 +123,14 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
const anthropicFormat = context.toAnthropic('assistant');
|
||||
|
||||
expect(openAIFormat).toEqual([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there!' },
|
||||
{ role: 'system', content: '<summary>Friendly greeting</summary>' },
|
||||
{ role: 'user', content: 'Hello', name: 'user' },
|
||||
{ role: 'assistant', content: 'Hi there!', name: 'assistant' },
|
||||
]);
|
||||
|
||||
expect(anthropicFormat).toEqual([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'user', content: '<summary>Friendly greeting</summary>' },
|
||||
{ role: 'user', content: 'user: Hello' },
|
||||
{ role: 'assistant', content: 'Hi there!' },
|
||||
]);
|
||||
|
||||
|
|
@ -193,32 +195,17 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
it('should handle search functionality across different scopes', async () => {
|
||||
// Setup mock responses
|
||||
const mockWorkspaceSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockWorkspaceSearchResults = [
|
||||
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
|
||||
];
|
||||
|
||||
const mockPeerSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockPeerSearchResults = [
|
||||
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
|
||||
];
|
||||
|
||||
const mockSessionSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSessionSearchResults = [
|
||||
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
|
||||
];
|
||||
|
||||
mockWorkspacesApi.workspaces.search.mockResolvedValue(mockWorkspaceSearchResults);
|
||||
mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(mockPeerSearchResults);
|
||||
|
|
@ -226,30 +213,30 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
// Step 1: Search workspace
|
||||
const workspaceResults = await honcho.search('test query');
|
||||
expect(workspaceResults).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(workspaceResults)).toBe(true);
|
||||
expect(mockWorkspacesApi.workspaces.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
{ body: 'test query' }
|
||||
{ query: 'test query', limit: undefined }
|
||||
);
|
||||
|
||||
// Step 2: Search peer
|
||||
const peer = honcho.peer('test-peer');
|
||||
const peer = await honcho.peer('test-peer');
|
||||
const peerResults = await peer.search('peer query');
|
||||
expect(peerResults).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(peerResults)).toBe(true);
|
||||
expect(mockWorkspacesApi.workspaces.peers.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'test-peer',
|
||||
{ query: 'peer query' }
|
||||
{ query: 'peer query', limit: undefined }
|
||||
);
|
||||
|
||||
// Step 3: Search session
|
||||
const session = honcho.session('test-session');
|
||||
const session = await honcho.session('test-session');
|
||||
const sessionResults = await session.search('session query');
|
||||
expect(sessionResults).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(sessionResults)).toBe(true);
|
||||
expect(mockWorkspacesApi.workspaces.sessions.search).toHaveBeenCalledWith(
|
||||
'integration-test-workspace',
|
||||
'test-session',
|
||||
{ query: 'session query' }
|
||||
{ query: 'session query', limit: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -258,14 +245,14 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(new Error('Chat API failed'));
|
||||
mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(new Error('Context API failed'));
|
||||
|
||||
const assistant = honcho.peer('assistant');
|
||||
const session = honcho.session('error-session');
|
||||
const assistant = await honcho.peer('assistant');
|
||||
const session = await honcho.session('error-session');
|
||||
|
||||
// Test error handling in chat
|
||||
await expect(assistant.chat('Hello')).rejects.toThrow('Chat API failed');
|
||||
await expect(assistant.chat('Hello')).rejects.toThrow();
|
||||
|
||||
// Test error handling in context
|
||||
await expect(session.getContext()).rejects.toThrow('Context API failed');
|
||||
await expect(session.getContext()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle pagination correctly', async () => {
|
||||
|
|
@ -329,9 +316,9 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(mockWorkingRep);
|
||||
|
||||
const session = honcho.session('working-rep-session');
|
||||
const alice = honcho.peer('alice');
|
||||
const bob = honcho.peer('bob');
|
||||
const session = await honcho.session('working-rep-session');
|
||||
const alice = await honcho.peer('alice');
|
||||
const bob = await honcho.peer('bob');
|
||||
|
||||
// Test working representation without target
|
||||
const globalRep = await session.workingRep('alice');
|
||||
|
|
@ -359,8 +346,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({ items: [], total: 0, hasNextPage: false });
|
||||
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ messages: [] });
|
||||
|
||||
const peer = honcho.peer('empty-peer');
|
||||
const session = honcho.session('empty-session');
|
||||
const peer = await honcho.peer('empty-peer');
|
||||
const session = await honcho.session('empty-session');
|
||||
|
||||
// Test null chat response
|
||||
const chatResult = await peer.chat('Hello');
|
||||
|
|
@ -379,20 +366,20 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
it('should maintain type safety throughout the workflow', async () => {
|
||||
// This test verifies TypeScript types are maintained correctly
|
||||
const peer: Peer = honcho.peer('typed-peer');
|
||||
const session: Session = honcho.session('typed-session');
|
||||
const peer: Peer = await honcho.peer('typed-peer');
|
||||
const session: Session = await honcho.session('typed-session');
|
||||
|
||||
expect(typeof peer.id).toBe('string');
|
||||
expect(typeof session.id).toBe('string');
|
||||
|
||||
const message = peer.message('typed message', { metadata: { type: 'test' } });
|
||||
expect(typeof message.peerId).toBe('string');
|
||||
expect(typeof message.peer_id).toBe('string');
|
||||
expect(typeof message.content).toBe('string');
|
||||
expect(typeof message.metadata).toBe('object');
|
||||
|
||||
// Mock successful operations
|
||||
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'typed-peer' }],
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'typed-peer' }],
|
||||
summary: 'Test summary',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ describe('Page', () => {
|
|||
mockOriginalPage.nextPage.mockRejectedValue(new Error('Failed to get next page'));
|
||||
const page = new Page(mockOriginalPage);
|
||||
|
||||
await expect(page.nextPage()).rejects.toThrow('Failed to get next page');
|
||||
await expect(page.nextPage()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -352,7 +352,7 @@ describe('Page', () => {
|
|||
for await (const item of page) {
|
||||
// This should throw
|
||||
}
|
||||
}).rejects.toThrow('Transform error');
|
||||
}).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle transform function returning null', async () => {
|
||||
|
|
|
|||
|
|
@ -42,16 +42,17 @@ describe('Peer', () => {
|
|||
environment: 'local',
|
||||
});
|
||||
|
||||
peer = new Peer('test-peer', honcho);
|
||||
peer = new Peer('test-peer', 'test-workspace', (honcho as any)._client);
|
||||
mockClient = (honcho as any)._client;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct properties', () => {
|
||||
const newPeer = new Peer('peer-id', honcho);
|
||||
const newPeer = new Peer('peer-id', 'test-workspace', mockClient);
|
||||
|
||||
expect(newPeer.id).toBe('peer-id');
|
||||
expect(newPeer['_honcho']).toBe(honcho);
|
||||
expect(newPeer.workspaceId).toBe('test-workspace');
|
||||
expect(newPeer['_client']).toBe(mockClient);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ describe('Peer', () => {
|
|||
});
|
||||
|
||||
it('should handle chat with target peer', async () => {
|
||||
const targetPeer = new Peer('target-peer', honcho);
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockResponse = { content: 'Targeted response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
|
|
@ -142,15 +143,11 @@ describe('Peer', () => {
|
|||
});
|
||||
|
||||
it('should handle all options together', async () => {
|
||||
const targetPeer = new Peer('target-peer', honcho);
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockResponse = { content: 'Full options response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
await peer.chat('Hello', {
|
||||
stream: true,
|
||||
target: targetPeer,
|
||||
sessionId: 'session-456'
|
||||
});
|
||||
await peer.chat('Hello', { stream: true, target: targetPeer, sessionId: 'session-456' });
|
||||
|
||||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
|
|
@ -162,7 +159,7 @@ describe('Peer', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.chat.mockRejectedValue(new Error('Chat failed'));
|
||||
|
||||
await expect(peer.chat('Hello')).rejects.toThrow('Chat failed');
|
||||
await expect(peer.chat('Hello')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -185,7 +182,7 @@ describe('Peer', () => {
|
|||
expect(mockClient.workspaces.peers.sessions.list).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ filter: undefined }
|
||||
{ filters: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -206,7 +203,7 @@ describe('Peer', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.sessions.list.mockRejectedValue(new Error('Failed to get sessions'));
|
||||
|
||||
await expect(peer.getSessions()).rejects.toThrow('Failed to get sessions');
|
||||
await expect(peer.getSessions()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -215,7 +212,7 @@ describe('Peer', () => {
|
|||
const message = peer.message('Test content');
|
||||
|
||||
expect(message).toEqual({
|
||||
peerId: 'test-peer',
|
||||
peer_id: 'test-peer',
|
||||
content: 'Test content',
|
||||
metadata: undefined,
|
||||
});
|
||||
|
|
@ -226,7 +223,7 @@ describe('Peer', () => {
|
|||
const message = peer.message('Hello there', { metadata });
|
||||
|
||||
expect(message).toEqual({
|
||||
peerId: 'test-peer',
|
||||
peer_id: 'test-peer',
|
||||
content: 'Hello there',
|
||||
metadata: { importance: 'high', category: 'greeting' },
|
||||
});
|
||||
|
|
@ -236,7 +233,7 @@ describe('Peer', () => {
|
|||
const message = peer.message('');
|
||||
|
||||
expect(message).toEqual({
|
||||
peerId: 'test-peer',
|
||||
peer_id: 'test-peer',
|
||||
content: '',
|
||||
metadata: undefined,
|
||||
});
|
||||
|
|
@ -275,7 +272,7 @@ describe('Peer', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.getOrCreate.mockRejectedValue(new Error('Peer not found'));
|
||||
|
||||
await expect(peer.getMetadata()).rejects.toThrow('Peer not found');
|
||||
await expect(peer.getMetadata()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -325,26 +322,66 @@ describe('Peer', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
await expect(peer.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
|
||||
await expect(peer.setMetadata({ key: 'value' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPeerConfig', () => {
|
||||
it('should return peer configuration', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: { observe_me: true, observe_others: false },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getPeerConfig();
|
||||
|
||||
expect(config).toEqual({ observe_me: true, observe_others: false });
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'test-peer' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty object when no configuration exists', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: null,
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getPeerConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPeerConfig', () => {
|
||||
it('should update peer configuration', async () => {
|
||||
const config = { observe_me: false, observe_others: true };
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
await peer.setPeerConfig(config);
|
||||
|
||||
expect(mockClient.workspaces.peers.update).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
{ configuration: config }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should search peer messages and return Page', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'test-peer' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'test-peer' },
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
it('should search peer messages and return array', async () => {
|
||||
const mockSearchResults = [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'test-peer' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'test-peer' },
|
||||
];
|
||||
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await peer.search('hello');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(mockClient.workspaces.peers.search).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-peer',
|
||||
|
|
@ -353,37 +390,27 @@ describe('Peer', () => {
|
|||
});
|
||||
|
||||
it('should handle empty search results', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [],
|
||||
total: 0,
|
||||
size: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults: any[] = [];
|
||||
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await peer.search('nonexistent');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error for empty query', async () => {
|
||||
await expect(peer.search('')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(peer.search(' ')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(peer.search('')).rejects.toThrow();
|
||||
await expect(peer.search(' ')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for non-string query', async () => {
|
||||
await expect(peer.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(peer.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(peer.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(peer.search(null as any)).rejects.toThrow();
|
||||
await expect(peer.search(undefined as any)).rejects.toThrow();
|
||||
await expect(peer.search(123 as any)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle complex search queries', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [],
|
||||
total: 0,
|
||||
size: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults: any[] = [];
|
||||
mockClient.workspaces.peers.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const complexQuery = 'complex query with "quotes" and special characters!@#$%';
|
||||
|
|
@ -399,7 +426,7 @@ describe('Peer', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.search.mockRejectedValue(new Error('Search failed'));
|
||||
|
||||
await expect(peer.search('test')).rejects.toThrow('Search failed');
|
||||
await expect(peer.search('test')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ jest.mock('@honcho-ai/core', () => {
|
|||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
list: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
setConfig: jest.fn(),
|
||||
},
|
||||
messages: {
|
||||
create: jest.fn(),
|
||||
list: jest.fn(),
|
||||
upload: jest.fn(),
|
||||
},
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
|
@ -49,25 +52,17 @@ describe('Session', () => {
|
|||
environment: 'local',
|
||||
});
|
||||
|
||||
session = new Session('test-session', honcho);
|
||||
session = new Session('test-session', 'test-workspace', (honcho as any)._client);
|
||||
mockClient = (honcho as any)._client;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct properties', () => {
|
||||
const newSession = new Session('session-id', honcho);
|
||||
|
||||
expect(newSession.id).toBe('session-id');
|
||||
expect(newSession['_honcho']).toBe(honcho);
|
||||
});
|
||||
|
||||
it('should handle constructor options', () => {
|
||||
const newSession = new Session('session-id', honcho, {
|
||||
anonymous: true,
|
||||
summarize: false
|
||||
});
|
||||
const newSession = new Session('session-id', 'test-workspace', mockClient);
|
||||
|
||||
expect(newSession.id).toBe('session-id');
|
||||
expect(newSession.workspaceId).toBe('test-workspace');
|
||||
expect(newSession['_client']).toBe(mockClient);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -80,12 +75,12 @@ describe('Session', () => {
|
|||
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ 'peer1': { observe_me: true, observe_others: false } }
|
||||
{ 'peer1': {} }
|
||||
);
|
||||
});
|
||||
|
||||
it('should add single peer by Peer object', async () => {
|
||||
const peer = new Peer('peer1', honcho);
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
|
||||
|
||||
await session.addPeers(peer);
|
||||
|
|
@ -93,7 +88,7 @@ describe('Session', () => {
|
|||
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ 'peer1': { observe_me: true, observe_others: false } }
|
||||
{ 'peer1': {} }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -106,18 +101,18 @@ describe('Session', () => {
|
|||
'test-workspace',
|
||||
'test-session',
|
||||
{
|
||||
'peer1': { observe_me: true, observe_others: false },
|
||||
'peer2': { observe_me: true, observe_others: false },
|
||||
'peer3': { observe_me: true, observe_others: false }
|
||||
'peer1': {},
|
||||
'peer2': {},
|
||||
'peer3': {}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should add array of Peer objects', async () => {
|
||||
const peers = [
|
||||
new Peer('peer1', honcho),
|
||||
new Peer('peer2', honcho),
|
||||
new Peer('peer3', honcho),
|
||||
new Peer('peer1', 'test-workspace', mockClient),
|
||||
new Peer('peer2', 'test-workspace', mockClient),
|
||||
new Peer('peer3', 'test-workspace', mockClient),
|
||||
];
|
||||
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
|
||||
|
||||
|
|
@ -127,9 +122,9 @@ describe('Session', () => {
|
|||
'test-workspace',
|
||||
'test-session',
|
||||
{
|
||||
'peer1': { observe_me: true, observe_others: false },
|
||||
'peer2': { observe_me: true, observe_others: false },
|
||||
'peer3': { observe_me: true, observe_others: false }
|
||||
'peer1': {},
|
||||
'peer2': {},
|
||||
'peer3': {}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -137,7 +132,7 @@ describe('Session', () => {
|
|||
it('should add mixed array of strings and Peer objects', async () => {
|
||||
const peers = [
|
||||
'string-peer',
|
||||
new Peer('object-peer', honcho),
|
||||
new Peer('object-peer', 'test-workspace', mockClient),
|
||||
];
|
||||
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
|
||||
|
||||
|
|
@ -147,8 +142,24 @@ describe('Session', () => {
|
|||
'test-workspace',
|
||||
'test-session',
|
||||
{
|
||||
'string-peer': { observe_me: true, observe_others: false },
|
||||
'object-peer': { observe_me: true, observe_others: false }
|
||||
'string-peer': {},
|
||||
'object-peer': {}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should add peer with SessionPeerConfig', async () => {
|
||||
const { SessionPeerConfig } = require('../src/session');
|
||||
const config = new SessionPeerConfig(false, true);
|
||||
mockClient.workspaces.sessions.peers.add.mockResolvedValue({});
|
||||
|
||||
await session.addPeers([['peer1', config]]);
|
||||
|
||||
expect(mockClient.workspaces.sessions.peers.add).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{
|
||||
'peer1': { observe_me: false, observe_others: true }
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -156,7 +167,7 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.peers.add.mockRejectedValue(new Error('Failed to add peers'));
|
||||
|
||||
await expect(session.addPeers('peer1')).rejects.toThrow('Failed to add peers');
|
||||
await expect(session.addPeers('peer1')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -169,12 +180,12 @@ describe('Session', () => {
|
|||
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ 'peer1': { observe_me: true, observe_others: false } }
|
||||
{ 'peer1': {} }
|
||||
);
|
||||
});
|
||||
|
||||
it('should set single peer by Peer object', async () => {
|
||||
const peer = new Peer('peer1', honcho);
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
|
||||
|
||||
await session.setPeers(peer);
|
||||
|
|
@ -182,12 +193,12 @@ describe('Session', () => {
|
|||
expect(mockClient.workspaces.sessions.peers.set).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ 'peer1': { observe_me: true, observe_others: false } }
|
||||
{ 'peer1': {} }
|
||||
);
|
||||
});
|
||||
|
||||
it('should set array of peers', async () => {
|
||||
const peers = ['peer1', new Peer('peer2', honcho)];
|
||||
const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)];
|
||||
mockClient.workspaces.sessions.peers.set.mockResolvedValue({});
|
||||
|
||||
await session.setPeers(peers);
|
||||
|
|
@ -196,8 +207,8 @@ describe('Session', () => {
|
|||
'test-workspace',
|
||||
'test-session',
|
||||
{
|
||||
'peer1': { observe_me: true, observe_others: false },
|
||||
'peer2': { observe_me: true, observe_others: false }
|
||||
'peer1': {},
|
||||
'peer2': {}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -205,7 +216,7 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.peers.set.mockRejectedValue(new Error('Failed to set peers'));
|
||||
|
||||
await expect(session.setPeers(['peer1'])).rejects.toThrow('Failed to set peers');
|
||||
await expect(session.setPeers(['peer1'])).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -223,7 +234,7 @@ describe('Session', () => {
|
|||
});
|
||||
|
||||
it('should remove single peer by Peer object', async () => {
|
||||
const peer = new Peer('peer1', honcho);
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
|
||||
|
||||
await session.removePeers(peer);
|
||||
|
|
@ -236,7 +247,7 @@ describe('Session', () => {
|
|||
});
|
||||
|
||||
it('should remove array of peers', async () => {
|
||||
const peers = ['peer1', new Peer('peer2', honcho)];
|
||||
const peers = ['peer1', new Peer('peer2', 'test-workspace', mockClient)];
|
||||
mockClient.workspaces.sessions.peers.remove.mockResolvedValue({});
|
||||
|
||||
await session.removePeers(peers);
|
||||
|
|
@ -251,12 +262,12 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.peers.remove.mockRejectedValue(new Error('Failed to remove peers'));
|
||||
|
||||
await expect(session.removePeers(['peer1'])).rejects.toThrow('Failed to remove peers');
|
||||
await expect(session.removePeers(['peer1'])).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPeers', () => {
|
||||
it('should return Page of Peer instances', async () => {
|
||||
it('should return array of Peer instances', async () => {
|
||||
const mockPeersData = {
|
||||
items: [
|
||||
{ id: 'peer1', metadata: {} },
|
||||
|
|
@ -271,6 +282,9 @@ describe('Session', () => {
|
|||
const peers = await session.getPeers();
|
||||
|
||||
expect(peers).toBeInstanceOf(Array);
|
||||
expect(peers).toHaveLength(2);
|
||||
expect(peers[0]).toBeInstanceOf(Peer);
|
||||
expect(peers[1]).toBeInstanceOf(Peer);
|
||||
expect(mockClient.workspaces.sessions.peers.list).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session'
|
||||
|
|
@ -295,14 +309,78 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.peers.list.mockRejectedValue(new Error('Failed to get peers'));
|
||||
|
||||
await expect(session.getPeers()).rejects.toThrow('Failed to get peers');
|
||||
await expect(session.getPeers()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPeerConfig', () => {
|
||||
it('should return peer configuration', async () => {
|
||||
const mockConfig = { observe_me: true, observe_others: false };
|
||||
mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(mockConfig);
|
||||
|
||||
const config = await session.getPeerConfig('peer1');
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
expect(mockClient.workspaces.sessions.peers.getConfig).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
'peer1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Peer object input', async () => {
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
const mockConfig = { observe_me: false, observe_others: true };
|
||||
mockClient.workspaces.sessions.peers.getConfig.mockResolvedValue(mockConfig);
|
||||
|
||||
const config = await session.getPeerConfig(peer);
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
expect(mockClient.workspaces.sessions.peers.getConfig).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
'peer1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPeerConfig', () => {
|
||||
it('should set peer configuration', async () => {
|
||||
const { SessionPeerConfig } = require('../src/session');
|
||||
const config = new SessionPeerConfig(false, true);
|
||||
mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({});
|
||||
|
||||
await session.setPeerConfig('peer1', config);
|
||||
|
||||
expect(mockClient.workspaces.sessions.peers.setConfig).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
'peer1',
|
||||
{ observe_me: false, observe_others: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Peer object input', async () => {
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
const { SessionPeerConfig } = require('../src/session');
|
||||
const config = new SessionPeerConfig(true, false);
|
||||
mockClient.workspaces.sessions.peers.setConfig.mockResolvedValue({});
|
||||
|
||||
await session.setPeerConfig(peer, config);
|
||||
|
||||
expect(mockClient.workspaces.sessions.peers.setConfig).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
'peer1',
|
||||
{ observe_me: true, observe_others: false }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMessages', () => {
|
||||
it('should add single message', async () => {
|
||||
const message = {
|
||||
peerId: 'peer1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Hello world',
|
||||
metadata: { type: 'greeting' },
|
||||
};
|
||||
|
|
@ -325,8 +403,8 @@ describe('Session', () => {
|
|||
|
||||
it('should add array of messages', async () => {
|
||||
const messages = [
|
||||
{ peerId: 'peer1', content: 'Message 1', metadata: { order: 1 } },
|
||||
{ peerId: 'peer2', content: 'Message 2', metadata: { order: 2 } },
|
||||
{ peer_id: 'peer1', content: 'Message 1', metadata: { order: 1 } },
|
||||
{ peer_id: 'peer2', content: 'Message 2', metadata: { order: 2 } },
|
||||
];
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
|
||||
|
||||
|
|
@ -346,7 +424,7 @@ describe('Session', () => {
|
|||
|
||||
it('should handle messages without metadata', async () => {
|
||||
const message = {
|
||||
peerId: 'peer1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Simple message',
|
||||
};
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue({});
|
||||
|
|
@ -356,7 +434,7 @@ describe('Session', () => {
|
|||
expect(mockClient.workspaces.sessions.messages.create).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ messages: [{ peer_id: 'peer1', content: 'Simple message', metadata: undefined }] }
|
||||
{ messages: [{ peer_id: 'peer1', content: 'Simple message' }] }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -375,12 +453,12 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.messages.create.mockRejectedValue(new Error('Failed to add messages'));
|
||||
|
||||
await expect(session.addMessages({ peerId: 'peer1', content: 'test' })).rejects.toThrow('Failed to add messages');
|
||||
await expect(session.addMessages({ peer_id: 'peer1', content: 'test' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessages', () => {
|
||||
it('should get messages without options', async () => {
|
||||
it('should get messages without filter', async () => {
|
||||
const mockMessagesData = {
|
||||
items: [
|
||||
{ id: 'msg1', content: 'Message 1', peer_id: 'peer1' },
|
||||
|
|
@ -411,22 +489,20 @@ describe('Session', () => {
|
|||
};
|
||||
mockClient.workspaces.sessions.messages.list.mockResolvedValue(mockMessagesData);
|
||||
|
||||
const options = {
|
||||
filter: { peer_id: 'peer1', type: 'important' }
|
||||
};
|
||||
await session.getMessages(options);
|
||||
const filter = { peer_id: { value: 'peer1' }, type: { value: 'important' } };
|
||||
await session.getMessages(filter);
|
||||
|
||||
expect(mockClient.workspaces.sessions.messages.list).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ peer_id: 'peer1', type: 'important' }
|
||||
{ peer_id: { value: 'peer1' }, type: { value: 'important' } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.messages.list.mockRejectedValue(new Error('Failed to get messages'));
|
||||
|
||||
await expect(session.getMessages()).rejects.toThrow('Failed to get messages');
|
||||
await expect(session.getMessages()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -462,7 +538,7 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.getOrCreate.mockRejectedValue(new Error('Session not found'));
|
||||
|
||||
await expect(session.getMetadata()).rejects.toThrow('Session not found');
|
||||
await expect(session.getMetadata()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -495,7 +571,7 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
await expect(session.setMetadata({ key: 'value' })).rejects.toThrow('Update failed');
|
||||
await expect(session.setMetadata({ key: 'value' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -503,8 +579,8 @@ describe('Session', () => {
|
|||
it('should get session context without options', async () => {
|
||||
const mockContext = {
|
||||
messages: [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hi there', peer_name: 'peer2' },
|
||||
{ id: 'msg1', content: 'Hello', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hi there', peer_id: 'peer2' },
|
||||
],
|
||||
summary: 'Conversation summary',
|
||||
};
|
||||
|
|
@ -525,13 +601,12 @@ describe('Session', () => {
|
|||
|
||||
it('should get session context with options', async () => {
|
||||
const mockContext = {
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'peer1' }],
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }],
|
||||
summary: 'Brief summary',
|
||||
};
|
||||
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
|
||||
|
||||
const options = { summary: true, tokens: 1000 };
|
||||
const context = await session.getContext(options);
|
||||
const context = await session.getContext({ summary: true, tokens: 1000 });
|
||||
|
||||
expect(context).toBeInstanceOf(SessionContext);
|
||||
expect(mockClient.workspaces.sessions.getContext).toHaveBeenCalledWith(
|
||||
|
|
@ -543,7 +618,7 @@ describe('Session', () => {
|
|||
|
||||
it('should handle context without summary', async () => {
|
||||
const mockContext = {
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'peer1' }],
|
||||
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'peer1' }],
|
||||
};
|
||||
mockClient.workspaces.sessions.getContext.mockResolvedValue(mockContext);
|
||||
|
||||
|
|
@ -555,62 +630,71 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.getContext.mockRejectedValue(new Error('Failed to get context'));
|
||||
|
||||
await expect(session.getContext()).rejects.toThrow('Failed to get context');
|
||||
await expect(session.getContext()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should search session messages and return Page', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults = [
|
||||
{ id: 'msg1', content: 'Hello world', peer_id: 'peer1' },
|
||||
{ id: 'msg2', content: 'Hello there', peer_id: 'peer2' },
|
||||
];
|
||||
mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await session.search('hello');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(mockClient.workspaces.sessions.search).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ query: 'hello' }
|
||||
{ query: 'hello', limit: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty search results', async () => {
|
||||
const mockSearchResults = {
|
||||
items: [],
|
||||
total: 0,
|
||||
size: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
const mockSearchResults: any[] = [];
|
||||
mockClient.workspaces.sessions.search.mockResolvedValue(mockSearchResults);
|
||||
|
||||
const results = await session.search('nonexistent');
|
||||
|
||||
expect(results).toBeInstanceOf(Page);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error for empty query', async () => {
|
||||
await expect(session.search('')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(session.search(' ')).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(session.search('')).rejects.toThrow();
|
||||
await expect(session.search(' ')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for non-string query', async () => {
|
||||
await expect(session.search(null as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(session.search(undefined as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(session.search(123 as any)).rejects.toThrow('Search query must be a non-empty string');
|
||||
await expect(session.search(null as any)).rejects.toThrow();
|
||||
await expect(session.search(undefined as any)).rejects.toThrow();
|
||||
await expect(session.search(123 as any)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.search.mockRejectedValue(new Error('Search failed'));
|
||||
|
||||
await expect(session.search('test')).rejects.toThrow('Search failed');
|
||||
await expect(session.search('test')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should upload file and return messages', async () => {
|
||||
const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
||||
const mockMessages = [
|
||||
{ id: 'msg1', content: 'test content', peer_id: 'peer1' }
|
||||
];
|
||||
mockClient.workspaces.sessions.messages.upload.mockResolvedValue(mockMessages);
|
||||
|
||||
const messages = await session.uploadFile(mockFile, 'peer1');
|
||||
|
||||
expect(messages).toEqual(mockMessages);
|
||||
expect(mockClient.workspaces.sessions.messages.upload).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ file: mockFile, peer_id: 'peer1' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -634,7 +718,7 @@ describe('Session', () => {
|
|||
});
|
||||
|
||||
it('should get working representation with Peer object', async () => {
|
||||
const peer = new Peer('peer1', honcho);
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
knowledge: 'Some knowledge',
|
||||
|
|
@ -669,8 +753,8 @@ describe('Session', () => {
|
|||
});
|
||||
|
||||
it('should get working representation with target Peer object', async () => {
|
||||
const peer = new Peer('peer1', honcho);
|
||||
const target = new Peer('target-peer', honcho);
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient);
|
||||
const target = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
target_knowledge: 'What peer1 knows about target',
|
||||
|
|
@ -690,7 +774,7 @@ describe('Session', () => {
|
|||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(new Error('Failed to get working representation'));
|
||||
|
||||
await expect(session.workingRep('peer1')).rejects.toThrow('Failed to get working representation');
|
||||
await expect(session.workingRep('peer1')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,26 +1,42 @@
|
|||
import { SessionContext } from '../src/session_context';
|
||||
import { Peer } from '../src/peer';
|
||||
|
||||
/**
|
||||
* Helper function to create a proper Message object for testing
|
||||
*/
|
||||
function createTestMessage(id: string, content: string, peer_id: string, additionalProps: any = {}): any {
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
peer_id,
|
||||
created_at: new Date().toISOString(),
|
||||
session_id: 'test-session',
|
||||
token_count: 0,
|
||||
workspace_id: 'test-workspace',
|
||||
...additionalProps
|
||||
};
|
||||
}
|
||||
|
||||
describe('SessionContext', () => {
|
||||
let sessionContext: SessionContext;
|
||||
let mockMessages: any[];
|
||||
|
||||
beforeEach(() => {
|
||||
mockMessages = [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: 'Hi there', peer_name: 'user' },
|
||||
{ id: 'msg3', content: 'How are you?', peer_name: 'user' },
|
||||
{ id: 'msg4', content: 'I am doing well, thank you!', peer_name: 'assistant' },
|
||||
createTestMessage('msg1', 'Hello', 'assistant'),
|
||||
createTestMessage('msg2', 'Hi there', 'user'),
|
||||
createTestMessage('msg3', 'How are you?', 'user'),
|
||||
createTestMessage('msg4', 'I am doing well, thank you!', 'assistant'),
|
||||
];
|
||||
|
||||
sessionContext = new SessionContext('test-session', mockMessages, 'This is a summary');
|
||||
sessionContext = new SessionContext('test-session', mockMessages, '');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with all properties', () => {
|
||||
expect(sessionContext.sessionId).toBe('test-session');
|
||||
expect(sessionContext.messages).toEqual(mockMessages);
|
||||
expect(sessionContext.summary).toBe('This is a summary');
|
||||
expect(sessionContext.summary).toBe('');
|
||||
});
|
||||
|
||||
it('should initialize with empty summary when not provided', () => {
|
||||
|
|
@ -53,24 +69,24 @@ describe('SessionContext', () => {
|
|||
const openAIMessages = sessionContext.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!' },
|
||||
{ role: 'assistant', content: 'Hello', name: 'assistant' },
|
||||
{ role: 'user', content: 'Hi there', name: 'user' },
|
||||
{ role: 'user', content: 'How are you?', name: 'user' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert messages to OpenAI format with Peer object', () => {
|
||||
const mockHoncho = {} as any;
|
||||
const assistantPeer = new Peer('assistant', mockHoncho);
|
||||
const mockClient = {} as any;
|
||||
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
|
||||
|
||||
const openAIMessages = sessionContext.toOpenAI(assistantPeer);
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!' },
|
||||
{ role: 'assistant', content: 'Hello', name: 'assistant' },
|
||||
{ role: 'user', content: 'Hi there', name: 'user' },
|
||||
{ role: 'user', content: 'How are you?', name: 'user' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -78,10 +94,10 @@ describe('SessionContext', () => {
|
|||
const openAIMessages = sessionContext.toOpenAI('different-assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'user', content: 'I am doing well, thank you!' },
|
||||
{ role: 'user', content: 'Hello', name: 'assistant' },
|
||||
{ role: 'user', content: 'Hi there', name: 'user' },
|
||||
{ role: 'user', content: 'How are you?', name: 'user' },
|
||||
{ role: 'user', content: 'I am doing well, thank you!', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -92,37 +108,50 @@ describe('SessionContext', () => {
|
|||
expect(openAIMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle messages with missing peer_name', () => {
|
||||
it('should include summary message when summary exists', () => {
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
|
||||
const openAIMessages = contextWithSummary.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'system', content: '<summary>This is a summary</summary>' },
|
||||
{ role: 'assistant', content: 'Hello', name: 'assistant' },
|
||||
{ role: 'user', content: 'Hi there', name: 'user' },
|
||||
{ role: 'user', content: 'How are you?', name: 'user' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle messages with missing peer_id', () => {
|
||||
const messagesWithMissingPeer = [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: 'No peer' }, // missing peer_name
|
||||
{ id: 'msg3', content: 'Another message', peer_name: null },
|
||||
createTestMessage('msg1', 'Hello', 'assistant'),
|
||||
createTestMessage('msg2', 'No peer', ''), // missing peer_id
|
||||
createTestMessage('msg3', 'Another message', ''), // null peer_id
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithMissingPeer);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'No peer' },
|
||||
{ role: 'user', content: 'Another message' },
|
||||
{ role: 'assistant', content: 'Hello', name: 'assistant' },
|
||||
{ role: 'user', content: 'No peer', name: '' },
|
||||
{ role: 'user', content: 'Another message', name: '' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle complex message content', () => {
|
||||
const complexMessages = [
|
||||
{ id: 'msg1', content: 'Message with\nnewlines and special chars!@#$%', peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: '', peer_name: 'user' }, // empty content
|
||||
{ id: 'msg3', content: ' whitespace ', peer_name: 'assistant' },
|
||||
createTestMessage('msg1', 'Message with\nnewlines and special chars!@#$%', 'assistant'),
|
||||
createTestMessage('msg2', '', 'user'), // empty content
|
||||
createTestMessage('msg3', ' whitespace ', 'assistant'),
|
||||
];
|
||||
const context = new SessionContext('test', complexMessages);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%' },
|
||||
{ role: 'user', content: '' },
|
||||
{ role: 'assistant', content: ' whitespace ' },
|
||||
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%', name: 'assistant' },
|
||||
{ role: 'user', content: '', name: 'user' },
|
||||
{ role: 'assistant', content: ' whitespace ', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -133,22 +162,22 @@ describe('SessionContext', () => {
|
|||
|
||||
expect(anthropicMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'user', content: 'user: Hi there' },
|
||||
{ role: 'user', content: 'user: How are you?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert messages to Anthropic format with Peer object', () => {
|
||||
const mockHoncho = {} as any;
|
||||
const assistantPeer = new Peer('assistant', mockHoncho);
|
||||
const mockClient = {} as any;
|
||||
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
|
||||
|
||||
const anthropicMessages = sessionContext.toAnthropic(assistantPeer);
|
||||
|
||||
expect(anthropicMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'user', content: 'user: Hi there' },
|
||||
{ role: 'user', content: 'user: How are you?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!' },
|
||||
]);
|
||||
});
|
||||
|
|
@ -157,10 +186,10 @@ describe('SessionContext', () => {
|
|||
const anthropicMessages = sessionContext.toAnthropic('different-assistant');
|
||||
|
||||
expect(anthropicMessages).toEqual([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
{ role: 'user', content: 'I am doing well, thank you!' },
|
||||
{ role: 'user', content: 'assistant: Hello' },
|
||||
{ role: 'user', content: 'user: Hi there' },
|
||||
{ role: 'user', content: 'user: How are you?' },
|
||||
{ role: 'user', content: 'assistant: I am doing well, thank you!' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -171,11 +200,24 @@ describe('SessionContext', () => {
|
|||
expect(anthropicMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle messages with missing peer_name', () => {
|
||||
it('should include summary message when summary exists', () => {
|
||||
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
|
||||
const anthropicMessages = contextWithSummary.toAnthropic('assistant');
|
||||
|
||||
expect(anthropicMessages).toEqual([
|
||||
{ role: 'user', content: '<summary>This is a summary</summary>' },
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'user: Hi there' },
|
||||
{ role: 'user', content: 'user: How are you?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you!' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle messages with missing peer_id', () => {
|
||||
const messagesWithMissingPeer = [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: 'No peer' }, // missing peer_name
|
||||
{ id: 'msg3', content: 'Another message', peer_name: undefined },
|
||||
createTestMessage('msg1', 'Hello', 'assistant'),
|
||||
createTestMessage('msg2', 'No peer', ''), // missing peer_id
|
||||
createTestMessage('msg3', 'Another message', ''), // undefined peer_id
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithMissingPeer);
|
||||
|
||||
|
|
@ -183,8 +225,8 @@ describe('SessionContext', () => {
|
|||
|
||||
expect(anthropicMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'No peer' },
|
||||
{ role: 'user', content: 'Another message' },
|
||||
{ role: 'user', content: ': No peer' },
|
||||
{ role: 'user', content: ': Another message' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -208,58 +250,56 @@ describe('SessionContext', () => {
|
|||
describe('toString', () => {
|
||||
it('should return correct string representation', () => {
|
||||
const result = sessionContext.toString();
|
||||
expect(result).toBe('SessionContext(messages=4)');
|
||||
expect(result).toBe('SessionContext(messages=4, summary=)');
|
||||
});
|
||||
|
||||
it('should handle empty messages', () => {
|
||||
const emptyContext = new SessionContext('session-id', []);
|
||||
const result = emptyContext.toString();
|
||||
expect(result).toBe('SessionContext(messages=0)');
|
||||
expect(result).toBe('SessionContext(messages=0, summary=)');
|
||||
});
|
||||
|
||||
it('should handle large number of messages', () => {
|
||||
const manyMessages = Array.from({ length: 1000 }, (_, i) => ({
|
||||
id: `msg${i}`,
|
||||
content: `Message ${i}`,
|
||||
peer_name: i % 2 === 0 ? 'assistant' : 'user',
|
||||
}));
|
||||
const manyMessages = Array.from({ length: 1000 }, (_, i) =>
|
||||
createTestMessage(`msg${i}`, `Message ${i}`, i % 2 === 0 ? 'assistant' : 'user')
|
||||
);
|
||||
const context = new SessionContext('session-id', manyMessages);
|
||||
|
||||
const result = context.toString();
|
||||
expect(result).toBe('SessionContext(messages=1000)');
|
||||
expect(result).toBe('SessionContext(messages=1000, summary=)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle messages with null content', () => {
|
||||
const messagesWithNullContent = [
|
||||
{ id: 'msg1', content: null, peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: undefined, peer_name: 'user' },
|
||||
createTestMessage('msg1', null as any, 'assistant'),
|
||||
createTestMessage('msg2', undefined as any, 'user'),
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithNullContent);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: null },
|
||||
{ role: 'user', content: undefined },
|
||||
{ role: 'assistant', content: null, name: 'assistant' },
|
||||
{ role: 'user', content: undefined, name: 'user' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle messages with non-string content', () => {
|
||||
const messagesWithNonStringContent = [
|
||||
{ id: 'msg1', content: 123, peer_name: 'assistant' },
|
||||
{ id: 'msg2', content: { text: 'object content' }, peer_name: 'user' },
|
||||
{ id: 'msg3', content: true, peer_name: 'assistant' },
|
||||
createTestMessage('msg1', 123 as any, 'assistant'),
|
||||
createTestMessage('msg2', { text: 'object content' } as any, 'user'),
|
||||
createTestMessage('msg3', true as any, 'assistant'),
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithNonStringContent);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 123 },
|
||||
{ role: 'user', content: { text: 'object content' } },
|
||||
{ role: 'assistant', content: true },
|
||||
{ role: 'assistant', content: 123, name: 'assistant' },
|
||||
{ role: 'user', content: { text: 'object content' }, name: 'user' },
|
||||
{ role: 'assistant', content: true, name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -270,57 +310,54 @@ describe('SessionContext', () => {
|
|||
|
||||
expect(context.sessionId).toBe(longSessionId);
|
||||
expect(context.summary).toBe(longSummary);
|
||||
expect(context.length).toBe(4);
|
||||
expect(context.length).toBe(5); // 4 messages + 1 summary
|
||||
});
|
||||
|
||||
it('should handle messages with additional properties', () => {
|
||||
const messagesWithExtraProps = [
|
||||
{
|
||||
id: 'msg1',
|
||||
content: 'Hello',
|
||||
peer_name: 'assistant',
|
||||
createTestMessage('msg1', 'Hello', 'assistant', {
|
||||
timestamp: '2023-01-01T00:00:00Z',
|
||||
metadata: { important: true },
|
||||
extra_field: 'extra_value'
|
||||
},
|
||||
}),
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithExtraProps);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hello', name: 'assistant' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle case-sensitive peer names', () => {
|
||||
const caseMessages = [
|
||||
{ id: 'msg1', content: 'Hello', peer_name: 'Assistant' },
|
||||
{ id: 'msg2', content: 'Hi', peer_name: 'ASSISTANT' },
|
||||
{ id: 'msg3', content: 'Hey', peer_name: 'assistant' },
|
||||
createTestMessage('msg1', 'Hello', 'Assistant'),
|
||||
createTestMessage('msg2', 'Hi', 'ASSISTANT'),
|
||||
createTestMessage('msg3', 'Hey', 'assistant'),
|
||||
];
|
||||
const context = new SessionContext('test', caseMessages);
|
||||
|
||||
const openAIMessages = context.toOpenAI('assistant');
|
||||
|
||||
expect(openAIMessages).toEqual([
|
||||
{ role: 'user', content: 'Hello' }, // 'Assistant' != 'assistant'
|
||||
{ role: 'user', content: 'Hi' }, // 'ASSISTANT' != 'assistant'
|
||||
{ role: 'assistant', content: 'Hey' }, // exact match
|
||||
{ role: 'user', content: 'Hello', name: 'Assistant' }, // 'Assistant' != 'assistant'
|
||||
{ role: 'user', content: 'Hi', name: 'ASSISTANT' }, // 'ASSISTANT' != 'assistant'
|
||||
{ role: 'assistant', content: 'Hey', name: 'assistant' }, // exact match
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle messages without id field', () => {
|
||||
const messagesWithoutId = [
|
||||
{ content: 'Message without ID', peer_name: 'assistant' },
|
||||
{ peer_name: 'user', content: 'Another message' },
|
||||
createTestMessage('', 'Message without ID', 'assistant'),
|
||||
createTestMessage('', 'Another message', 'user'),
|
||||
];
|
||||
const context = new SessionContext('test', messagesWithoutId);
|
||||
|
||||
expect(context.length).toBe(2);
|
||||
expect(context.toOpenAI('assistant')).toEqual([
|
||||
{ role: 'assistant', content: 'Message without ID' },
|
||||
{ role: 'user', content: 'Another message' },
|
||||
{ role: 'assistant', content: 'Message without ID', name: 'assistant' },
|
||||
{ role: 'user', content: 'Another message', name: 'user' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "1.2.0",
|
||||
"@honcho-ai/core": "1.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.2",
|
||||
|
|
@ -89,25 +90,25 @@
|
|||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.1.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.3", "@biomejs/cli-darwin-x64": "2.1.3", "@biomejs/cli-linux-arm64": "2.1.3", "@biomejs/cli-linux-arm64-musl": "2.1.3", "@biomejs/cli-linux-x64": "2.1.3", "@biomejs/cli-linux-x64-musl": "2.1.3", "@biomejs/cli-win32-arm64": "2.1.3", "@biomejs/cli-win32-x64": "2.1.3" }, "bin": { "biome": "bin/biome" } }, "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.2.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-VPHCFIGfC00GeE4P83DDIT7hkuMnMVkWlMTmMd2tw4HSEUciqLBh09AX/6aMKfJAzprg1diub6pJJ6LJP6eJ+g=="],
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.3.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-yhxpZ00sQhO24PrHoTlj1cwR1q7/8FKSFZoCImqsFFmdERiXtE74YXhhfqzseyuShu1WfM7IwD8T2oCOB3hu7A=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
|
||||
|
||||
|
|
@ -161,7 +162,7 @@
|
|||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="],
|
||||
|
||||
|
|
@ -173,9 +174,9 @@
|
|||
|
||||
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
||||
|
||||
|
|
@ -197,8 +198,6 @@
|
|||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="],
|
||||
|
|
@ -207,7 +206,7 @@
|
|||
|
||||
"babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="],
|
||||
|
||||
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.1", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-23fWKohMTvS5s0wwJKycOe0dBdCwQ6+iiLaNR9zy8P13mtFRFM9qLLX6HJX5DL2pi/FNDf3fCQHM4FIMoHH/7w=="],
|
||||
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="],
|
||||
|
||||
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
|
||||
|
||||
|
|
@ -231,7 +230,7 @@
|
|||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
|
|
@ -275,9 +274,7 @@
|
|||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.192", "", {}, "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.197", "", {}, "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ=="],
|
||||
|
||||
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
|
||||
|
||||
|
|
@ -311,8 +308,6 @@
|
|||
|
||||
"fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="],
|
||||
|
||||
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
|
@ -347,6 +342,8 @@
|
|||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
|
@ -393,8 +390,6 @@
|
|||
|
||||
"istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="],
|
||||
|
||||
"jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="],
|
||||
|
||||
"jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="],
|
||||
|
||||
"jest-changed-files": ["jest-changed-files@29.7.0", "", { "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="],
|
||||
|
|
@ -489,10 +484,14 @@
|
|||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
|
@ -595,15 +594,17 @@
|
|||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-jest": ["ts-jest@29.4.0", "", { "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q=="],
|
||||
"ts-jest": ["ts-jest@29.4.1", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
|
|
@ -619,6 +620,8 @@
|
|||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
|
@ -635,11 +638,13 @@
|
|||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.120", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA=="],
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
|
|
@ -649,8 +654,6 @@
|
|||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
|
@ -660,7 +663,5 @@
|
|||
"@honcho-ai/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Honcho } from '../src';
|
||||
import { Honcho, Message } from '../src';
|
||||
|
||||
/**
|
||||
* Example demonstrating how to get context from a session with summary and token limits.
|
||||
|
|
@ -15,19 +15,19 @@ async function main() {
|
|||
|
||||
console.log('Creating peers...');
|
||||
const peers = [
|
||||
honcho.peer('alice'),
|
||||
honcho.peer('bob'),
|
||||
honcho.peer('charlie'),
|
||||
await honcho.peer('alice'),
|
||||
await honcho.peer('bob'),
|
||||
await honcho.peer('charlie'),
|
||||
];
|
||||
|
||||
// Create a new session
|
||||
const sessionId = `context_test_${crypto.randomUUID()}`;
|
||||
const session = honcho.session(sessionId);
|
||||
const session = await honcho.session(sessionId);
|
||||
console.log(`Created session: ${sessionId}`);
|
||||
|
||||
console.log('Generating random messages...');
|
||||
// Generate some random messages from alice, bob, and charlie and add them to the session
|
||||
const messages = [];
|
||||
const messages: Message[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
|
||||
messages.push(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Honcho } from '../src';
|
||||
import { Honcho, Message } from '../src';
|
||||
|
||||
/**
|
||||
* Example demonstrating how to get peer representations.
|
||||
|
|
@ -15,19 +15,19 @@ async function main() {
|
|||
|
||||
console.log('Creating peers...');
|
||||
const peers = [
|
||||
honcho.peer('alice'),
|
||||
honcho.peer('bob'),
|
||||
honcho.peer('charlie'),
|
||||
await honcho.peer('alice'),
|
||||
await honcho.peer('bob'),
|
||||
await honcho.peer('charlie'),
|
||||
];
|
||||
|
||||
// Create a new session
|
||||
const sessionId = `context_test_${crypto.randomUUID()}`;
|
||||
const session = honcho.session(sessionId);
|
||||
const session = await honcho.session(sessionId);
|
||||
console.log(`Created session: ${sessionId}`);
|
||||
|
||||
console.log('Generating random messages...');
|
||||
// Generate some random messages from alice, bob, and charlie and add them to the session
|
||||
const messages = [];
|
||||
const messages: Message[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
|
||||
messages.push(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Honcho } from '../src';
|
||||
import { Honcho, Message } from '../src';
|
||||
|
||||
/**
|
||||
* Example demonstrating search functionality across different scopes.
|
||||
|
|
@ -15,16 +15,16 @@ async function main() {
|
|||
|
||||
console.log('Creating peers...');
|
||||
const peers = [
|
||||
honcho.peer('alice'),
|
||||
honcho.peer('bob'),
|
||||
honcho.peer('charlie'),
|
||||
await honcho.peer('alice'),
|
||||
await honcho.peer('bob'),
|
||||
await honcho.peer('charlie'),
|
||||
];
|
||||
|
||||
const alice = peers[0];
|
||||
|
||||
// Create a new session
|
||||
const sessionId = `search_test_${crypto.randomUUID()}`;
|
||||
const session = honcho.session(sessionId);
|
||||
const session = await honcho.session(sessionId);
|
||||
console.log(`Created session: ${sessionId}`);
|
||||
|
||||
// Create a message with our special keyword
|
||||
|
|
@ -34,7 +34,7 @@ async function main() {
|
|||
|
||||
console.log('Generating random messages...');
|
||||
// Generate some random messages from alice, bob, and charlie and add them to the session
|
||||
const messages = [];
|
||||
const messages: Message[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const randomPeer = peers[Math.floor(Math.random() * peers.length)];
|
||||
messages.push(
|
||||
|
|
@ -48,24 +48,24 @@ async function main() {
|
|||
console.log('Searching the session...');
|
||||
// Search the session for the special keyword
|
||||
const sessionSearchResults = await session.search(keyword);
|
||||
console.log(`Session search returned ${sessionSearchResults.total} results:`);
|
||||
for await (const message of sessionSearchResults) {
|
||||
console.log(`Session search returned ${sessionSearchResults.length} results:`);
|
||||
for (const message of sessionSearchResults) {
|
||||
console.log(` - ${message.content} (from ${message.peer_id})`);
|
||||
}
|
||||
|
||||
console.log('Searching the workspace...');
|
||||
// Search the workspace for the special keyword
|
||||
const workspaceSearchResults = await honcho.search(keyword);
|
||||
console.log(`Workspace search returned ${workspaceSearchResults.total} results:`);
|
||||
for await (const message of workspaceSearchResults) {
|
||||
console.log(`Workspace search returned ${workspaceSearchResults.length} results:`);
|
||||
for (const message of workspaceSearchResults) {
|
||||
console.log(` - ${message.content} (from ${message.peer_id})`);
|
||||
}
|
||||
|
||||
console.log('Searching alice\'s messages...');
|
||||
// Search alice's messages for the special keyword
|
||||
const aliceSearchResults = await alice.search(keyword);
|
||||
console.log(`Alice search returned ${aliceSearchResults.total} results:`);
|
||||
for await (const message of aliceSearchResults) {
|
||||
console.log(`Alice search returned ${aliceSearchResults.length} results:`);
|
||||
for (const message of aliceSearchResults) {
|
||||
console.log(` - ${message.content} (from ${message.peer_id})`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -20,8 +20,9 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "1.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"@honcho-ai/core": "1.2.0"
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.2",
|
||||
|
|
|
|||
|
|
@ -1,94 +1,272 @@
|
|||
import HonchoCore from '@honcho-ai/core'
|
||||
import type { DefaultQuery } from '@honcho-ai/core/src/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type {
|
||||
DeriverStatus,
|
||||
WorkspaceDeriverStatusParams,
|
||||
} from '@honcho-ai/core/src/resources/workspaces/workspaces'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import { Session } from './session'
|
||||
import {
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
type HonchoConfig,
|
||||
HonchoConfigSchema,
|
||||
LimitSchema,
|
||||
type PeerConfig,
|
||||
PeerConfigSchema,
|
||||
PeerIdSchema,
|
||||
type PeerMetadata,
|
||||
PeerMetadataSchema,
|
||||
SearchQuerySchema,
|
||||
type SessionConfig,
|
||||
SessionConfigSchema,
|
||||
SessionIdSchema,
|
||||
type SessionMetadata,
|
||||
SessionMetadataSchema,
|
||||
type WorkspaceMetadata,
|
||||
WorkspaceMetadataSchema,
|
||||
} from './validation'
|
||||
|
||||
/**
|
||||
* Main client for the Honcho TypeScript SDK.
|
||||
* Provides access to peers, sessions, and workspace operations.
|
||||
*
|
||||
* Provides access to peers, sessions, and workspace operations with configuration
|
||||
* from environment variables or explicit parameters. This is the primary entry
|
||||
* point for interacting with the Honcho conversational memory platform.
|
||||
*
|
||||
* For advanced usage, the underlying @honcho-ai/core client can be accessed via the
|
||||
* `core` property to use functionality not exposed through this SDK.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const honcho = new Honcho({
|
||||
* apiKey: 'your-api-key',
|
||||
* workspaceId: 'your-workspace-id'
|
||||
* })
|
||||
*
|
||||
* const peer = await honcho.peer('user123')
|
||||
* const session = await honcho.session('session456')
|
||||
* ```
|
||||
*/
|
||||
export class Honcho {
|
||||
private _client: InstanceType<typeof HonchoCore>
|
||||
/**
|
||||
* Workspace ID for scoping operations.
|
||||
*/
|
||||
readonly workspaceId: string
|
||||
/**
|
||||
* Reference to the core Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
|
||||
/**
|
||||
* Access the underlying @honcho-ai/core client. The @honcho-ai/core client is the raw Stainless-generated client,
|
||||
* allowing users to access functionality that is not exposed through this SDK.
|
||||
*
|
||||
* @returns The underlying HonchoCore client instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Honcho } from '@honcho-ai/sdk';
|
||||
*
|
||||
* const client = new Honcho();
|
||||
*
|
||||
* const workspace = await client.core.workspaces.getOrCreate({ id: "custom-workspace-id" });
|
||||
* ```
|
||||
*/
|
||||
get core(): InstanceType<typeof HonchoCore> {
|
||||
return this._client
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Honcho client.
|
||||
*
|
||||
* @param options - Configuration options for the client
|
||||
* @param options.apiKey - API key for authentication. If not provided, will attempt to
|
||||
* read from HONCHO_API_KEY environment variable
|
||||
* @param options.environment - Environment to use (local, production, or demo)
|
||||
* @param options.baseURL - Base URL for the Honcho API. If not provided, will attempt to
|
||||
* read from HONCHO_URL environment variable or default to the
|
||||
* production API URL
|
||||
* @param options.workspaceId - Workspace ID to use for operations. If not provided, will
|
||||
* attempt to read from HONCHO_WORKSPACE_ID environment variable
|
||||
* or default to "default"
|
||||
* @param options.timeout - Optional custom timeout for the HTTP client
|
||||
* @param options.maxRetries - Optional custom maximum number of retries for the HTTP client
|
||||
* @param options.defaultHeaders - Optional custom default headers for the HTTP client
|
||||
* @param options.defaultQuery - Optional custom default query parameters for the HTTP client
|
||||
*/
|
||||
constructor(options: {
|
||||
apiKey?: string
|
||||
environment?: 'local' | 'production' | 'demo'
|
||||
baseURL?: string
|
||||
workspaceId?: string
|
||||
timeout?: number
|
||||
maxRetries?: number
|
||||
defaultHeaders?: Record<string, string>
|
||||
defaultQuery?: Record<string, unknown>
|
||||
}) {
|
||||
constructor(options: HonchoConfig) {
|
||||
const validatedOptions = HonchoConfigSchema.parse(options)
|
||||
this.workspaceId =
|
||||
options.workspaceId || process.env.HONCHO_WORKSPACE_ID || 'default'
|
||||
validatedOptions.workspaceId ||
|
||||
process.env.HONCHO_WORKSPACE_ID ||
|
||||
'default'
|
||||
this._client = new HonchoCore({
|
||||
apiKey: options.apiKey || process.env.HONCHO_API_KEY,
|
||||
environment: options.environment,
|
||||
baseURL: options.baseURL || process.env.HONCHO_URL,
|
||||
timeout: options.timeout,
|
||||
maxRetries: options.maxRetries,
|
||||
defaultHeaders: options.defaultHeaders,
|
||||
defaultQuery: options.defaultQuery as any,
|
||||
}) as any
|
||||
apiKey: validatedOptions.apiKey || process.env.HONCHO_API_KEY,
|
||||
environment: validatedOptions.environment,
|
||||
baseURL: validatedOptions.baseURL || process.env.HONCHO_URL,
|
||||
timeout: validatedOptions.timeout,
|
||||
maxRetries: validatedOptions.maxRetries,
|
||||
defaultHeaders: validatedOptions.defaultHeaders,
|
||||
defaultQuery: validatedOptions.defaultQuery as DefaultQuery,
|
||||
})
|
||||
// Note: Constructor cannot be async, so we can't await here
|
||||
// The workspace will be created on first use if it doesn't exist
|
||||
// due to the upsert behavior of the API
|
||||
this._client.workspaces.getOrCreate({ id: this.workspaceId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a peer with the given ID.
|
||||
*
|
||||
* Creates a Peer object that can be used to interact with the specified peer.
|
||||
* If metadata or config is provided, makes an API call to get/create the peer
|
||||
* immediately with those values.
|
||||
*
|
||||
* Provided metadata and configuration will overwrite existing data for this peer
|
||||
* if it already exists.
|
||||
*
|
||||
* @param id - Unique identifier for the peer within the workspace. Should be a
|
||||
* stable identifier that can be used consistently across sessions.
|
||||
* @param metadata - Optional metadata dictionary to associate with this peer.
|
||||
* If set, will get/create peer immediately with metadata.
|
||||
* @param config - Optional configuration to set for this peer.
|
||||
* If set, will get/create peer immediately with flags.
|
||||
* @returns Promise resolving to a Peer object that can be used to send messages,
|
||||
* join sessions, and query the peer's knowledge representations
|
||||
* @throws Error if the peer ID is empty or invalid
|
||||
*/
|
||||
peer(id: string, options?: { config?: Record<string, unknown> }): Peer {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('Peer ID must be a non-empty string')
|
||||
async peer(
|
||||
id: string,
|
||||
options?: {
|
||||
metadata?: PeerMetadata
|
||||
config?: PeerConfig
|
||||
}
|
||||
return new Peer(id, this, options?.config)
|
||||
): Promise<Peer> {
|
||||
const validatedId = PeerIdSchema.parse(id)
|
||||
const validatedMetadata = options?.metadata
|
||||
? PeerMetadataSchema.parse(options.metadata)
|
||||
: undefined
|
||||
const validatedConfig = options?.config
|
||||
? PeerConfigSchema.parse(options.config)
|
||||
: undefined
|
||||
const peer = new Peer(validatedId, this.workspaceId, this._client)
|
||||
|
||||
if (validatedConfig || validatedMetadata) {
|
||||
await this._client.workspaces.peers.getOrCreate(this.workspaceId, {
|
||||
id: peer.id,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
}
|
||||
|
||||
return 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.
|
||||
*
|
||||
* @param filters - Optional filter criteria for peers. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @returns Promise resolving to a Page of Peer objects representing all peers in the workspace
|
||||
*/
|
||||
async getPeers(
|
||||
filter?: { [key: string]: unknown } | null
|
||||
): Promise<Page<Peer>> {
|
||||
async getPeers(filters?: Filters): Promise<Page<Peer>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const peersPage = await this._client.workspaces.peers.list(
|
||||
this.workspaceId,
|
||||
{ filter }
|
||||
{ filters: validatedFilter }
|
||||
)
|
||||
return new Page(
|
||||
peersPage,
|
||||
(peer) => new Peer(peer.id, this.workspaceId, this._client)
|
||||
)
|
||||
return new Page(peersPage, (peer: any) => new Peer(peer.id, this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a session with the given ID.
|
||||
*
|
||||
* Creates a Session object that can be used to manage conversations between
|
||||
* multiple peers. If metadata or config is provided, makes an API call to
|
||||
* get/create the session immediately with those values.
|
||||
*
|
||||
* Provided metadata and configuration will overwrite existing data for this session
|
||||
* if it already exists.
|
||||
*
|
||||
* @param id - Unique identifier for the session within the workspace. Should be a
|
||||
* stable identifier that can be used consistently to reference the
|
||||
* same conversation
|
||||
* @param metadata - Optional metadata dictionary to associate with this session.
|
||||
* If set, will get/create session immediately with metadata.
|
||||
* @param config - Optional configuration to set for this session.
|
||||
* If set, will get/create session immediately with flags.
|
||||
* @returns Promise resolving to a Session object that can be used to add peers,
|
||||
* send messages, and manage conversation context
|
||||
* @throws Error if the session ID is empty or invalid
|
||||
*/
|
||||
session(id: string, options?: { config?: Record<string, unknown> }): Session {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('Session ID must be a non-empty string')
|
||||
async session(
|
||||
id: string,
|
||||
options?: {
|
||||
metadata?: SessionMetadata
|
||||
config?: SessionConfig
|
||||
}
|
||||
return new Session(id, this, options?.config)
|
||||
): Promise<Session> {
|
||||
const validatedId = SessionIdSchema.parse(id)
|
||||
const validatedMetadata = options?.metadata
|
||||
? SessionMetadataSchema.parse(options.metadata)
|
||||
: undefined
|
||||
const validatedConfig = options?.config
|
||||
? SessionConfigSchema.parse(options.config)
|
||||
: undefined
|
||||
const session = new Session(validatedId, this.workspaceId, this._client)
|
||||
|
||||
if (validatedConfig || validatedMetadata) {
|
||||
await this._client.workspaces.sessions.getOrCreate(this.workspaceId, {
|
||||
id: session.id,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sessions in the current workspace.
|
||||
*
|
||||
* 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/v2/guides/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 getSessions(
|
||||
filter?: { [key: string]: unknown } | null
|
||||
): Promise<Page<Session>> {
|
||||
async getSessions(filters?: Filters): Promise<Page<Session>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const sessionsPage = await this._client.workspaces.sessions.list(
|
||||
this.workspaceId,
|
||||
{ filter }
|
||||
{ filters: validatedFilter }
|
||||
)
|
||||
return new Page(
|
||||
sessionsPage,
|
||||
(session: any) => new Session(session.id, this)
|
||||
(session) => new Session(session.id, this.workspaceId, this._client)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for the current workspace.
|
||||
*
|
||||
* Makes an API call to retrieve metadata associated with the current workspace.
|
||||
* Workspace metadata can include settings, configuration, or any other
|
||||
* key-value data associated with the workspace.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the workspace's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
*/
|
||||
async getMetadata(): Promise<Record<string, unknown>> {
|
||||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
|
|
@ -99,18 +277,35 @@ export class Honcho {
|
|||
|
||||
/**
|
||||
* Set metadata for the current workspace.
|
||||
*
|
||||
* Makes an API call to update the metadata associated with the current workspace.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with the workspace.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.update(this.workspaceId, { metadata })
|
||||
async setMetadata(metadata: WorkspaceMetadata): Promise<void> {
|
||||
const validatedMetadata = WorkspaceMetadataSchema.parse(metadata)
|
||||
await this._client.workspaces.update(this.workspaceId, {
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workspace IDs from the Honcho instance.
|
||||
*
|
||||
* 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/v2/guides/using-filters).
|
||||
* @returns Promise resolving to a list of workspace ID strings. Returns an empty
|
||||
* list if no workspaces are accessible or none exist
|
||||
*/
|
||||
async getWorkspaces(
|
||||
filter?: { [key: string]: unknown } | null
|
||||
): Promise<string[]> {
|
||||
const workspacesPage = await this._client.workspaces.list({ filter })
|
||||
async getWorkspaces(filters?: Filters): Promise<string[]> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const workspacesPage = await this._client.workspaces.list({
|
||||
filters: validatedFilter,
|
||||
})
|
||||
const ids: string[] = []
|
||||
for await (const workspace of workspacesPage) {
|
||||
ids.push(workspace.id)
|
||||
|
|
@ -123,45 +318,63 @@ export class Honcho {
|
|||
*
|
||||
* Makes an API call to search for messages in the current workspace.
|
||||
*
|
||||
* @param query The search query to use
|
||||
* @returns A Page of Message objects representing the search results.
|
||||
* Returns an empty page if no messages are found.
|
||||
* @param query - The search query to use
|
||||
* @param filters - Optional filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @param limit - Number of results to return (1-100, default: 10).
|
||||
* @returns Promise resolving to an array of Message objects representing the search results.
|
||||
* Returns an empty array if no messages are found.
|
||||
* @throws Error if the search query is empty or invalid
|
||||
*/
|
||||
async search(query: string): Promise<Page<any>> {
|
||||
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
||||
throw new Error('Search query must be a non-empty string')
|
||||
async search(
|
||||
query: string,
|
||||
options?: {
|
||||
filters?: Filters
|
||||
limit?: number
|
||||
}
|
||||
const messagesPage = await this._client.workspaces.search(
|
||||
this.workspaceId,
|
||||
{ body: query }
|
||||
)
|
||||
return new Page(messagesPage)
|
||||
): Promise<Message[]> {
|
||||
const validatedQuery = SearchQuerySchema.parse(query)
|
||||
const validatedFilters = options?.filters
|
||||
? FilterSchema.parse(options.filters)
|
||||
: undefined
|
||||
const validatedLimit = options?.limit
|
||||
? LimitSchema.parse(options.limit)
|
||||
: undefined
|
||||
return await this._client.workspaces.search(this.workspaceId, {
|
||||
query: validatedQuery,
|
||||
filters: validatedFilters,
|
||||
limit: validatedLimit,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
*
|
||||
* @param options Configuration options for the status request
|
||||
* @param options.observerId Optional observer ID to scope the status to
|
||||
* @param options.senderId Optional sender ID to scope the status to
|
||||
* @param options.sessionId Optional session ID to scope the status to
|
||||
* @returns Promise resolving to the deriver status information
|
||||
* Makes an API call to retrieve the current status of the deriver processing queue.
|
||||
* The deriver is responsible for processing messages and updating peer representations.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.sessionId - Optional session ID to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(options?: {
|
||||
observerId?: string
|
||||
senderId?: string
|
||||
sessionId?: string
|
||||
}): Promise<{
|
||||
async getDeriverStatus(options?: DeriverStatusOptions): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, any>
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const queryParams: any = {}
|
||||
if (options?.observerId) queryParams.observer_id = options.observerId
|
||||
if (options?.senderId) queryParams.sender_id = options.senderId
|
||||
if (options?.sessionId) queryParams.session_id = options.sessionId
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const queryParams: WorkspaceDeriverStatusParams = {}
|
||||
if (validatedOptions?.observerId)
|
||||
queryParams.observer_id = validatedOptions.observerId
|
||||
if (validatedOptions?.senderId)
|
||||
queryParams.sender_id = validatedOptions.senderId
|
||||
if (validatedOptions?.sessionId)
|
||||
queryParams.session_id = validatedOptions.sessionId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
|
|
@ -184,31 +397,29 @@ export class Honcho {
|
|||
*
|
||||
* The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
*
|
||||
* @param options Configuration options for the status request
|
||||
* @param options.observerId Optional observer ID to scope the status to
|
||||
* @param options.senderId Optional sender ID to scope the status to
|
||||
* @param options.sessionId Optional session ID to scope the status to
|
||||
* @param options.timeoutMs Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.sessionId - Optional session ID to scope the status to
|
||||
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @returns Promise resolving to the final deriver status when processing is complete
|
||||
* @throws Error if timeout is exceeded before processing completes
|
||||
*/
|
||||
async pollDeriverStatus(options?: {
|
||||
observerId?: string
|
||||
senderId?: string
|
||||
sessionId?: string
|
||||
timeoutMs?: number
|
||||
}): Promise<{
|
||||
async pollDeriverStatus(options?: DeriverStatusOptions): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, any>
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(options)
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
|
@ -237,4 +448,13 @@ export class Honcho {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Honcho client.
|
||||
*
|
||||
* @returns A string representation suitable for debugging
|
||||
*/
|
||||
toString(): string {
|
||||
return `Honcho(workspaceId='${this.workspaceId}', baseURL='${this._client.baseURL}')`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,29 @@
|
|||
// Main entry point for the Honcho TypeScript SDK
|
||||
// Exports all main classes and types
|
||||
|
||||
export { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
export { Honcho } from './client'
|
||||
export { Page } from './pagination'
|
||||
export { Peer } from './peer'
|
||||
export { Session, SessionPeerConfig } from './session'
|
||||
export { SessionContext } from './session_context'
|
||||
|
||||
// Export validation types for advanced usage
|
||||
export type {
|
||||
ChatQuery,
|
||||
ContextParams,
|
||||
DeriverStatusOptions,
|
||||
FileUpload,
|
||||
Filters,
|
||||
HonchoConfig,
|
||||
MessageAddition,
|
||||
MessageCreate,
|
||||
PeerAddition,
|
||||
PeerConfig,
|
||||
PeerMetadata,
|
||||
PeerRemoval,
|
||||
SessionConfig,
|
||||
SessionMetadata,
|
||||
WorkingRepParams,
|
||||
WorkspaceMetadata,
|
||||
} from './validation'
|
||||
|
|
|
|||
|
|
@ -1,55 +1,95 @@
|
|||
import type { Honcho } from './client'
|
||||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import { Page } from './pagination'
|
||||
import { Session } from './session'
|
||||
import {
|
||||
ChatQuerySchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
LimitSchema,
|
||||
MessageContentSchema,
|
||||
MessageMetadataSchema,
|
||||
SearchQuerySchema,
|
||||
type MessageCreate as ValidatedMessageCreate,
|
||||
} from './validation'
|
||||
|
||||
/**
|
||||
* Represents a peer in the Honcho system.
|
||||
*
|
||||
* Peers can send messages, participate in sessions, and maintain both global
|
||||
* and local representations for contextual interactions. A peer represents
|
||||
* an entity (user, assistant, etc.) that can communicate within the system.
|
||||
*/
|
||||
export class Peer {
|
||||
/**
|
||||
* Unique identifier for this peer.
|
||||
*/
|
||||
readonly id: string
|
||||
private _honcho: Honcho
|
||||
/**
|
||||
* Workspace ID for scoping operations.
|
||||
*/
|
||||
readonly workspaceId: string
|
||||
/**
|
||||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
|
||||
/**
|
||||
* Initialize a new Peer.
|
||||
* Initialize a new Peer. **Do not call this directly, use the client.peer() method instead.**
|
||||
*
|
||||
* @param id - Unique identifier for this peer within the workspace
|
||||
* @param workspaceId - Workspace ID for scoping operations
|
||||
* @param client - Reference to the parent Honcho client instance
|
||||
*/
|
||||
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
this.id = id
|
||||
this._honcho = honcho
|
||||
|
||||
if (config) {
|
||||
this._honcho['_client'].workspaces.peers.getOrCreate(
|
||||
this._honcho.workspaceId,
|
||||
{ id: this.id, configuration: config }
|
||||
)
|
||||
}
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the peer's representation with a natural language question.
|
||||
*
|
||||
* Makes an API call to the Honcho dialectic endpoint to query either the peer's
|
||||
* global representation (all content associated with this peer) or their local
|
||||
* representation of another peer (what this peer knows about the target peer).
|
||||
*
|
||||
* @param query - The natural language question to ask
|
||||
* @param stream - Whether to stream the response
|
||||
* @param target - Optional target peer for local representation query. If provided,
|
||||
* queries what this peer knows about the target peer rather than
|
||||
* querying the peer's global representation
|
||||
* @param sessionId - Optional session ID to scope the query to a specific session.
|
||||
* If provided, only information from that session is considered
|
||||
* @returns Promise resolving to response string containing the answer to the query,
|
||||
* or null if no relevant information is available
|
||||
*/
|
||||
async chat(
|
||||
query: string,
|
||||
opts?: {
|
||||
options?: {
|
||||
stream?: boolean
|
||||
target?: string | Peer
|
||||
sessionId?: string
|
||||
}
|
||||
): Promise<string | null> {
|
||||
const response = await this._honcho['_client'].workspaces.peers.chat(
|
||||
this._honcho.workspaceId,
|
||||
const chatParams = ChatQuerySchema.parse({
|
||||
query,
|
||||
stream: options?.stream,
|
||||
target: options?.target,
|
||||
sessionId: options?.sessionId,
|
||||
})
|
||||
const response = await this._client.workspaces.peers.chat(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
query,
|
||||
stream: opts?.stream,
|
||||
target: opts?.target
|
||||
? typeof opts.target === 'string'
|
||||
? opts.target
|
||||
: opts.target.id
|
||||
query: chatParams.query,
|
||||
stream: chatParams.stream,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: opts?.sessionId,
|
||||
session_id: chatParams.sessionId,
|
||||
}
|
||||
)
|
||||
if (!response.content || response.content === 'None') {
|
||||
|
|
@ -60,38 +100,68 @@ export class Peer {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param filters - Optional filter criteria for sessions. See [search filters documentation](https://docs.honcho.dev/v2/guides/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 getSessions(
|
||||
filter?: { [key: string]: unknown } | null
|
||||
): Promise<Page<Session>> {
|
||||
const sessionsPage = await this._honcho[
|
||||
'_client'
|
||||
].workspaces.peers.sessions.list(this._honcho.workspaceId, this.id, {
|
||||
filter,
|
||||
})
|
||||
async getSessions(filters?: Filters | null): Promise<Page<Session>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const sessionsPage = await this._client.workspaces.peers.sessions.list(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
filters: validatedFilter,
|
||||
}
|
||||
)
|
||||
return new Page(
|
||||
sessionsPage,
|
||||
(session: any) => new Session(session.id, this._honcho)
|
||||
(session) => new Session(session.id, this.workspaceId, this._client)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message attributed to this peer.
|
||||
* Create a message object attributed to this peer.
|
||||
*
|
||||
* This is a convenience method for creating message objects with this peer's ID.
|
||||
* The created message object can then be added to sessions or used in other operations.
|
||||
*
|
||||
* @param content - The text content for the message
|
||||
* @param metadata - Optional metadata to associate with the message
|
||||
* @returns A new message object with this peer's ID and the provided content
|
||||
*/
|
||||
message(content: string, opts?: { metadata?: Record<string, unknown> }): any {
|
||||
message(
|
||||
content: string,
|
||||
options?: { metadata?: Record<string, unknown> }
|
||||
): ValidatedMessageCreate {
|
||||
const validatedContent = MessageContentSchema.parse(content)
|
||||
const validatedMetadata = options?.metadata
|
||||
? MessageMetadataSchema.parse(options.metadata)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
peerId: this.id,
|
||||
content,
|
||||
metadata: opts?.metadata,
|
||||
peer_id: this.id,
|
||||
content: validatedContent,
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current metadata for this peer.
|
||||
*
|
||||
* Makes an API call to retrieve metadata associated with this peer. Metadata
|
||||
* can include custom attributes, settings, or any other key-value data
|
||||
* associated with the peer.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
*/
|
||||
async getMetadata(): Promise<Record<string, unknown>> {
|
||||
const peer = await this._honcho['_client'].workspaces.peers.getOrCreate(
|
||||
this._honcho.workspaceId,
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.metadata || {}
|
||||
|
|
@ -99,13 +169,50 @@ export class Peer {
|
|||
|
||||
/**
|
||||
* Set the metadata for this peer.
|
||||
*
|
||||
* Makes an API call to update the metadata associated with this peer.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this peer.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
|
||||
await this._honcho['_client'].workspaces.peers.update(
|
||||
this._honcho.workspaceId,
|
||||
this.id,
|
||||
{ metadata }
|
||||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current workspace-level configuration for this peer.
|
||||
*
|
||||
* Makes an API call to retrieve configuration associated with this peer.
|
||||
* Configuration currently includes one optional flag, `observe_me`.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's configuration
|
||||
*/
|
||||
async getPeerConfig(): Promise<Record<string, unknown>> {
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the configuration for this peer. Currently the only supported config
|
||||
* value is the `observe_me` flag, which controls whether derivation tasks
|
||||
* should be created for this peer's global representation. Default is True.
|
||||
*
|
||||
* Makes an API call to update the configuration associated with this peer.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
*
|
||||
* @param config - A dictionary of configuration to associate with this peer.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setPeerConfig(config: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
configuration: config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,54 +221,39 @@ export class Peer {
|
|||
* Makes an API call to search endpoint.
|
||||
*
|
||||
* @param query The search query to use
|
||||
* @returns A Page of Message objects representing the search results.
|
||||
* Returns an empty page if no messages are found.
|
||||
* @param filters - Optional filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @param limit - Optional limit on the number of results to return.
|
||||
* @returns Promise resolving to an array of Message objects representing the search results.
|
||||
* Returns an empty array if no messages are found.
|
||||
*/
|
||||
async search(query: string): Promise<Page<any>> {
|
||||
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
||||
throw new Error('Search query must be a non-empty string')
|
||||
}
|
||||
const messagesPage = await this._honcho['_client'].workspaces.peers.search(
|
||||
this._honcho.workspaceId,
|
||||
async search(
|
||||
query: string,
|
||||
options?: { filters?: Filters; limit?: number }
|
||||
): Promise<Message[]> {
|
||||
const validatedQuery = SearchQuerySchema.parse(query)
|
||||
const validatedFilters = options?.filters
|
||||
? FilterSchema.parse(options.filters)
|
||||
: undefined
|
||||
const validatedLimit = options?.limit
|
||||
? LimitSchema.parse(options.limit)
|
||||
: undefined
|
||||
return await this._client.workspaces.peers.search(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{ query: query }
|
||||
{
|
||||
query: validatedQuery,
|
||||
filters: validatedFilters,
|
||||
limit: validatedLimit,
|
||||
}
|
||||
)
|
||||
return new Page(messagesPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to create messages in this peer's global representation.
|
||||
* Return a string representation of the Peer.
|
||||
*
|
||||
* Makes an API call to upload a file and convert it into messages. The file is
|
||||
* processed to extract text content, split into appropriately sized chunks,
|
||||
* and created as messages attributed to this peer.
|
||||
*
|
||||
* @param file File to upload. Should be an object with filename, content (as Buffer or Uint8Array), and content_type
|
||||
* @returns A list of Message objects representing the created messages
|
||||
*
|
||||
* @note Supported file types include PDFs, text files, and JSON documents.
|
||||
* Large files will be automatically split into multiple messages to fit
|
||||
* within message size limits.
|
||||
* @returns A string representation suitable for debugging
|
||||
*/
|
||||
async uploadFile(file: {
|
||||
filename: string
|
||||
content: Buffer | Uint8Array
|
||||
content_type: string
|
||||
}): Promise<any[]> {
|
||||
// Convert file to the format expected by the API
|
||||
const fileData = {
|
||||
filename: file.filename,
|
||||
content: file.content,
|
||||
content_type: file.content_type,
|
||||
}
|
||||
|
||||
// Call the upload endpoint
|
||||
const response = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.peers.messages.upload(this._honcho.workspaceId, this.id, {
|
||||
file: fileData,
|
||||
})
|
||||
|
||||
return response
|
||||
toString(): string {
|
||||
return `Peer(id='${this.id}')`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,186 @@
|
|||
import type { Honcho } from './client'
|
||||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Uploadable } from '@honcho-ai/core/src/uploads'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import { SessionContext } from './session_context'
|
||||
import {
|
||||
ContextParamsSchema,
|
||||
FileUploadSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
LimitSchema,
|
||||
type MessageAddition,
|
||||
MessageAdditionSchema,
|
||||
type PeerAddition,
|
||||
PeerAdditionSchema,
|
||||
type PeerRemoval,
|
||||
PeerRemovalSchema,
|
||||
SearchQuerySchema,
|
||||
SessionPeerConfigSchema,
|
||||
WorkingRepParamsSchema,
|
||||
} from './validation'
|
||||
|
||||
/**
|
||||
* Configuration options for a peer within a specific session.
|
||||
*
|
||||
* Controls how peers interact and observe each other within the context
|
||||
* of a particular session, allowing for fine-grained control over
|
||||
* representation building and theory-of-mind behaviors.
|
||||
*/
|
||||
export class SessionPeerConfig {
|
||||
observe_others: boolean
|
||||
observe_me: boolean
|
||||
/**
|
||||
* Whether other peers in this session should try to form a session-level
|
||||
* theory-of-mind representation of this peer. When false, prevents other
|
||||
* peers from building local representations of this peer within this session.
|
||||
*/
|
||||
observe_me?: boolean | null
|
||||
|
||||
constructor(opts?: { observe_others?: boolean; observe_me?: boolean }) {
|
||||
this.observe_others = opts?.observe_others ?? false
|
||||
this.observe_me = opts?.observe_me ?? true
|
||||
/**
|
||||
* Whether this peer should form session-level theory-of-mind representations
|
||||
* of other peers in the session. When false, this peer will not build local
|
||||
* representations of other peers within this session.
|
||||
*/
|
||||
observe_others?: boolean
|
||||
|
||||
/**
|
||||
* Initialize SessionPeerConfig with observation settings.
|
||||
*
|
||||
* @param observe_me - Whether other peers should observe this peer in the session
|
||||
* @param observe_others - Whether this peer should observe others in the session
|
||||
*/
|
||||
constructor(observe_me?: boolean | null, observe_others?: boolean) {
|
||||
const validatedConfig = SessionPeerConfigSchema.parse({
|
||||
observe_me,
|
||||
observe_others,
|
||||
})
|
||||
this.observe_me = validatedConfig.observe_me
|
||||
this.observe_others = validatedConfig.observe_others
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a session in Honcho.
|
||||
* Represents a session in the Honcho system.
|
||||
*
|
||||
* Sessions are scoped to a set of peers and contain messages/content. They create
|
||||
* bidirectional relationships between peers and provide a context for multi-party
|
||||
* conversations and interactions. Sessions serve as containers for conversations,
|
||||
* allowing peers to communicate while maintaining both global and local
|
||||
* representations of each other.
|
||||
*
|
||||
* Key features:
|
||||
* - Multi-peer conversations with configurable observation settings
|
||||
* - Message storage and retrieval with filtering capabilities
|
||||
* - Context optimization for token-limited scenarios
|
||||
* - File upload support with automatic message creation
|
||||
* - Session-scoped peer representations and theory-of-mind modeling
|
||||
* - Search functionality across session messages
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const session = await honcho.session('conversation-123')
|
||||
*
|
||||
* // Add peers to the session
|
||||
* await session.addPeers(['user1', 'assistant1'])
|
||||
*
|
||||
* // Send messages
|
||||
* await session.addMessages([
|
||||
* { peer_id: 'user1', content: 'Hello!' },
|
||||
* { peer_id: 'assistant1', content: 'Hi there!' }
|
||||
* ])
|
||||
*
|
||||
* // Get optimized context
|
||||
* const context = await session.getContext(true, 4000)
|
||||
* ```
|
||||
*/
|
||||
export class Session {
|
||||
/**
|
||||
* Unique identifier for this session.
|
||||
*/
|
||||
readonly id: string
|
||||
private _honcho: Honcho
|
||||
/**
|
||||
* Workspace ID for scoping operations.
|
||||
*/
|
||||
readonly workspaceId: string
|
||||
/**
|
||||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
|
||||
/**
|
||||
* Initialize a new Session.
|
||||
* Initialize a new Session. **Do not call this directly, use the client.session() method instead.**
|
||||
*
|
||||
* @param id - Unique identifier for this session within the workspace
|
||||
* @param workspaceId - Workspace ID for scoping operations
|
||||
* @param client - Reference to the parent Honcho client instance
|
||||
*/
|
||||
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
this.id = id
|
||||
this._honcho = honcho
|
||||
|
||||
if (config) {
|
||||
this._honcho['_client'].workspaces.sessions.getOrCreate(
|
||||
this._honcho.workspaceId,
|
||||
{ id: this.id, configuration: config }
|
||||
)
|
||||
}
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
}
|
||||
|
||||
/**
|
||||
* Add peers to this session.
|
||||
*
|
||||
* Makes an API call to add one or more peers to this session. Adding peers
|
||||
* creates bidirectional relationships and allows them to participate in
|
||||
* the session's conversations. Peers can be added with optional session-specific
|
||||
* configuration to control observation behaviors.
|
||||
*
|
||||
* @param peers - Peers to add to the session. Can be:
|
||||
* - string: Single peer ID
|
||||
* - Peer: Single Peer object
|
||||
* - Array<string | Peer>: List of peer IDs and/or Peer objects
|
||||
* - [string | Peer, SessionPeerConfig]: Single peer with session config
|
||||
* - Array<string | Peer | [string | Peer, SessionPeerConfig]>: Mixed list
|
||||
* of peers and peer+config combinations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add single peer
|
||||
* await session.addPeers('user123')
|
||||
*
|
||||
* // Add multiple peers
|
||||
* await session.addPeers(['user1', 'user2', peer3])
|
||||
*
|
||||
* // Add peer with custom config
|
||||
* await session.addPeers(['user1', new SessionPeerConfig(false, true)])
|
||||
*
|
||||
* // Add mixed peers with and without configs
|
||||
* await session.addPeers([
|
||||
* 'user1',
|
||||
* ['user2', new SessionPeerConfig(true, false)],
|
||||
* peer3
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
async addPeers(
|
||||
peers:
|
||||
| string
|
||||
| Peer
|
||||
| Array<string | Peer>
|
||||
| [string | Peer, SessionPeerConfig]
|
||||
| Array<[string | Peer, SessionPeerConfig]>
|
||||
| Array<string | Peer | [string | Peer, SessionPeerConfig]>
|
||||
): Promise<void> {
|
||||
async addPeers(peers: PeerAddition): Promise<void> {
|
||||
const validatedPeers = PeerAdditionSchema.parse(peers)
|
||||
const peerDict: Record<string, SessionPeerConfig> = {}
|
||||
const peersArray = Array.isArray(peers) ? peers : [peers]
|
||||
const peersArray = Array.isArray(validatedPeers)
|
||||
? validatedPeers
|
||||
: [validatedPeers]
|
||||
|
||||
for (const peer of peersArray) {
|
||||
if (typeof peer === 'string') {
|
||||
peerDict[peer] = { observe_others: false, observe_me: true }
|
||||
} else if (typeof peer === 'object' && 'id' in peer) {
|
||||
peerDict[peer.id] = { observe_others: false, observe_me: true }
|
||||
// Handle string peer ID
|
||||
peerDict[peer] = {}
|
||||
} else if (Array.isArray(peer)) {
|
||||
// Handle tuple [string | Peer, SessionPeerConfig]
|
||||
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id
|
||||
peerDict[peerId] = peer[1]
|
||||
} else if (
|
||||
typeof peer === 'object' &&
|
||||
'id' in peer &&
|
||||
'observe_others' in peer &&
|
||||
'observe_me' in peer
|
||||
) {
|
||||
peerDict[(peer as any).id] = {
|
||||
observe_others: (peer as any).observe_others,
|
||||
observe_me: (peer as any).observe_me,
|
||||
}
|
||||
} else if (typeof peer === 'object' && 'id' in peer) {
|
||||
// Handle Peer object
|
||||
peerDict[peer.id] = {}
|
||||
} else {
|
||||
// This should never happen with proper typing, but handle gracefully
|
||||
throw new Error(`Invalid peer type: ${typeof peer}`)
|
||||
}
|
||||
}
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.peers.add(
|
||||
this._honcho.workspaceId,
|
||||
|
||||
await this._client.workspaces.sessions.peers.add(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
peerDict
|
||||
)
|
||||
|
|
@ -81,40 +188,45 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Set the complete peer list for this session.
|
||||
*
|
||||
* Makes an API call to replace the current peer list with the provided peers.
|
||||
* This will remove any peers not in the new list and add any that are missing.
|
||||
* Unlike addPeers(), this method overwrites the entire peer membership.
|
||||
*
|
||||
* @param peers - Peers to set for the session. Can be:
|
||||
* - string: Single peer ID
|
||||
* - Peer: Single Peer object
|
||||
* - Array<string | Peer>: List of peer IDs and/or Peer objects
|
||||
* - [string | Peer, SessionPeerConfig]: Single peer with session config
|
||||
* - Array<string | Peer | [string | Peer, SessionPeerConfig]>: Mixed list
|
||||
* of peers and peer+config combinations
|
||||
*/
|
||||
async setPeers(
|
||||
peers:
|
||||
| string
|
||||
| Peer
|
||||
| Array<string | Peer>
|
||||
| [string | Peer, SessionPeerConfig]
|
||||
| Array<[string | Peer, SessionPeerConfig]>
|
||||
| Array<string | Peer | [string | Peer, SessionPeerConfig]>
|
||||
): Promise<void> {
|
||||
async setPeers(peers: PeerAddition): Promise<void> {
|
||||
const validatedPeers = PeerAdditionSchema.parse(peers)
|
||||
const peerDict: Record<string, SessionPeerConfig> = {}
|
||||
const peersArray = Array.isArray(peers) ? peers : [peers]
|
||||
const peersArray = Array.isArray(validatedPeers)
|
||||
? validatedPeers
|
||||
: [validatedPeers]
|
||||
|
||||
for (const peer of peersArray) {
|
||||
if (typeof peer === 'string') {
|
||||
peerDict[peer] = { observe_others: false, observe_me: true }
|
||||
} else if (typeof peer === 'object' && 'id' in peer) {
|
||||
peerDict[peer.id] = { observe_others: false, observe_me: true }
|
||||
// Handle string peer ID
|
||||
peerDict[peer] = {}
|
||||
} else if (Array.isArray(peer)) {
|
||||
// Handle tuple [string | Peer, SessionPeerConfig]
|
||||
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id
|
||||
peerDict[peerId] = peer[1]
|
||||
} else if (
|
||||
typeof peer === 'object' &&
|
||||
'id' in peer &&
|
||||
'observe_others' in peer &&
|
||||
'observe_me' in peer
|
||||
) {
|
||||
peerDict[(peer as any).id] = {
|
||||
observe_others: (peer as any).observe_others,
|
||||
observe_me: (peer as any).observe_me,
|
||||
}
|
||||
} else if (typeof peer === 'object' && 'id' in peer) {
|
||||
// Handle Peer object
|
||||
peerDict[peer.id] = {}
|
||||
} else {
|
||||
// This should never happen with proper typing, but handle gracefully
|
||||
throw new Error(`Invalid peer type: ${typeof peer}`)
|
||||
}
|
||||
}
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.peers.set(
|
||||
this._honcho.workspaceId,
|
||||
|
||||
await this._client.workspaces.sessions.peers.set(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
peerDict
|
||||
)
|
||||
|
|
@ -122,40 +234,65 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Remove peers from this session.
|
||||
*
|
||||
* Makes an API call to remove one or more peers from this session.
|
||||
* Removed peers will no longer be able to participate in the session
|
||||
* unless added back. Their existing messages remain in the session.
|
||||
*
|
||||
* @param peers - Peers to remove from the session. Can be:
|
||||
* - string: Single peer ID
|
||||
* - Peer: Single Peer object
|
||||
* - Array<string | Peer>: List of peer IDs and/or Peer objects
|
||||
*/
|
||||
async removePeers(
|
||||
peers: string | Peer | Array<string | Peer>
|
||||
): Promise<void> {
|
||||
const peerIds = Array.isArray(peers)
|
||||
? peers.map((p) => (typeof p === 'string' ? p : p.id))
|
||||
: [typeof peers === 'string' ? peers : peers.id]
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.peers.remove(
|
||||
this._honcho.workspaceId,
|
||||
async removePeers(peers: PeerRemoval): Promise<void> {
|
||||
const validatedPeers = PeerRemovalSchema.parse(peers)
|
||||
const peerIds = Array.isArray(validatedPeers)
|
||||
? validatedPeers.map((p) => (typeof p === 'string' ? p : p.id))
|
||||
: [
|
||||
typeof validatedPeers === 'string'
|
||||
? validatedPeers
|
||||
: validatedPeers.id,
|
||||
]
|
||||
await this._client.workspaces.sessions.peers.remove(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
peerIds
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all peers in this session. Automatically converts the paginated response
|
||||
* into a list for us -- the max number of peers in a session is usually 10.
|
||||
* Get all peers in this session.
|
||||
*
|
||||
* Makes an API call to retrieve the list of peers that are currently
|
||||
* members of this session. Automatically converts the paginated response
|
||||
* into a list for convenience -- the max number of peers in a session is usually 10.
|
||||
*
|
||||
* @returns Promise resolving to a list of Peer objects that are members of this session
|
||||
*/
|
||||
async getPeers(): Promise<Peer[]> {
|
||||
const peersPage = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.peers.list(this._honcho.workspaceId, this.id)
|
||||
return peersPage.items.map((peer: any) => new Peer(peer.id, this._honcho))
|
||||
const peersPage = await this._client.workspaces.sessions.peers.list(
|
||||
this.workspaceId,
|
||||
this.id
|
||||
)
|
||||
return peersPage.items.map(
|
||||
(peer) => new Peer(peer.id, this.workspaceId, this._client)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for a peer in this session.
|
||||
*
|
||||
* Makes an API call to retrieve the session-specific configuration for a peer.
|
||||
* This includes observation settings that control how this peer interacts
|
||||
* with other peers within this session context.
|
||||
*
|
||||
* @param peer - The peer to get configuration for. Can be peer ID string or Peer object
|
||||
* @returns Promise resolving to SessionPeerConfig object with the peer's session settings
|
||||
*/
|
||||
async getPeerConfig(peer: string | Peer): Promise<SessionPeerConfig> {
|
||||
const peerId = typeof peer === 'string' ? peer : peer.id
|
||||
return await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.peers.getConfig(
|
||||
this._honcho.workspaceId,
|
||||
return await this._client.workspaces.sessions.peers.getConfig(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
peerId
|
||||
)
|
||||
|
|
@ -163,92 +300,164 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Set the configuration for a peer in this session.
|
||||
*
|
||||
* Makes an API call to update the session-specific configuration for a peer.
|
||||
* This controls observation behaviors and theory-of-mind formation within
|
||||
* this session context.
|
||||
*
|
||||
* @param peer - The peer to configure. Can be peer ID string or Peer object
|
||||
* @param config - SessionPeerConfig object specifying the observation settings
|
||||
*/
|
||||
async setPeerConfig(
|
||||
peer: string | Peer,
|
||||
config: SessionPeerConfig
|
||||
): Promise<void> {
|
||||
const peerId = typeof peer === 'string' ? peer : peer.id
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.peers.setConfig(
|
||||
this._honcho.workspaceId,
|
||||
const validatedConfig = SessionPeerConfigSchema.parse(config)
|
||||
await this._client.workspaces.sessions.peers.setConfig(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
peerId,
|
||||
{
|
||||
observe_others: config.observe_others,
|
||||
observe_me: config.observe_me,
|
||||
observe_others: validatedConfig.observe_others,
|
||||
observe_me: validatedConfig.observe_me,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more messages to this session.
|
||||
*
|
||||
* Makes an API call to store messages in this session. Any message added
|
||||
* to a session will automatically add the creating peer to the session
|
||||
* if they are not already a member. Messages are the primary way content
|
||||
* flows through the Honcho system.
|
||||
*
|
||||
* @param messages - Messages to add to the session. Can be:
|
||||
* - MessageCreate: Single message object with peer_id and content
|
||||
* - MessageCreate[]: Array of message objects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add single message
|
||||
* await session.addMessages({
|
||||
* peer_id: 'user123',
|
||||
* content: 'Hello world!'
|
||||
* })
|
||||
*
|
||||
* // Add multiple messages
|
||||
* await session.addMessages([
|
||||
* { peer_id: 'user1', content: 'Hello!' },
|
||||
* { peer_id: 'assistant', content: 'Hi there!' }
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
async addMessages(messages: any | any[]): Promise<void> {
|
||||
const msgs = Array.isArray(messages) ? messages : [messages]
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.messages.create(
|
||||
this._honcho.workspaceId,
|
||||
async addMessages(messages: MessageAddition): Promise<void> {
|
||||
const validatedMessages = MessageAdditionSchema.parse(messages)
|
||||
const messagesList = Array.isArray(validatedMessages)
|
||||
? validatedMessages
|
||||
: [validatedMessages]
|
||||
await this._client.workspaces.sessions.messages.create(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
messages: msgs.map((msg) => ({
|
||||
peer_id: msg.peerId,
|
||||
content: msg.content,
|
||||
metadata: msg.metadata,
|
||||
})),
|
||||
messages: messagesList,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 and are returned in a paginated format.
|
||||
* Messages are ordered by creation time (most recent first by default).
|
||||
*
|
||||
* @param filters - Optional filter criteria for messages. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @returns Promise resolving to a Page of Message objects matching the specified criteria
|
||||
*/
|
||||
async getMessages(opts?: {
|
||||
filter?: Record<string, unknown>
|
||||
}): Promise<Page<any>> {
|
||||
const messagesPage = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.messages.list(
|
||||
this._honcho.workspaceId,
|
||||
async getMessages(filters?: Filters): Promise<Page<Message>> {
|
||||
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
|
||||
const messagesPage = await this._client.workspaces.sessions.messages.list(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
opts?.filter
|
||||
validatedFilter
|
||||
)
|
||||
return new Page(messagesPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for this session.
|
||||
*
|
||||
* Makes an API call to retrieve the current metadata associated with this session.
|
||||
* Metadata can include custom attributes, settings, or any other key-value data
|
||||
* that provides context about the session.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the session's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
*/
|
||||
async getMetadata(): Promise<Record<string, unknown>> {
|
||||
const session = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.getOrCreate(this._honcho.workspaceId, { id: this.id })
|
||||
const session = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return session.metadata || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metadata for this session.
|
||||
*
|
||||
* Makes an API call to update the metadata associated with this session.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* Metadata is useful for storing custom attributes, configuration, or
|
||||
* contextual information about the session.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this session.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
|
||||
await (this._honcho['_client'] as any).workspaces.sessions.update(
|
||||
this._honcho.workspaceId,
|
||||
this.id,
|
||||
{ metadata }
|
||||
)
|
||||
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized context for this session within a token limit.
|
||||
*
|
||||
* Makes an API call to retrieve a curated list of messages that provides
|
||||
* optimal context for the conversation while staying within the specified
|
||||
* token limit. Uses tiktoken for token counting, so results should be
|
||||
* compatible with OpenAI models. The context optimization balances
|
||||
* recency and relevance to provide the best conversational context.
|
||||
*
|
||||
* @param summary - Whether to include summary information in the context.
|
||||
* When true, includes session summary if available. Defaults to true
|
||||
* @param tokens - Maximum number of tokens to include in the context. If not provided,
|
||||
* uses the server's default configuration
|
||||
* @returns Promise resolving to a SessionContext object containing the optimized
|
||||
* message history and summary (if available) that maximizes conversational
|
||||
* context while respecting the token limit
|
||||
*
|
||||
* @note Token counting is performed using tiktoken. For models using different
|
||||
* tokenizers, you may need to adjust the token limit accordingly.
|
||||
*/
|
||||
async getContext(opts?: {
|
||||
async getContext(options?: {
|
||||
summary?: boolean
|
||||
tokens?: number
|
||||
}): Promise<SessionContext> {
|
||||
const context = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.getContext(this._honcho.workspaceId, this.id, {
|
||||
tokens: opts?.tokens,
|
||||
summary: opts?.summary,
|
||||
const contextParams = ContextParamsSchema.parse({
|
||||
summary: options?.summary,
|
||||
tokens: options?.tokens,
|
||||
})
|
||||
return new SessionContext(this.id, context.messages, context.summary || '')
|
||||
const context = await this._client.workspaces.sessions.getContext(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
tokens: contextParams.tokens,
|
||||
summary: contextParams.summary,
|
||||
}
|
||||
)
|
||||
return new SessionContext(this.id, context.messages, context.summary)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -257,19 +466,34 @@ export class Session {
|
|||
* Makes an API call to search for messages in this session.
|
||||
*
|
||||
* @param query The search query to use
|
||||
* @returns A Page of Message objects representing the search results.
|
||||
* Returns an empty page if no messages are found.
|
||||
* @param filters - Optional filters to scope the search: see [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @param limit Number of results to return (1-100, default: 10).
|
||||
* @returns A list of Message objects representing the search results.
|
||||
* Returns an empty list if no messages are found.
|
||||
*/
|
||||
async search(query: string): Promise<Page<any>> {
|
||||
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
||||
throw new Error('Search query must be a non-empty string')
|
||||
async search(
|
||||
query: string,
|
||||
options?: {
|
||||
filters?: Filters
|
||||
limit?: number
|
||||
}
|
||||
const messagesPage = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.search(this._honcho.workspaceId, this.id, {
|
||||
query: query,
|
||||
})
|
||||
return new Page(messagesPage)
|
||||
): Promise<Message[]> {
|
||||
const validatedQuery = SearchQuerySchema.parse(query)
|
||||
const validatedFilters = options?.filters
|
||||
? FilterSchema.parse(options.filters)
|
||||
: undefined
|
||||
const validatedLimit = options?.limit
|
||||
? LimitSchema.parse(options.limit)
|
||||
: undefined
|
||||
return await this._client.workspaces.sessions.search(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
query: validatedQuery,
|
||||
filters: validatedFilters,
|
||||
limit: validatedLimit,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -277,65 +501,94 @@ export class Session {
|
|||
*
|
||||
* Makes an API call to upload a file and convert it into messages. The file is
|
||||
* processed to extract text content, split into appropriately sized chunks,
|
||||
* and created as messages attributed to this peer.
|
||||
* and created as messages attributed to the specified peer. The peer will be
|
||||
* automatically added to the session if not already a member.
|
||||
*
|
||||
* @param file File to upload. Should be an object with filename, content (as Buffer or Uint8Array), and content_type
|
||||
* @param peerId The peer ID to attribute the messages to
|
||||
* @returns A list of Message objects representing the created messages
|
||||
* @param file - File to upload. Can be:
|
||||
* - File objects (browser File API)
|
||||
* - Buffer or Uint8Array with filename and content_type
|
||||
* - { filename: string, content: Buffer | Uint8Array, content_type: string }
|
||||
* @param peerId - The peer ID to attribute the created messages to
|
||||
* @returns Promise resolving to a list of Message objects representing the created messages
|
||||
*
|
||||
* @note Supported file types include PDFs, text files, and JSON documents.
|
||||
* Large files will be automatically split into multiple messages to fit
|
||||
* within message size limits.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Upload a file
|
||||
* const messages = await session.uploadFile(fileInput.files[0], 'user123')
|
||||
* console.log(`Created ${messages.length} messages from file`)
|
||||
* ```
|
||||
*/
|
||||
async uploadFile(
|
||||
file: {
|
||||
filename: string
|
||||
content: Buffer | Uint8Array
|
||||
content_type: string
|
||||
},
|
||||
peerId: string
|
||||
): Promise<any[]> {
|
||||
// Convert file to the format expected by the API
|
||||
const fileData = {
|
||||
filename: file.filename,
|
||||
content: file.content,
|
||||
content_type: file.content_type,
|
||||
}
|
||||
|
||||
// Call the upload endpoint
|
||||
const response = await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.sessions.messages.upload(this._honcho.workspaceId, this.id, {
|
||||
file: fileData,
|
||||
peer_id: peerId,
|
||||
})
|
||||
async uploadFile(file: Uploadable, peerId: string): Promise<Message[]> {
|
||||
const uploadParams = FileUploadSchema.parse({ file, peerId })
|
||||
const response = await this._client.workspaces.sessions.messages.upload(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
file: uploadParams.file,
|
||||
peer_id: uploadParams.peerId,
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current working representation of the peer in this session.
|
||||
* Get the current working representation of a peer in this session.
|
||||
*
|
||||
* @param peer The peer to get the working representation of.
|
||||
* @param target The target peer to get the representation of. If provided, queries what `peer` knows about the `target`.
|
||||
* @returns A dictionary containing information about the peer.
|
||||
* Makes an API call to retrieve the session-scoped representation that has been
|
||||
* built for a peer. This can be either the peer's global representation or
|
||||
* their local representation of another peer (theory-of-mind).
|
||||
*
|
||||
* @param peer - The peer to get the working representation of. Can be peer ID string or Peer object
|
||||
* @param target - Optional target peer. If provided, returns what `peer` knows about
|
||||
* `target` within this session context rather than `peer`'s global representation
|
||||
* @returns Promise resolving to a dictionary containing the peer's representation information,
|
||||
* including facts, characteristics, and contextual knowledge
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get peer's global representation in this session
|
||||
* const globalRep = await session.workingRep('user123')
|
||||
*
|
||||
* // Get what user123 knows about assistant in this session
|
||||
* const localRep = await session.workingRep('user123', 'assistant')
|
||||
* ```
|
||||
*/
|
||||
async workingRep(
|
||||
peer: string | Peer,
|
||||
target?: string | Peer
|
||||
): Promise<Record<string, unknown>> {
|
||||
const peerId = typeof peer === 'string' ? peer : peer.id
|
||||
const targetId = target
|
||||
? typeof target === 'string'
|
||||
? target
|
||||
: target.id
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({ peer, target })
|
||||
const peerId =
|
||||
typeof workingRepParams.peer === 'string'
|
||||
? workingRepParams.peer
|
||||
: workingRepParams.peer.id
|
||||
const targetId = workingRepParams.target
|
||||
? typeof workingRepParams.target === 'string'
|
||||
? workingRepParams.target
|
||||
: workingRepParams.target.id
|
||||
: undefined
|
||||
|
||||
return await (
|
||||
this._honcho['_client'] as any
|
||||
).workspaces.peers.workingRepresentation(this._honcho.workspaceId, peerId, {
|
||||
session_id: this.id,
|
||||
target: targetId,
|
||||
})
|
||||
return await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
peerId,
|
||||
{
|
||||
session_id: this.id,
|
||||
target: targetId,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Session.
|
||||
*
|
||||
* @returns A string representation suitable for debugging
|
||||
*/
|
||||
toString(): string {
|
||||
return `Session(id='${this.id}')`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
|
||||
import type { Peer } from './peer'
|
||||
|
||||
/**
|
||||
|
|
@ -16,7 +17,7 @@ export class SessionContext {
|
|||
/**
|
||||
* List of Message objects representing the conversation context.
|
||||
*/
|
||||
readonly messages: any[]
|
||||
readonly messages: Message[]
|
||||
|
||||
/**
|
||||
* Summary of the session history prior to the message cutoff.
|
||||
|
|
@ -30,7 +31,7 @@ export class SessionContext {
|
|||
* @param messages List of Message objects to include in the context
|
||||
* @param summary Summary of the session history prior to the message cutoff
|
||||
*/
|
||||
constructor(sessionId: string, messages: any[], summary: string = '') {
|
||||
constructor(sessionId: string, messages: Message[], summary: string = '') {
|
||||
this.sessionId = sessionId
|
||||
this.messages = messages
|
||||
this.summary = summary || ''
|
||||
|
|
@ -39,8 +40,8 @@ export class SessionContext {
|
|||
/**
|
||||
* Convert the context to OpenAI-compatible message format.
|
||||
*
|
||||
* Transforms the message history into the format expected by OpenAI's
|
||||
* Chat Completions API, with proper role assignments based on the
|
||||
* Transforms the message history and summary into the format expected by
|
||||
* OpenAI's Chat Completions API, with proper role assignments based on the
|
||||
* assistant's identity.
|
||||
*
|
||||
* @param assistant The assistant peer (Peer object or peer ID string) to use
|
||||
|
|
@ -49,47 +50,71 @@ export class SessionContext {
|
|||
* @returns A list of dictionaries in OpenAI format, where each dictionary contains
|
||||
* "role" and "content" keys suitable for the OpenAI API
|
||||
*/
|
||||
toOpenAI(assistant: string | Peer): Array<{ role: string; content: string }> {
|
||||
toOpenAI(
|
||||
assistant: string | Peer
|
||||
): Array<{ role: string; content: string; name?: string }> {
|
||||
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
|
||||
return this.messages.map((message) => ({
|
||||
role: message.peer_name === assistantId ? 'assistant' : 'user',
|
||||
const summaryMessage = {
|
||||
role: 'system',
|
||||
content: `<summary>${this.summary}</summary>`,
|
||||
}
|
||||
const messages = this.messages.map((message) => ({
|
||||
role: message.peer_id === assistantId ? 'assistant' : 'user',
|
||||
name: message.peer_id,
|
||||
content: message.content,
|
||||
}))
|
||||
return this.summary ? [summaryMessage, ...messages] : messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the context to Anthropic-compatible message format.
|
||||
*
|
||||
* Transforms the message history into the format expected by Anthropic's
|
||||
* Claude API.
|
||||
* Claude API, with proper role assignments based on the assistant's identity.
|
||||
*
|
||||
* @param assistant The assistant peer (Peer object or peer ID string) to use
|
||||
* for determining message roles. Messages from this peer will
|
||||
* be marked as "assistant", others as "user"
|
||||
* @returns A list of dictionaries in Anthropic format, where each dictionary contains
|
||||
* "role" and "content" keys suitable for the Anthropic API
|
||||
*
|
||||
* Note:
|
||||
* Future versions may implement role alternation requirements for
|
||||
* Anthropic's API compatibility
|
||||
*/
|
||||
toAnthropic(
|
||||
assistant: string | Peer
|
||||
): Array<{ role: string; content: string }> {
|
||||
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
|
||||
return this.messages.map((message) => ({
|
||||
role: message.peer_name === assistantId ? 'assistant' : 'user',
|
||||
content: message.content,
|
||||
}))
|
||||
const summaryMessage = {
|
||||
role: 'user',
|
||||
content: `<summary>${this.summary}</summary>`,
|
||||
}
|
||||
const messages = this.messages.map((message) =>
|
||||
message.peer_id === assistantId
|
||||
? {
|
||||
role: 'assistant',
|
||||
content: message.content,
|
||||
}
|
||||
: {
|
||||
role: 'user',
|
||||
content: `${message.peer_id}: ${message.content}`,
|
||||
}
|
||||
)
|
||||
return this.summary ? [summaryMessage, ...messages] : messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of messages in the context.
|
||||
*/
|
||||
get length(): number {
|
||||
return this.messages.length
|
||||
return this.messages.length + (this.summary.length > 0 ? 1 : 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the SessionContext.
|
||||
*/
|
||||
toString(): string {
|
||||
return `SessionContext(messages=${this.messages.length})`
|
||||
return `SessionContext(messages=${this.messages.length}, summary=${this.summary})`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Validation schemas for the Honcho TypeScript SDK.
|
||||
*
|
||||
* These schemas ensure type safety and runtime validation for all inputs
|
||||
* to the SDK, providing clear error messages when validation fails.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Schema for Honcho client configuration options.
|
||||
*/
|
||||
export const HonchoConfigSchema = z.object({
|
||||
apiKey: z.string().optional(),
|
||||
environment: z.enum(['local', 'production', 'demo']).optional(),
|
||||
baseURL: z.string().url('Base URL must be a valid URL').optional(),
|
||||
workspaceId: z
|
||||
.string()
|
||||
.min(1, 'Workspace ID must be a non-empty string')
|
||||
.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')
|
||||
.optional(),
|
||||
defaultHeaders: z.record(z.string(), z.string()).optional(),
|
||||
defaultQuery: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for peer metadata.
|
||||
*/
|
||||
export const PeerMetadataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for peer configuration.
|
||||
*/
|
||||
export const PeerConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for peer ID validation.
|
||||
*/
|
||||
export const PeerIdSchema = z
|
||||
.string()
|
||||
.min(1, 'Peer ID must be a non-empty string')
|
||||
|
||||
/**
|
||||
* Schema for session metadata.
|
||||
*/
|
||||
export const SessionMetadataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for session configuration.
|
||||
*/
|
||||
export const SessionConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for session ID validation.
|
||||
*/
|
||||
export const SessionIdSchema = z
|
||||
.string()
|
||||
.min(1, 'Session ID must be a non-empty string')
|
||||
|
||||
/**
|
||||
* Schema for session peer configuration.
|
||||
*/
|
||||
export const SessionPeerConfigSchema = z.object({
|
||||
observe_me: z.boolean().nullable().optional(),
|
||||
observe_others: z.boolean().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for message content.
|
||||
*/
|
||||
export const MessageContentSchema = z
|
||||
.string()
|
||||
.refine(
|
||||
(content: string) => content === '' || content.trim().length > 0,
|
||||
'Message content cannot be only whitespace'
|
||||
)
|
||||
|
||||
/**
|
||||
* Schema for message metadata.
|
||||
*/
|
||||
export const MessageMetadataSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message creation.
|
||||
*/
|
||||
export const MessageCreateSchema = z.object({
|
||||
peer_id: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for search query validation.
|
||||
*/
|
||||
export const SearchQuerySchema = z
|
||||
.string()
|
||||
.min(1, 'Search query must be a non-empty string')
|
||||
.refine(
|
||||
(query: string) => query.trim().length > 0,
|
||||
'Search query cannot be only whitespace'
|
||||
)
|
||||
|
||||
/**
|
||||
* Schema for filter objects.
|
||||
*/
|
||||
export const FilterSchema = z.record(z.string(), z.unknown()).optional()
|
||||
|
||||
/**
|
||||
* Schema for chat query parameters.
|
||||
*/
|
||||
export const ChatQuerySchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
stream: z.boolean().optional(),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sessionId: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for context retrieval parameters.
|
||||
*/
|
||||
export const ContextParamsSchema = z.object({
|
||||
summary: z.boolean().optional(),
|
||||
tokens: z
|
||||
.number()
|
||||
.positive('Token limit must be a positive number')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for deriver status options.
|
||||
*/
|
||||
export const DeriverStatusOptionsSchema = z.object({
|
||||
observerId: z.string().optional(),
|
||||
senderId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
timeoutMs: z
|
||||
.number()
|
||||
.positive('Timeout must be a positive number')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for file upload parameters.
|
||||
* Supports File objects (browser), Buffer, Uint8Array, and custom uploadable objects.
|
||||
*/
|
||||
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'
|
||||
),
|
||||
]),
|
||||
peerId: PeerIdSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for working representation parameters.
|
||||
*/
|
||||
export const WorkingRepParamsSchema = z.object({
|
||||
peer: z.union([z.string(), z.object({ id: z.string() })]),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for peer addition to session.
|
||||
*/
|
||||
export const PeerAdditionSchema = z.union([
|
||||
z.string(),
|
||||
z.object({ id: z.string() }),
|
||||
z.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({ id: z.string() }),
|
||||
z.tuple([
|
||||
z.union([z.string(), z.object({ id: z.string() })]),
|
||||
SessionPeerConfigSchema,
|
||||
]),
|
||||
])
|
||||
),
|
||||
z.tuple([
|
||||
z.union([z.string(), z.object({ id: z.string() })]),
|
||||
SessionPeerConfigSchema,
|
||||
]),
|
||||
])
|
||||
|
||||
/**
|
||||
* Schema for peer removal from session.
|
||||
*/
|
||||
export const PeerRemovalSchema = z.union([
|
||||
z.string(),
|
||||
z.object({ id: z.string() }),
|
||||
z.array(z.union([z.string(), z.object({ id: z.string() })])),
|
||||
])
|
||||
|
||||
/**
|
||||
* Schema for message addition to session.
|
||||
*/
|
||||
export const MessageAdditionSchema = z.union([
|
||||
MessageCreateSchema,
|
||||
z.array(MessageCreateSchema),
|
||||
])
|
||||
|
||||
/**
|
||||
* Schema for workspace metadata.
|
||||
*/
|
||||
export const WorkspaceMetadataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for limit.
|
||||
*/
|
||||
export const LimitSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'Limit must be a positive integer')
|
||||
.max(100, 'Limit must be less than or equal to 100')
|
||||
|
||||
/**
|
||||
* Type exports for use throughout the SDK.
|
||||
*/
|
||||
export type HonchoConfig = z.infer<typeof HonchoConfigSchema>
|
||||
export type PeerMetadata = z.infer<typeof PeerMetadataSchema>
|
||||
export type PeerConfig = z.infer<typeof PeerConfigSchema>
|
||||
export type SessionMetadata = z.infer<typeof SessionMetadataSchema>
|
||||
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
||||
export type SessionPeerConfig = z.infer<typeof SessionPeerConfigSchema>
|
||||
export type MessageCreate = z.infer<typeof MessageCreateSchema>
|
||||
export type Filters = z.infer<typeof FilterSchema>
|
||||
export type ChatQuery = z.infer<typeof ChatQuerySchema>
|
||||
export type ContextParams = z.infer<typeof ContextParamsSchema>
|
||||
export type DeriverStatusOptions = z.infer<typeof DeriverStatusOptionsSchema>
|
||||
export type FileUpload = z.infer<typeof FileUploadSchema>
|
||||
export type WorkingRepParams = z.infer<typeof WorkingRepParamsSchema>
|
||||
export type PeerAddition = z.infer<typeof PeerAdditionSchema>
|
||||
export type PeerRemoval = z.infer<typeof PeerRemovalSchema>
|
||||
export type MessageAddition = z.infer<typeof MessageAdditionSchema>
|
||||
export type WorkspaceMetadata = z.infer<typeof WorkspaceMetadataSchema>
|
||||
export type Limit = z.infer<typeof LimitSchema>
|
||||
|
|
@ -15,11 +15,11 @@
|
|||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"baseUrl": "."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class TomlConfigSettingsSource(PydanticBaseSettingsSource):
|
|||
"DERIVER": "deriver",
|
||||
"DIALECTIC": "dialectic",
|
||||
"SUMMARY": "summary",
|
||||
"WEBHOOK": "webhook",
|
||||
"": "app", # For AppSettings with no prefix
|
||||
}
|
||||
|
||||
|
|
@ -193,10 +194,6 @@ class DeriverSettings(HonchoSettings):
|
|||
# Thinking budget tokens are only applied when using Anthropic as provider
|
||||
THINKING_BUDGET_TOKENS: Annotated[int, Field(default=1024, gt=0, le=5000)] = 1024
|
||||
|
||||
# Default number of observations to retrieve for each reasoning level
|
||||
DEDUCTIVE_OBSERVATIONS_COUNT: Annotated[int, Field(default=6, gt=0, le=50)] = 6
|
||||
EXPLICIT_OBSERVATIONS_COUNT: Annotated[int, Field(default=10, gt=0, le=50)] = 10
|
||||
|
||||
|
||||
class DialecticSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DIALECTIC_", extra="ignore") # pyright: ignore
|
||||
|
|
@ -236,6 +233,13 @@ class SummarySettings(HonchoSettings):
|
|||
THINKING_BUDGET_TOKENS: Annotated[int, Field(default=512, gt=0, le=2000)] = 512
|
||||
|
||||
|
||||
class WebhookSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="WEBHOOK_", extra="ignore") # pyright: ignore
|
||||
|
||||
SECRET: str | None = None # Must be set if configuring webhooks
|
||||
MAX_WORKSPACE_LIMIT: int = 10
|
||||
|
||||
|
||||
class AppSettings(HonchoSettings):
|
||||
# No env_prefix for app-level settings
|
||||
model_config = SettingsConfigDict( # pyright: ignore
|
||||
|
|
@ -244,8 +248,6 @@ class AppSettings(HonchoSettings):
|
|||
|
||||
# Application-wide settings
|
||||
LOG_LEVEL: str = "INFO"
|
||||
FASTAPI_HOST: str = "0.0.0.0"
|
||||
FASTAPI_PORT: Annotated[int, Field(default=8000, gt=0, le=65_535)] = 8000
|
||||
SESSION_PEERS_LIMIT: Annotated[int, Field(default=10, gt=0)] = 10
|
||||
MAX_FILE_SIZE: Annotated[int, Field(default=5_242_880, gt=0)] = 5_242_880 # 5MB
|
||||
GET_CONTEXT_MAX_TOKENS: Annotated[int, Field(default=100_000, gt=0, le=250_000)] = (
|
||||
|
|
@ -268,6 +270,7 @@ class AppSettings(HonchoSettings):
|
|||
DERIVER: DeriverSettings = Field(default_factory=DeriverSettings)
|
||||
DIALECTIC: DialecticSettings = Field(default_factory=DialecticSettings)
|
||||
SUMMARY: SummarySettings = Field(default_factory=SummarySettings)
|
||||
WEBHOOK: WebhookSettings = Field(default_factory=WebhookSettings)
|
||||
|
||||
@field_validator("LOG_LEVEL")
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from .message import (
|
|||
get_message_seq_in_session,
|
||||
get_messages,
|
||||
get_messages_id_range,
|
||||
search,
|
||||
update_message,
|
||||
)
|
||||
from .peer import (
|
||||
|
|
@ -37,6 +36,11 @@ from .session import (
|
|||
set_peers_for_session,
|
||||
update_session,
|
||||
)
|
||||
from .webhook import (
|
||||
delete_webhook_endpoint,
|
||||
get_or_create_webhook_endpoint,
|
||||
list_webhook_endpoints,
|
||||
)
|
||||
from .workspace import get_all_workspaces, get_or_create_workspace, update_workspace
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -56,7 +60,6 @@ __all__ = [
|
|||
"get_message",
|
||||
"get_message_seq_in_session",
|
||||
"update_message",
|
||||
"search",
|
||||
# Peer
|
||||
"get_or_create_peers",
|
||||
"get_peer",
|
||||
|
|
@ -82,6 +85,10 @@ __all__ = [
|
|||
"set_peers_for_session",
|
||||
"get_peer_config",
|
||||
"set_peer_config",
|
||||
# Webhook
|
||||
"get_or_create_webhook_endpoint",
|
||||
"delete_webhook_endpoint",
|
||||
"list_webhook_endpoints",
|
||||
# Workspace
|
||||
"get_or_create_workspace",
|
||||
"get_all_workspaces",
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ def _build_queue_status_query(
|
|||
"""Build SQL query for queue status with validation and aggregation."""
|
||||
sender_name_expr = models.QueueItem.payload["sender_name"].astext
|
||||
target_name_expr = models.QueueItem.payload["target_name"].astext
|
||||
task_type_expr = models.QueueItem.payload["task_type"].astext
|
||||
|
||||
# Define conditions for cleaner window functions
|
||||
is_completed = models.QueueItem.processed
|
||||
|
|
@ -94,10 +93,7 @@ def _build_queue_status_query(
|
|||
|
||||
stmt = stmt.outerjoin(
|
||||
models.ActiveQueueSession,
|
||||
(models.QueueItem.session_id == models.ActiveQueueSession.session_id)
|
||||
& (sender_name_expr == models.ActiveQueueSession.sender_name)
|
||||
& (target_name_expr == models.ActiveQueueSession.target_name)
|
||||
& (task_type_expr == models.ActiveQueueSession.task_type),
|
||||
models.QueueItem.work_unit_key == models.ActiveQueueSession.work_unit_key,
|
||||
)
|
||||
|
||||
stmt = stmt.join(models.Session, models.QueueItem.session_id == models.Session.id)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import models, schemas
|
||||
from src.config import settings
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import DisabledException, ValidationException
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
from .session import get_or_create_session
|
||||
|
|
@ -314,141 +313,3 @@ async def update_message(
|
|||
await db.commit()
|
||||
# await db.refresh(honcho_message)
|
||||
return honcho_message
|
||||
|
||||
|
||||
async def search(
|
||||
query: str,
|
||||
*,
|
||||
workspace_name: str,
|
||||
session_name: str | None = None,
|
||||
peer_name: str | None = None,
|
||||
semantic: bool | None = None,
|
||||
) -> Select[tuple[models.Message]]:
|
||||
"""
|
||||
Search across message content using a hybrid approach:
|
||||
- Uses semantic search if embed_messages is set, else fall back to full text
|
||||
- Uses PostgreSQL full text search for natural language queries
|
||||
- Falls back to exact string matching for queries with special characters
|
||||
- Optionally uses semantic search with embeddings
|
||||
|
||||
If a session or peer is provided, the search will be scoped to that
|
||||
session or peer. Otherwise, it will search across all messages in the workspace.
|
||||
|
||||
Args:
|
||||
query: Search query to match against message content
|
||||
workspace_name: Name of the workspace
|
||||
session_name: Optional name of the session
|
||||
peer_name: Optional name of the peer
|
||||
semantic: Optional boolean to configure semantic search:
|
||||
- None: try semantic search if embed_messages is set, else fall back to full text
|
||||
- True: try semantic search if embed_messages is set, else throw error
|
||||
- False: use full text search
|
||||
|
||||
Returns:
|
||||
List of messages that match the search query, ordered by relevance
|
||||
"""
|
||||
import re
|
||||
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
# Base query conditions
|
||||
base_conditions = [models.Message.workspace_name == workspace_name]
|
||||
|
||||
should_use_semantic_search = False # Default to full text search
|
||||
|
||||
if semantic is None:
|
||||
# Try semantic search if embed_messages is set, else fall back to full text
|
||||
should_use_semantic_search = settings.EMBED_MESSAGES
|
||||
elif semantic is True:
|
||||
# Try semantic search if embed_messages is set, else throw error
|
||||
if settings.EMBED_MESSAGES:
|
||||
should_use_semantic_search = True
|
||||
else:
|
||||
raise DisabledException(
|
||||
"Semantic search requires EMBED_MESSAGES flag to be enabled"
|
||||
)
|
||||
|
||||
if should_use_semantic_search:
|
||||
# Generate embedding for the search query
|
||||
try:
|
||||
embedding_query = await embedding_client.embed(query)
|
||||
except ValueError as e:
|
||||
raise ValidationException(
|
||||
f"Query exceeds maximum token limit of {settings.MAX_EMBEDDING_TOKENS}."
|
||||
) from e
|
||||
|
||||
# Use cosine distance for semantic search on MessageEmbedding table
|
||||
# Join with Message table to get the actual message data
|
||||
base_query = (
|
||||
select(models.Message)
|
||||
.join(
|
||||
models.MessageEmbedding,
|
||||
models.Message.public_id == models.MessageEmbedding.message_id,
|
||||
)
|
||||
.where(models.MessageEmbedding.workspace_name == workspace_name)
|
||||
.order_by(
|
||||
models.MessageEmbedding.embedding.cosine_distance(embedding_query)
|
||||
)
|
||||
)
|
||||
|
||||
if session_name is not None:
|
||||
stmt = base_query.where(
|
||||
models.MessageEmbedding.session_name == session_name
|
||||
)
|
||||
elif peer_name is not None:
|
||||
stmt = base_query.where(models.MessageEmbedding.peer_name == peer_name)
|
||||
else:
|
||||
stmt = base_query
|
||||
|
||||
else:
|
||||
# Check if query contains special characters that FTS might not handle well
|
||||
has_special_chars = bool(
|
||||
re.search(r'[~`!@#$%^&*()_+=\[\]{};\':"\\|,.<>/?-]', query)
|
||||
)
|
||||
|
||||
if has_special_chars:
|
||||
# For queries with special characters, use exact string matching (ILIKE)
|
||||
# This ensures we can find exact matches like "~special-uuid~"
|
||||
search_condition = models.Message.content.ilike(f"%{query}%")
|
||||
|
||||
base_query = (
|
||||
select(models.Message)
|
||||
.where(*base_conditions, search_condition)
|
||||
.order_by(models.Message.created_at.desc())
|
||||
)
|
||||
else:
|
||||
# For natural language queries, use full text search with ranking
|
||||
fts_condition = func.to_tsvector("english", models.Message.content).op(
|
||||
"@@"
|
||||
)(func.plainto_tsquery("english", query))
|
||||
|
||||
# Combine FTS with ILIKE as fallback for better coverage
|
||||
combined_condition = or_(
|
||||
fts_condition, models.Message.content.ilike(f"%{query}%")
|
||||
)
|
||||
|
||||
base_query = (
|
||||
select(models.Message)
|
||||
.where(*base_conditions, combined_condition)
|
||||
.order_by(
|
||||
# Order by FTS relevance first, then by creation time
|
||||
func.coalesce(
|
||||
func.ts_rank(
|
||||
func.to_tsvector("english", models.Message.content),
|
||||
func.plainto_tsquery("english", query),
|
||||
),
|
||||
0,
|
||||
).desc(),
|
||||
models.Message.created_at.desc(),
|
||||
)
|
||||
)
|
||||
|
||||
# Add additional filters based on parameters
|
||||
if session_name is not None:
|
||||
stmt = base_query.where(models.Message.session_name == session_name)
|
||||
elif peer_name is not None:
|
||||
stmt = base_query.where(models.Message.peer_name == peer_name)
|
||||
else:
|
||||
stmt = base_query
|
||||
|
||||
return stmt
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
from logging import getLogger
|
||||
|
||||
from sqlalchemy import Select, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models, schemas
|
||||
from src.config import settings
|
||||
from src.crud.workspace import get_workspace
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
async def get_or_create_webhook_endpoint(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
webhook: schemas.WebhookEndpointCreate,
|
||||
) -> schemas.WebhookEndpoint:
|
||||
"""
|
||||
Get or create a webhook endpoint, optionally for a workspace.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
webhook: Webhook endpoint creation schema
|
||||
|
||||
Returns:
|
||||
The webhook endpoint
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the workspace is specified and does not exist
|
||||
"""
|
||||
# Verify workspace exists
|
||||
await get_workspace(db, workspace_name=workspace_name)
|
||||
|
||||
stmt = select(models.WebhookEndpoint).where(
|
||||
models.WebhookEndpoint.workspace_name == workspace_name,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
endpoints = result.scalars().all()
|
||||
|
||||
# No more than WORKSPACE_LIMIT webhooks per workspace
|
||||
if len(endpoints) >= settings.WEBHOOK.MAX_WORKSPACE_LIMIT:
|
||||
raise ValueError(
|
||||
f"Maximum number of webhook endpoints ({settings.WEBHOOK.MAX_WORKSPACE_LIMIT}) reached for this workspace."
|
||||
)
|
||||
|
||||
# Check if webhook already exists for this workspace
|
||||
for endpoint in endpoints:
|
||||
if endpoint.url == webhook.url:
|
||||
return schemas.WebhookEndpoint.model_validate(endpoint)
|
||||
|
||||
# Create new webhook endpoint
|
||||
webhook_endpoint = models.WebhookEndpoint(
|
||||
workspace_name=workspace_name,
|
||||
url=webhook.url,
|
||||
)
|
||||
db.add(webhook_endpoint)
|
||||
await db.commit()
|
||||
await db.refresh(webhook_endpoint)
|
||||
|
||||
logger.info(f"Webhook endpoint created: {webhook.url}")
|
||||
return schemas.WebhookEndpoint.model_validate(webhook_endpoint)
|
||||
|
||||
|
||||
async def list_webhook_endpoints(
|
||||
db: AsyncSession, workspace_name: str
|
||||
) -> Select[tuple[models.WebhookEndpoint]]:
|
||||
"""
|
||||
List all webhook endpoints, optionally filtered by workspace.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace (optional)
|
||||
|
||||
Returns:
|
||||
List of webhook endpoints
|
||||
"""
|
||||
# Verify workspace exists
|
||||
await get_workspace(db, workspace_name)
|
||||
|
||||
return select(models.WebhookEndpoint).where(
|
||||
models.WebhookEndpoint.workspace_name == workspace_name
|
||||
)
|
||||
|
||||
|
||||
async def delete_webhook_endpoint(
|
||||
db: AsyncSession, workspace_name: str, endpoint_id: str
|
||||
) -> None:
|
||||
"""
|
||||
Delete a webhook endpoint.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
endpoint_id: ID of the webhook endpoint
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the webhook endpoint is not found
|
||||
"""
|
||||
# Verify webhook endpoint exists
|
||||
stmt = select(models.WebhookEndpoint).where(
|
||||
models.WebhookEndpoint.id == endpoint_id,
|
||||
models.WebhookEndpoint.workspace_name == workspace_name,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
endpoint = result.scalar_one_or_none()
|
||||
|
||||
if not endpoint:
|
||||
raise ResourceNotFoundException(
|
||||
f"Webhook endpoint {endpoint_id} not found for workspace {workspace_name}"
|
||||
)
|
||||
|
||||
await db.delete(endpoint)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"Webhook endpoint {endpoint_id} deleted")
|
||||
|
|
@ -6,7 +6,7 @@ from sqlalchemy.exc import IntegrityError
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models, schemas
|
||||
from src.exceptions import ConflictException
|
||||
from src.exceptions import ConflictException, ResourceNotFoundException
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
|
@ -77,6 +77,34 @@ async def get_all_workspaces(
|
|||
return stmt
|
||||
|
||||
|
||||
async def get_workspace(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
) -> models.Workspace:
|
||||
"""
|
||||
Get an existing workspace.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace
|
||||
|
||||
Returns:
|
||||
The workspace if found or created
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the workspace does not exist
|
||||
"""
|
||||
# Try to get the existing peer
|
||||
stmt = select(models.Workspace).where(models.Workspace.name == workspace_name)
|
||||
result = await db.execute(stmt)
|
||||
existing_workspace = result.scalar_one_or_none()
|
||||
|
||||
if existing_workspace is not None:
|
||||
return existing_workspace
|
||||
|
||||
raise ResourceNotFoundException(f"Workspace {workspace_name} not found")
|
||||
|
||||
|
||||
async def update_workspace(
|
||||
db: AsyncSession, workspace_name: str, workspace: schemas.WorkspaceUpdate
|
||||
) -> models.Workspace:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from pydantic import ValidationError
|
|||
from rich.console import Console
|
||||
|
||||
from .deriver import Deriver
|
||||
from .queue_payload import RepresentationPayload, SummaryPayload
|
||||
from .queue_payload import RepresentationPayload, SummaryPayload, WebhookPayload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.getLogger("sqlalchemy.engine.Engine").disabled = True
|
||||
|
|
@ -15,14 +15,15 @@ console = Console(markup=True)
|
|||
deriver = Deriver()
|
||||
|
||||
|
||||
async def process_item(payload: dict[str, Any]) -> None:
|
||||
async def process_item(task_type: str, payload: dict[str, Any]) -> None:
|
||||
# Validate payload structure and types before processing
|
||||
try:
|
||||
task_type = payload.get("task_type")
|
||||
if task_type == "representation":
|
||||
validated_payload = RepresentationPayload(**payload)
|
||||
elif task_type == "summary":
|
||||
validated_payload = SummaryPayload(**payload)
|
||||
elif task_type == "webhook":
|
||||
validated_payload = WebhookPayload(**payload)
|
||||
else:
|
||||
raise ValueError(f"Invalid task_type: {task_type}")
|
||||
except ValidationError as e:
|
||||
|
|
@ -30,10 +31,20 @@ async def process_item(payload: dict[str, Any]) -> None:
|
|||
raise ValueError(f"Invalid payload structure: {str(e)}") from e
|
||||
|
||||
logger.debug(
|
||||
"process_item received payload for message %s in session %s, task type %s",
|
||||
validated_payload.message_id,
|
||||
validated_payload.session_name,
|
||||
validated_payload.task_type,
|
||||
"process_item received payload for task type %s ",
|
||||
task_type,
|
||||
)
|
||||
await deriver.process_message(validated_payload)
|
||||
logger.debug("Finished processing message %s", validated_payload.message_id)
|
||||
|
||||
if task_type == "webhook":
|
||||
if not isinstance(validated_payload, WebhookPayload):
|
||||
raise ValueError(f"Expected WebhookPayload, got {type(validated_payload)}")
|
||||
await deriver.process_webhook(validated_payload)
|
||||
logger.debug("Finished processing webhook %s", validated_payload.event_type)
|
||||
else:
|
||||
if not isinstance(validated_payload, RepresentationPayload | SummaryPayload):
|
||||
raise ValueError(
|
||||
f"Expected DeriverQueuePayload, got {type(validated_payload)}"
|
||||
)
|
||||
deriver_payload = validated_payload
|
||||
await deriver.process_message(task_type, deriver_payload)
|
||||
logger.debug("Finished processing message %s", deriver_payload.message_id)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
import time
|
||||
from typing import Any
|
||||
|
||||
import sentry_sdk
|
||||
from langfuse.decorators import langfuse_context
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -36,9 +37,15 @@ from src.utils.shared_models import (
|
|||
ReasoningResponseWithThinking,
|
||||
UnifiedObservation,
|
||||
)
|
||||
from src.webhooks import webhook_delivery
|
||||
|
||||
from .prompts import critical_analysis_prompt
|
||||
from .queue_payload import DeriverQueuePayload, RepresentationPayload, SummaryPayload
|
||||
from .queue_payload import (
|
||||
DeriverQueuePayload,
|
||||
RepresentationPayload,
|
||||
SummaryPayload,
|
||||
WebhookPayload,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.getLogger("sqlalchemy.engine.Engine").disabled = True
|
||||
|
|
@ -132,8 +139,18 @@ async def critical_analysis_call(
|
|||
class Deriver:
|
||||
"""Deriver class for processing messages and extracting insights."""
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_webhook(
|
||||
self,
|
||||
payload: WebhookPayload,
|
||||
) -> None:
|
||||
async with tracked_db() as db:
|
||||
await webhook_delivery.deliver_webhook(db, payload)
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_message(
|
||||
self,
|
||||
task_type: str,
|
||||
payload: DeriverQueuePayload,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -150,11 +167,20 @@ class Deriver:
|
|||
|
||||
# Open a DB session only for the duration of the processing call
|
||||
async with tracked_db("deriver") as db:
|
||||
if payload.task_type == "summary":
|
||||
if task_type == "summary":
|
||||
if not isinstance(payload, SummaryPayload):
|
||||
raise ValueError(f"Expected SummaryPayload, got {type(payload)}")
|
||||
await self.process_summary_task(db, payload)
|
||||
else:
|
||||
elif task_type == "representation":
|
||||
if not isinstance(payload, RepresentationPayload):
|
||||
raise ValueError(
|
||||
f"Expected RepresentationPayload, got {type(payload)}"
|
||||
)
|
||||
await self.process_representation_task(db, payload)
|
||||
else:
|
||||
raise ValueError(f"Unknown task type: {task_type}")
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_summary_task(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
|
|
@ -172,6 +198,7 @@ class Deriver:
|
|||
)
|
||||
log_performance_metrics(f"deriver_message_{payload.message_id}")
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_representation_task(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
|
|
@ -390,6 +417,7 @@ class CertaintyReasoner:
|
|||
)
|
||||
|
||||
@conditional_observe
|
||||
@sentry_sdk.trace
|
||||
async def derive_new_insights(
|
||||
self,
|
||||
context: ReasoningResponseWithThinking,
|
||||
|
|
@ -542,6 +570,7 @@ class CertaintyReasoner:
|
|||
return response
|
||||
|
||||
@conditional_observe
|
||||
@sentry_sdk.trace
|
||||
async def reason(
|
||||
self,
|
||||
context: ReasoningResponseWithThinking,
|
||||
|
|
@ -599,6 +628,7 @@ class CertaintyReasoner:
|
|||
return reasoning_response
|
||||
|
||||
@conditional_observe
|
||||
@sentry_sdk.trace
|
||||
async def _save_new_observations(
|
||||
self,
|
||||
original_context: ReasoningResponse,
|
||||
|
|
@ -671,6 +701,7 @@ class CertaintyReasoner:
|
|||
)
|
||||
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def save_working_representation_to_peer(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import crud, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.utils import get_work_unit_key
|
||||
from src.exceptions import ValidationException
|
||||
from src.models import QueueItem
|
||||
|
||||
|
|
@ -152,8 +153,12 @@ def create_representation_record(
|
|||
task_type="representation",
|
||||
)
|
||||
return {
|
||||
"work_unit_key": get_work_unit_key(
|
||||
task_type="representation", payload=processed_payload
|
||||
),
|
||||
"payload": processed_payload,
|
||||
"session_id": session_id,
|
||||
"task_type": "representation",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -180,8 +185,12 @@ def create_summary_record(
|
|||
message_seq_in_session=message_seq_in_session,
|
||||
)
|
||||
return {
|
||||
"work_unit_key": get_work_unit_key(
|
||||
task_type="summary", payload=processed_payload
|
||||
),
|
||||
"payload": processed_payload,
|
||||
"session_id": session_id,
|
||||
"task_type": "summary",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import asyncio
|
|||
import signal
|
||||
from asyncio import Task
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from logging import getLogger
|
||||
|
||||
import sentry_sdk
|
||||
|
|
@ -15,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from sqlalchemy.sql import func
|
||||
|
||||
from src.config import settings
|
||||
from src.models import QueueItem
|
||||
|
||||
from .. import models
|
||||
from ..dependencies import tracked_db
|
||||
|
|
@ -25,33 +25,11 @@ logger = getLogger(__name__)
|
|||
load_dotenv(override=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkUnit:
|
||||
"""
|
||||
Represents a unit of work in the queue system.
|
||||
|
||||
A work unit is uniquely identified by the combination of session_id,
|
||||
sender_name, target_name, and task_type. This allows multiple workers
|
||||
to process different work units from the same session in parallel.
|
||||
|
||||
For summary tasks, sender_name and target_name are None since summary
|
||||
tasks don't have these fields and should be processed sequentially per session.
|
||||
"""
|
||||
|
||||
session_id: str
|
||||
sender_name: str | None
|
||||
target_name: str | None
|
||||
task_type: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"({self.session_id}, {self.sender_name}, {self.target_name}, {self.task_type})"
|
||||
|
||||
|
||||
class QueueManager:
|
||||
def __init__(self):
|
||||
self.shutdown_event: asyncio.Event = asyncio.Event()
|
||||
self.active_tasks: set[asyncio.Task[None]] = set()
|
||||
self.owned_work_units: set[WorkUnit] = set()
|
||||
self.owned_work_units: set[str] = set()
|
||||
self.queue_empty_flag: asyncio.Event = asyncio.Event()
|
||||
|
||||
# Initialize from settings
|
||||
|
|
@ -70,21 +48,21 @@ class QueueManager:
|
|||
integrations=[AsyncioIntegration()],
|
||||
)
|
||||
|
||||
def add_task(self, task: asyncio.Task[None]):
|
||||
def add_task(self, task: asyncio.Task[None]) -> None:
|
||||
"""Track a new task"""
|
||||
self.active_tasks.add(task)
|
||||
task.add_done_callback(self.active_tasks.discard)
|
||||
|
||||
def track_work_unit(self, work_unit: WorkUnit):
|
||||
def track_work_unit(self, work_unit_key: str) -> None:
|
||||
"""Track a new work unit owned by this process"""
|
||||
self.owned_work_units.add(work_unit)
|
||||
self.owned_work_units.add(work_unit_key)
|
||||
|
||||
def untrack_work_unit(self, work_unit: WorkUnit):
|
||||
def untrack_work_unit(self, work_unit_key: str) -> None:
|
||||
"""Remove a work unit from tracking"""
|
||||
self.owned_work_units.discard(work_unit)
|
||||
self.owned_work_units.discard(work_unit_key)
|
||||
|
||||
async def initialize(self):
|
||||
"""Setup signal handlers and start the main polling loop"""
|
||||
async def initialize(self) -> None:
|
||||
"""Setup signal handlers, initialize client, and start the main polling loop"""
|
||||
logger.debug(f"Initializing QueueManager with {self.workers} workers")
|
||||
|
||||
# Set up signal handlers
|
||||
|
|
@ -103,7 +81,7 @@ class QueueManager:
|
|||
finally:
|
||||
await self.cleanup()
|
||||
|
||||
async def shutdown(self, sig: signal.Signals):
|
||||
async def shutdown(self, sig: signal.Signals) -> None:
|
||||
"""Handle graceful shutdown"""
|
||||
logger.info(f"Received exit signal {sig.name}...")
|
||||
self.shutdown_event.set()
|
||||
|
|
@ -114,24 +92,17 @@ class QueueManager:
|
|||
)
|
||||
await asyncio.gather(*self.active_tasks, return_exceptions=True)
|
||||
|
||||
async def cleanup(self):
|
||||
async def cleanup(self) -> None:
|
||||
"""Clean up owned work units"""
|
||||
if self.owned_work_units:
|
||||
logger.info(f"Cleaning up {len(self.owned_work_units)} owned work units...")
|
||||
try:
|
||||
# Use the tracked_db dependency for transaction safety
|
||||
async with tracked_db("queue_cleanup") as db:
|
||||
for work_unit in self.owned_work_units:
|
||||
for work_unit_key in self.owned_work_units:
|
||||
await db.execute(
|
||||
delete(models.ActiveQueueSession).where(
|
||||
models.ActiveQueueSession.session_id
|
||||
== work_unit.session_id,
|
||||
models.ActiveQueueSession.sender_name
|
||||
== work_unit.sender_name,
|
||||
models.ActiveQueueSession.target_name
|
||||
== work_unit.target_name,
|
||||
models.ActiveQueueSession.task_type
|
||||
== work_unit.task_type,
|
||||
models.ActiveQueueSession.work_unit_key == work_unit_key
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
|
@ -145,13 +116,13 @@ class QueueManager:
|
|||
# Polling and Scheduling #
|
||||
##########################
|
||||
|
||||
async def get_available_work_units(self, db: AsyncSession) -> Sequence[WorkUnit]:
|
||||
async def get_available_work_units(self, db: AsyncSession) -> Sequence[str]:
|
||||
"""
|
||||
Get available work units that aren't being processed.
|
||||
Returns a list of WorkUnit objects.
|
||||
Returns a list of work unit keys.
|
||||
"""
|
||||
# Clean up stale work units
|
||||
five_minutes_ago = datetime.now(UTC) - timedelta(
|
||||
five_minutes_ago = datetime.now(timezone.utc) - timedelta(
|
||||
minutes=settings.DERIVER.STALE_SESSION_TIMEOUT_MINUTES
|
||||
)
|
||||
await db.execute(
|
||||
|
|
@ -160,64 +131,24 @@ class QueueManager:
|
|||
)
|
||||
)
|
||||
|
||||
# Create the JSON path expressions once to ensure they're identical in SELECT and GROUP BY
|
||||
sender_name_expr = models.QueueItem.payload["sender_name"].astext
|
||||
target_name_expr = models.QueueItem.payload["target_name"].astext
|
||||
task_type_expr = models.QueueItem.payload["task_type"].astext
|
||||
|
||||
# Get available work units by extracting sender_name, target_name, task_type from payload
|
||||
# We need to join with ActiveQueueSession to find units that aren't already being processed
|
||||
result = await db.execute(
|
||||
select(
|
||||
models.QueueItem.session_id,
|
||||
sender_name_expr.label("sender_name"),
|
||||
target_name_expr.label("target_name"),
|
||||
task_type_expr.label("task_type"),
|
||||
)
|
||||
query = (
|
||||
select(models.QueueItem.work_unit_key)
|
||||
.outerjoin(
|
||||
models.ActiveQueueSession,
|
||||
(models.QueueItem.session_id == models.ActiveQueueSession.session_id)
|
||||
& (
|
||||
(sender_name_expr == models.ActiveQueueSession.sender_name)
|
||||
| (
|
||||
sender_name_expr.is_(None)
|
||||
& models.ActiveQueueSession.sender_name.is_(None)
|
||||
)
|
||||
)
|
||||
& (
|
||||
(target_name_expr == models.ActiveQueueSession.target_name)
|
||||
| (
|
||||
target_name_expr.is_(None)
|
||||
& models.ActiveQueueSession.target_name.is_(None)
|
||||
)
|
||||
)
|
||||
& (task_type_expr == models.ActiveQueueSession.task_type),
|
||||
models.QueueItem.work_unit_key
|
||||
== models.ActiveQueueSession.work_unit_key,
|
||||
)
|
||||
.where(~models.QueueItem.processed)
|
||||
.where(
|
||||
models.ActiveQueueSession.id.is_(None)
|
||||
) # Only work units not in active_queue_sessions
|
||||
.group_by(
|
||||
models.QueueItem.session_id,
|
||||
sender_name_expr,
|
||||
target_name_expr,
|
||||
task_type_expr,
|
||||
)
|
||||
.limit(self.workers) # Process multiple work units in parallel
|
||||
.where(models.QueueItem.work_unit_key.isnot(None))
|
||||
.where(models.ActiveQueueSession.work_unit_key.is_(None))
|
||||
.distinct()
|
||||
.limit(self.workers)
|
||||
)
|
||||
|
||||
rows = result.fetchall()
|
||||
return [
|
||||
WorkUnit(
|
||||
session_id=row.session_id,
|
||||
sender_name=row.sender_name,
|
||||
target_name=row.target_name,
|
||||
task_type=row.task_type,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def polling_loop(self):
|
||||
async def polling_loop(self) -> None:
|
||||
"""Main polling loop to find and process new work units"""
|
||||
logger.debug("Starting polling loop")
|
||||
try:
|
||||
|
|
@ -242,13 +173,10 @@ class QueueManager:
|
|||
if new_work_units and not self.shutdown_event.is_set():
|
||||
for work_unit in new_work_units:
|
||||
try:
|
||||
# Try to claim the work unit
|
||||
# Try to claim the work unit using work_unit_key
|
||||
await db.execute(
|
||||
insert(models.ActiveQueueSession).values(
|
||||
session_id=work_unit.session_id,
|
||||
sender_name=work_unit.sender_name,
|
||||
target_name=work_unit.target_name,
|
||||
task_type=work_unit.task_type,
|
||||
work_unit_key=work_unit
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
|
@ -265,12 +193,14 @@ class QueueManager:
|
|||
self.process_work_unit(work_unit)
|
||||
)
|
||||
self.add_task(task)
|
||||
|
||||
except IntegrityError:
|
||||
# Rollback the failed transaction to clear the error state
|
||||
await db.rollback()
|
||||
logger.debug(
|
||||
f"Failed to claim work unit {work_unit}, already owned"
|
||||
f"Failed to claim work unit {work_unit}, already owned by another worker"
|
||||
)
|
||||
# If we couldn't claim any work units, avoid tight loop
|
||||
else:
|
||||
self.queue_empty_flag.set()
|
||||
await asyncio.sleep(
|
||||
|
|
@ -292,32 +222,34 @@ class QueueManager:
|
|||
######################
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def process_work_unit(self, work_unit: WorkUnit):
|
||||
"""Process all messages for a specific work unit"""
|
||||
logger.debug(f"Starting to process work unit {work_unit}")
|
||||
# Use the tracked_db dependency for transaction safety
|
||||
async def process_work_unit(self, work_unit_key: str):
|
||||
"""Process all messages for a specific work unit by routing to the correct handler."""
|
||||
logger.debug(f"Starting to process work unit {work_unit_key}")
|
||||
async with (
|
||||
self.semaphore,
|
||||
tracked_db("queue_process_work_unit") as db,
|
||||
): # Hold the semaphore for the entire work unit duration
|
||||
message_count = 0
|
||||
try:
|
||||
message_count = 0
|
||||
while not self.shutdown_event.is_set():
|
||||
message = await self.get_next_message(db, work_unit)
|
||||
message = await self.get_next_message(db, work_unit_key)
|
||||
if not message:
|
||||
logger.debug(f"No more messages for work unit {work_unit}")
|
||||
logger.debug(f"No more messages for work unit {work_unit_key}")
|
||||
|
||||
break
|
||||
|
||||
message_count += 1
|
||||
try:
|
||||
logger.info(
|
||||
f"Processing message {message.payload['message_id']} from work unit {work_unit}"
|
||||
f"Processing item for task type {message.task_type} with id {message.id} from work unit {work_unit_key}"
|
||||
)
|
||||
await process_item(message.task_type, message.payload)
|
||||
logger.debug(
|
||||
f"Successfully processed queue item for task type {message.task_type} with id {message.id}"
|
||||
)
|
||||
await process_item(message.payload)
|
||||
logger.debug(f"Successfully processed message {message.id}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing message {message.id}: {str(e)}",
|
||||
f"Error processing queue item for task type {message.task_type} with id {message.id}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
if settings.SENTRY.ENABLED:
|
||||
|
|
@ -326,69 +258,83 @@ class QueueManager:
|
|||
# Prevent malformed messages from stalling queue indefinitely
|
||||
message.processed = True
|
||||
await db.commit()
|
||||
logger.debug(f"Marked message {message.id} as processed")
|
||||
|
||||
if self.shutdown_event.is_set():
|
||||
logger.debug(
|
||||
f"Shutdown requested, stopping processing for work unit {work_unit}"
|
||||
f"Shutdown requested, stopping processing for work unit {work_unit_key}"
|
||||
)
|
||||
break
|
||||
|
||||
# Update last_updated timestamp to show this work unit is still being processed
|
||||
await db.execute(
|
||||
update(models.ActiveQueueSession)
|
||||
.where(
|
||||
models.ActiveQueueSession.session_id
|
||||
== work_unit.session_id,
|
||||
models.ActiveQueueSession.sender_name
|
||||
== work_unit.sender_name,
|
||||
models.ActiveQueueSession.target_name
|
||||
== work_unit.target_name,
|
||||
models.ActiveQueueSession.task_type == work_unit.task_type,
|
||||
)
|
||||
.where(models.ActiveQueueSession.work_unit_key == work_unit_key)
|
||||
.values(last_updated=func.now())
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
logger.debug(
|
||||
f"Completed processing work unit {work_unit}, processed {message_count} messages"
|
||||
f"Completed processing work unit {work_unit_key}, processed {message_count} messages"
|
||||
)
|
||||
finally:
|
||||
# Remove work unit from active_queue_sessions when done
|
||||
logger.debug(f"Removing work unit {work_unit} from active sessions")
|
||||
await db.execute(
|
||||
logger.debug(f"Removing work unit {work_unit_key} from active sessions")
|
||||
delete_result = await db.execute(
|
||||
delete(models.ActiveQueueSession).where(
|
||||
models.ActiveQueueSession.session_id == work_unit.session_id,
|
||||
models.ActiveQueueSession.sender_name == work_unit.sender_name,
|
||||
models.ActiveQueueSession.target_name == work_unit.target_name,
|
||||
models.ActiveQueueSession.task_type == work_unit.task_type,
|
||||
models.ActiveQueueSession.work_unit_key == work_unit_key
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
self.untrack_work_unit(work_unit)
|
||||
|
||||
# Only publish webhook if we actually removed an active session
|
||||
if delete_result.rowcount > 0 and message_count > 0:
|
||||
try:
|
||||
from src.deriver.utils import parse_work_unit_key
|
||||
from src.webhooks.events import (
|
||||
QueueEmptyEvent,
|
||||
publish_webhook_event,
|
||||
)
|
||||
|
||||
parsed_key = parse_work_unit_key(work_unit_key)
|
||||
if parsed_key["task_type"] in ["representation", "summary"]:
|
||||
logger.info(
|
||||
f"Publishing queue.empty event for {work_unit_key}"
|
||||
)
|
||||
await publish_webhook_event(
|
||||
QueueEmptyEvent(
|
||||
workspace_id=parsed_key["workspace_name"],
|
||||
queue_type=parsed_key["task_type"],
|
||||
session_id=parsed_key["session_name"],
|
||||
sender_name=parsed_key["sender_name"],
|
||||
observer_name=parsed_key["target_name"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Skipping queue.empty event for webhook work unit {work_unit_key}"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error triggering queue_empty webhook")
|
||||
else:
|
||||
logger.debug(
|
||||
f"Work unit {work_unit_key} already cleaned up by another worker, skipping webhook"
|
||||
)
|
||||
|
||||
self.untrack_work_unit(work_unit_key)
|
||||
|
||||
@sentry_sdk.trace
|
||||
async def get_next_message(self, db: AsyncSession, work_unit: WorkUnit):
|
||||
"""Get the next unprocessed message for a specific work unit"""
|
||||
async def get_next_message(
|
||||
self, db: AsyncSession, work_unit_key: str
|
||||
) -> QueueItem | None:
|
||||
"""Get the next unprocessed message for a specific work unit."""
|
||||
|
||||
query = (
|
||||
select(models.QueueItem)
|
||||
.where(models.QueueItem.session_id == work_unit.session_id)
|
||||
.where(models.QueueItem.payload["task_type"].astext == work_unit.task_type)
|
||||
.where(models.QueueItem.work_unit_key == work_unit_key)
|
||||
.where(~models.QueueItem.processed)
|
||||
.order_by(models.QueueItem.id)
|
||||
.with_for_update(skip_locked=True)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
# For summary tasks, sender_name and target_name don't exist in payload
|
||||
# For other tasks, filter by sender_name and target_name
|
||||
if work_unit.task_type != "summary":
|
||||
query = query.where(
|
||||
models.QueueItem.payload["sender_name"].astext == work_unit.sender_name
|
||||
).where(
|
||||
models.QueueItem.payload["target_name"].astext == work_unit.target_name
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ from pydantic import BaseModel, ConfigDict
|
|||
class BasePayload(BaseModel):
|
||||
"""Base payload with common fields."""
|
||||
|
||||
workspace_name: str
|
||||
session_name: str
|
||||
message_id: int
|
||||
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore
|
||||
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
|
||||
|
||||
|
||||
class RepresentationPayload(BasePayload):
|
||||
"""Payload for representation tasks."""
|
||||
|
||||
task_type: Literal["representation"] = "representation"
|
||||
workspace_name: str
|
||||
session_name: str
|
||||
message_id: int
|
||||
content: str
|
||||
sender_name: str
|
||||
target_name: str
|
||||
|
|
@ -28,11 +27,34 @@ class SummaryPayload(BasePayload):
|
|||
"""Payload for summary tasks."""
|
||||
|
||||
task_type: Literal["summary"] = "summary"
|
||||
workspace_name: str
|
||||
session_name: str
|
||||
message_id: int
|
||||
message_seq_in_session: int
|
||||
|
||||
|
||||
# Union type for the actual payload
|
||||
class WebhookPayload(BasePayload):
|
||||
"""Payload for webhook delivery tasks."""
|
||||
|
||||
task_type: Literal["webhook"] = "webhook"
|
||||
workspace_name: str
|
||||
event_type: str
|
||||
data: dict[str, Any]
|
||||
|
||||
|
||||
# Union type for all possible queue payloads
|
||||
DeriverQueuePayload = RepresentationPayload | SummaryPayload
|
||||
QueuePayload = DeriverQueuePayload | WebhookPayload
|
||||
|
||||
|
||||
def create_webhook_payload(
|
||||
workspace_name: str,
|
||||
event_type: str,
|
||||
data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return WebhookPayload(
|
||||
workspace_name=workspace_name, event_type=event_type, data=data
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
def create_payload(
|
||||
|
|
@ -112,6 +134,7 @@ def create_payload(
|
|||
# Convert back to dict for compatibility with JSON serialization
|
||||
# mode='json' ensures datetime is converted to ISO string
|
||||
payload = validated_payload.model_dump(mode="json")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to create valid payload: {str(e)}") from e
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
from typing_extensions import Any, TypedDict
|
||||
|
||||
|
||||
class ParsedWorkUnit(TypedDict):
|
||||
task_type: str
|
||||
workspace_name: str
|
||||
session_name: str | None
|
||||
sender_name: str | None
|
||||
target_name: str | None
|
||||
|
||||
|
||||
def get_work_unit_key(task_type: str, payload: dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate a work unit key for a given task type, workspace name, and event type.
|
||||
"""
|
||||
workspace_name = payload.get("workspace_name")
|
||||
if not workspace_name:
|
||||
raise ValueError("workspace_name is required to generate a work_unit_key")
|
||||
|
||||
if task_type in ["representation", "summary"]:
|
||||
sender_name = payload.get("sender_name", "None")
|
||||
target_name = payload.get("target_name", "None")
|
||||
session_name = payload.get("session_name", "None")
|
||||
return (
|
||||
f"{task_type}:{workspace_name}:{session_name}:{sender_name}:{target_name}"
|
||||
)
|
||||
|
||||
if task_type == "webhook":
|
||||
return f"webhook:{workspace_name}"
|
||||
|
||||
raise ValueError(f"Invalid task type: {task_type}")
|
||||
|
||||
|
||||
def parse_work_unit_key(work_unit_key: str) -> ParsedWorkUnit:
|
||||
"""
|
||||
Parse a work unit key to extract its components.
|
||||
"""
|
||||
parts = work_unit_key.split(":")
|
||||
task_type = parts[0]
|
||||
|
||||
if task_type in ["representation", "summary"]:
|
||||
if len(parts) != 5:
|
||||
raise ValueError(
|
||||
f"Invalid work_unit_key format for task_type {task_type}: {work_unit_key}"
|
||||
)
|
||||
return {
|
||||
"task_type": task_type,
|
||||
"workspace_name": parts[1],
|
||||
"session_name": parts[2],
|
||||
"sender_name": parts[3],
|
||||
"target_name": parts[4],
|
||||
}
|
||||
|
||||
if task_type == "webhook":
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f"Invalid work_unit_key format for task_type {task_type}: {work_unit_key}"
|
||||
)
|
||||
return {
|
||||
"task_type": task_type,
|
||||
"workspace_name": parts[1],
|
||||
"session_name": None,
|
||||
"sender_name": None,
|
||||
"target_name": None,
|
||||
}
|
||||
|
||||
raise ValueError(f"Invalid task type in work_unit_key: {task_type}")
|
||||
|
|
@ -24,6 +24,7 @@ from src.routers import (
|
|||
messages,
|
||||
peers,
|
||||
sessions,
|
||||
webhooks,
|
||||
workspaces,
|
||||
)
|
||||
from src.security import create_admin_jwt
|
||||
|
|
@ -101,6 +102,7 @@ if SENTRY_ENABLED:
|
|||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
# Lifespan events are now handled by the respective services
|
||||
yield
|
||||
await engine.dispose()
|
||||
|
||||
|
|
@ -115,7 +117,7 @@ app = FastAPI(
|
|||
title="Honcho API",
|
||||
summary="The Identity Layer for the Agentic World",
|
||||
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
|
||||
version="2.1.2",
|
||||
version="2.2.0",
|
||||
contact={
|
||||
"name": "Plastic Labs",
|
||||
"url": "https://honcho.dev",
|
||||
|
|
@ -150,6 +152,7 @@ app.include_router(peers.router, prefix="/v2")
|
|||
app.include_router(sessions.router, prefix="/v2")
|
||||
app.include_router(messages.router, prefix="/v2")
|
||||
app.include_router(keys.router, prefix="/v2")
|
||||
app.include_router(webhooks.router, prefix="/v2")
|
||||
|
||||
|
||||
# Global exception handlers
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import datetime
|
||||
from logging import getLogger
|
||||
from typing import Any, final
|
||||
from typing import Any, Literal, final
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
|
@ -83,6 +83,7 @@ class Workspace(Base):
|
|||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(TEXT, index=True, unique=True)
|
||||
peers = relationship("Peer", back_populates="workspace")
|
||||
webhook_endpoints = relationship("WebhookEndpoint", back_populates="workspace")
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), index=True, default=func.now()
|
||||
)
|
||||
|
|
@ -358,6 +359,9 @@ class Document(Base):
|
|||
)
|
||||
|
||||
|
||||
TaskType = Literal["webhook", "summary", "representation"]
|
||||
|
||||
|
||||
@final
|
||||
class QueueItem(Base):
|
||||
__tablename__: str = "queue"
|
||||
|
|
@ -367,6 +371,9 @@ class QueueItem(Base):
|
|||
session_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("sessions.id"), index=True, nullable=True
|
||||
)
|
||||
work_unit_key: Mapped[str] = mapped_column(TEXT, nullable=False)
|
||||
|
||||
task_type: Mapped[TaskType] = mapped_column(TEXT, nullable=False)
|
||||
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
processed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
|
|
@ -376,25 +383,35 @@ class ActiveQueueSession(Base):
|
|||
__tablename__: str = "active_queue_sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
session_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("sessions.id"), nullable=True
|
||||
)
|
||||
sender_name: Mapped[str | None] = mapped_column(TEXT, nullable=True)
|
||||
target_name: Mapped[str | None] = mapped_column(TEXT, nullable=True)
|
||||
task_type: Mapped[str] = mapped_column(TEXT)
|
||||
|
||||
work_unit_key: Mapped[str] = mapped_column(TEXT, unique=True, index=True)
|
||||
|
||||
last_updated: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"session_id",
|
||||
"sender_name",
|
||||
"target_name",
|
||||
"task_type",
|
||||
name="unique_active_queue_session",
|
||||
),
|
||||
|
||||
@final
|
||||
class WebhookEndpoint(Base):
|
||||
__tablename__: str = "webhook_endpoints"
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
workspace_name: Mapped[str] = mapped_column(
|
||||
ForeignKey("workspaces.name"), index=True, nullable=False
|
||||
)
|
||||
url: Mapped[str] = mapped_column(TEXT, nullable=False)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=func.now()
|
||||
)
|
||||
|
||||
workspace = relationship("Workspace", back_populates="webhook_endpoints")
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("length(url) <= 2048", name="webhook_endpoint_url_length"),
|
||||
Index("idx_webhook_endpoints_workspace_lookup", "workspace_name"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"WebhookEndpoint(id={self.id}, workspace_name={self.workspace_name}, url={self.url})"
|
||||
|
||||
|
||||
@final
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ async def get_messages(
|
|||
"""Get all messages for a session"""
|
||||
try:
|
||||
filters = None
|
||||
if options and hasattr(options, "filter"):
|
||||
filters = options.filter
|
||||
if options and hasattr(options, "filters"):
|
||||
filters = options.filters
|
||||
if filters == {}:
|
||||
filters = None
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from src.dependencies import db
|
|||
from src.dialectic import chat as dialectic_chat
|
||||
from src.exceptions import AuthenticationException, ResourceNotFoundException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils.search import search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -42,8 +43,8 @@ async def get_peers(
|
|||
):
|
||||
"""Get All Peers for a Workspace"""
|
||||
filter_param = None
|
||||
if options and hasattr(options, "filter"):
|
||||
filter_param = options.filter
|
||||
if options and hasattr(options, "filters"):
|
||||
filter_param = options.filters
|
||||
if filter_param == {}:
|
||||
filter_param = None
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ async def get_or_create_peer(
|
|||
Get a Peer by ID
|
||||
|
||||
If peer_id is provided as a query parameter, it uses that (must match JWT workspace_id).
|
||||
Otherwise, it uses the peer_id from the JWT token.
|
||||
Otherwise, it uses the peer_id from the JWT.
|
||||
"""
|
||||
# validate workspace query param
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
|
|
@ -125,8 +126,8 @@ async def get_sessions_for_peer(
|
|||
"""Get All Sessions for a Peer"""
|
||||
filter_param = None
|
||||
|
||||
if options and hasattr(options, "filter"):
|
||||
filter_param = options.filter
|
||||
if options and hasattr(options, "filters"):
|
||||
filter_param = options.filters
|
||||
if filter_param == {}:
|
||||
filter_param = None
|
||||
|
||||
|
|
@ -239,7 +240,7 @@ async def get_working_representation(
|
|||
|
||||
@router.post(
|
||||
"/{peer_id}/search",
|
||||
response_model=Page[schemas.Message],
|
||||
response_model=list[schemas.Message],
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", peer_name="peer_id"))
|
||||
],
|
||||
|
|
@ -247,17 +248,14 @@ async def get_working_representation(
|
|||
async def search_peer(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
search: schemas.MessageSearchOptions = Body(
|
||||
body: schemas.MessageSearchOptions = Body(
|
||||
..., description="Message search parameters "
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Peer"""
|
||||
stmt = await crud.search(
|
||||
search.query,
|
||||
workspace_name=workspace_id,
|
||||
peer_name=peer_id,
|
||||
semantic=search.semantic,
|
||||
)
|
||||
|
||||
return await apaginate(db, stmt)
|
||||
# take user-provided filter and add workspace_id and peer_id to it
|
||||
filters = body.filters or {}
|
||||
filters["workspace_id"] = workspace_id
|
||||
filters["peer_id"] = peer_id
|
||||
return await search(db, body.query, filters=filters, limit=body.limit)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from src.exceptions import (
|
|||
)
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils import summarizer
|
||||
from src.utils.search import search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ async def get_or_create_session(
|
|||
Get a specific session in a workspace.
|
||||
|
||||
If session_id is provided as a query parameter, it verifies the session is in the workspace.
|
||||
Otherwise, it uses the session_id from the JWT token for verification.
|
||||
Otherwise, it uses the session_id from the JWT for verification.
|
||||
"""
|
||||
# Verify JWT has access to the requested resource
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
|
|
@ -85,8 +86,8 @@ async def get_sessions(
|
|||
"""Get All Sessions in a Workspace"""
|
||||
filter_param = None
|
||||
|
||||
if options and hasattr(options, "filter") and options.filter:
|
||||
filter_param = options.filter
|
||||
if options and hasattr(options, "filters") and options.filters:
|
||||
filter_param = options.filters
|
||||
if filter_param == {}: # Explicitly check for empty dict
|
||||
filter_param = None
|
||||
|
||||
|
|
@ -448,7 +449,7 @@ async def get_session_context(
|
|||
|
||||
@router.post(
|
||||
"/{session_id}/search",
|
||||
response_model=Page[schemas.Message],
|
||||
response_model=list[schemas.Message],
|
||||
dependencies=[
|
||||
Depends(require_auth(workspace_name="workspace_id", session_name="session_id"))
|
||||
],
|
||||
|
|
@ -456,19 +457,19 @@ async def get_session_context(
|
|||
async def search_session(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
search: schemas.MessageSearchOptions = Body(
|
||||
..., description="Message search parameters "
|
||||
body: schemas.MessageSearchOptions = Body(
|
||||
..., description="Message search parameters"
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Session"""
|
||||
query, semantic = search.query, search.semantic
|
||||
|
||||
stmt = await crud.search(
|
||||
query,
|
||||
workspace_name=workspace_id,
|
||||
session_name=session_id,
|
||||
semantic=semantic,
|
||||
# take user-provided filter and add workspace_id and session_id to it
|
||||
filters = body.filters or {}
|
||||
filters["workspace_id"] = workspace_id
|
||||
filters["session_id"] = session_id
|
||||
return await search(
|
||||
db,
|
||||
body.query,
|
||||
filters=filters,
|
||||
limit=body.limit,
|
||||
)
|
||||
|
||||
return await apaginate(db, stmt)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import apaginate
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import schemas
|
||||
from src.config import settings
|
||||
from src.crud import webhook as crud
|
||||
from src.dependencies import db
|
||||
from src.exceptions import AuthenticationException, ConflictException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.webhooks.events import (
|
||||
TestEvent,
|
||||
publish_webhook_event,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/workspaces/{workspace_id}/webhooks",
|
||||
tags=["webhooks"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.WebhookEndpoint)
|
||||
async def get_or_create_webhook_endpoint(
|
||||
workspace_id: str = Path(..., description="Workspace ID"),
|
||||
webhook: schemas.WebhookEndpointCreate = Body(
|
||||
..., description="Webhook endpoint parameters"
|
||||
),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db: AsyncSession = db,
|
||||
) -> schemas.WebhookEndpoint:
|
||||
"""
|
||||
Get or create a webhook endpoint URL.
|
||||
"""
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
raise AuthenticationException("Unauthorized access to resource")
|
||||
|
||||
try:
|
||||
return await crud.get_or_create_webhook_endpoint(
|
||||
db, workspace_id, webhook=webhook
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ConflictException(
|
||||
f"Maximum number of webhook endpoints ({settings.WEBHOOK.MAX_WORKSPACE_LIMIT}) reached for this workspace."
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("", response_model=Page[schemas.WebhookEndpoint])
|
||||
async def list_webhook_endpoints(
|
||||
workspace_id: str = Path(..., description="Workspace ID"),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db: AsyncSession = db,
|
||||
) -> Page[schemas.WebhookEndpoint]:
|
||||
"""
|
||||
List all webhook endpoints, optionally filtered by workspace.
|
||||
"""
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
raise AuthenticationException("Unauthorized access to resource")
|
||||
|
||||
stmt = await crud.list_webhook_endpoints(db, workspace_id)
|
||||
return await apaginate(db, stmt)
|
||||
|
||||
|
||||
@router.delete("/{endpoint_id}", response_model=None)
|
||||
async def delete_webhook_endpoint(
|
||||
workspace_id: str = Path(..., description="Workspace ID"),
|
||||
endpoint_id: str = Path(..., description="Webhook endpoint ID"),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db: AsyncSession = db,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a specific webhook endpoint.
|
||||
"""
|
||||
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
raise AuthenticationException("Unauthorized access to resource")
|
||||
|
||||
await crud.delete_webhook_endpoint(db, workspace_id, endpoint_id)
|
||||
|
||||
|
||||
@router.get("/test")
|
||||
async def test_emit(
|
||||
workspace_id: str = Path(..., description="Workspace ID"),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
) -> None:
|
||||
"""
|
||||
Test publishing a webhook event.
|
||||
"""
|
||||
if not jwt_params.ad and jwt_params.w is not None and jwt_params.w != workspace_id:
|
||||
raise AuthenticationException("Unable to publish test webhook")
|
||||
|
||||
event = TestEvent(workspace_id=workspace_id)
|
||||
await publish_webhook_event(event)
|
||||
|
|
@ -9,6 +9,7 @@ from src import crud, schemas
|
|||
from src.dependencies import db
|
||||
from src.exceptions import AuthenticationException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils.search import search
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ async def get_or_create_workspace(
|
|||
Get a Workspace by ID.
|
||||
|
||||
If workspace_id is provided as a query parameter, it uses that (must match JWT workspace_id).
|
||||
Otherwise, it uses the workspace_id from the JWT token.
|
||||
Otherwise, it uses the workspace_id from the JWT.
|
||||
"""
|
||||
# If workspace_id provided in query, check if it matches jwt or user is admin
|
||||
if workspace.name:
|
||||
|
|
@ -60,8 +61,8 @@ async def get_all_workspaces(
|
|||
):
|
||||
"""Get all Workspaces"""
|
||||
filter_param = None
|
||||
if options and hasattr(options, "filter"):
|
||||
filter_param = options.filter
|
||||
if options and hasattr(options, "filters"):
|
||||
filter_param = options.filters
|
||||
if filter_param == {}:
|
||||
filter_param = None
|
||||
|
||||
|
|
@ -93,18 +94,21 @@ async def update_workspace(
|
|||
|
||||
@router.post(
|
||||
"/{workspace_id}/search",
|
||||
response_model=Page[schemas.Message],
|
||||
response_model=list[schemas.Message],
|
||||
dependencies=[Depends(require_auth(workspace_name="workspace_id"))],
|
||||
)
|
||||
async def search_workspace(
|
||||
workspace_id: str = Path(..., description="ID of the workspace to search"),
|
||||
query: str = Body(..., description="Search query"),
|
||||
body: schemas.MessageSearchOptions = Body(
|
||||
..., description="Message search parameters "
|
||||
),
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Workspace"""
|
||||
stmt = await crud.search(query, workspace_name=workspace_id)
|
||||
|
||||
return await apaginate(db, stmt)
|
||||
# take user-provided filter and add workspace_id to it
|
||||
filters = body.filters or {}
|
||||
filters["workspace_id"] = workspace_id
|
||||
return await search(db, body.query, filters=filters, limit=body.limit)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# pyright: reportUnannotatedClassAttribute=false # pyright: ignore
|
||||
import datetime
|
||||
import ipaddress
|
||||
from typing import Annotated, Any, Self
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import tiktoken
|
||||
from pydantic import (
|
||||
|
|
@ -8,6 +10,7 @@ from pydantic import (
|
|||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ class WorkspaceCreate(WorkspaceBase):
|
|||
|
||||
|
||||
class WorkspaceGet(WorkspaceBase):
|
||||
filter: dict[str, Any] | None = None
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class WorkspaceUpdate(WorkspaceBase):
|
||||
|
|
@ -67,7 +70,7 @@ class PeerCreate(PeerBase):
|
|||
|
||||
|
||||
class PeerGet(PeerBase):
|
||||
filter: dict[str, Any] | None = None
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class PeerUpdate(PeerBase):
|
||||
|
|
@ -131,7 +134,7 @@ class MessageCreate(MessageBase):
|
|||
|
||||
|
||||
class MessageGet(MessageBase):
|
||||
filter: dict[str, Any] | None = None
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MessageUpdate(MessageBase):
|
||||
|
|
@ -197,7 +200,7 @@ class SessionCreate(SessionBase):
|
|||
|
||||
|
||||
class SessionGet(SessionBase):
|
||||
filter: dict[str, Any] | None = None
|
||||
filters: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class SessionUpdate(SessionBase):
|
||||
|
|
@ -246,9 +249,14 @@ class DocumentUpdate(DocumentBase):
|
|||
|
||||
class MessageSearchOptions(BaseModel):
|
||||
query: str = Field(..., description="Search query")
|
||||
semantic: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to explicitly use semantic search to filter the results",
|
||||
filters: dict[str, Any] | None = Field(
|
||||
default=None, description="Filters to scope the search"
|
||||
)
|
||||
limit: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of results to return",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -346,3 +354,44 @@ class DeriverStatus(BaseModel):
|
|||
sessions: dict[str, SessionDeriverStatus] | None = Field(
|
||||
default=None, description="Per-session status when not filtered by session"
|
||||
)
|
||||
|
||||
|
||||
# Webhook endpoint schemas
|
||||
class WebhookEndpointBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class WebhookEndpointCreate(WebhookEndpointBase):
|
||||
url: str
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_webhook_url(cls, v: str) -> str:
|
||||
parsed = urlparse(v)
|
||||
|
||||
if not all([parsed.scheme, parsed.netloc]):
|
||||
raise ValueError("Invalid URL format")
|
||||
|
||||
# Only allow HTTP/HTTPS
|
||||
if parsed.scheme not in ["http", "https"]:
|
||||
raise ValueError("Only HTTP and HTTPS URLs are allowed")
|
||||
|
||||
# Block private/internal addresses
|
||||
if parsed.hostname:
|
||||
try:
|
||||
ip_address = ipaddress.ip_address(parsed.hostname)
|
||||
if ip_address.is_private:
|
||||
raise ValueError("Private IP addresses are not allowed")
|
||||
except ValueError: # Not an IP address, might be a hostname
|
||||
pass
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class WebhookEndpoint(WebhookEndpointBase):
|
||||
id: str
|
||||
workspace_name: str | None = Field(serialization_alias="workspace_id")
|
||||
url: str
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True) # pyright: ignore
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ def create_admin_jwt() -> str:
|
|||
|
||||
|
||||
def create_jwt(params: JWTParams) -> str:
|
||||
"""Create a JWT token from the given parameters."""
|
||||
"""Create a JWT from the given parameters."""
|
||||
payload = {k: v for k, v in params.__dict__.items() if v is not None}
|
||||
if not settings.AUTH.JWT_SECRET:
|
||||
raise ValueError("AUTH_JWT_SECRET is not set, cannot create JWT.")
|
||||
|
|
@ -80,7 +80,7 @@ def create_jwt(params: JWTParams) -> str:
|
|||
|
||||
|
||||
async def verify_jwt(token: str) -> JWTParams:
|
||||
"""Verify a JWT token and return the decoded parameters."""
|
||||
"""Verify a JWT and return the decoded parameters."""
|
||||
|
||||
params = JWTParams()
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -33,29 +33,6 @@ class EmbeddingStore:
|
|||
self.workspace_name: str = workspace_name
|
||||
self.peer_name: str = peer_name
|
||||
self.collection_name: str = collection_name
|
||||
# Initialize observation counts with config defaults
|
||||
self.explicit_observations_count: int = (
|
||||
settings.DERIVER.EXPLICIT_OBSERVATIONS_COUNT
|
||||
)
|
||||
self.deductive_observations_count: int = (
|
||||
settings.DERIVER.DEDUCTIVE_OBSERVATIONS_COUNT
|
||||
)
|
||||
|
||||
def set_observation_counts(
|
||||
self,
|
||||
explicit: int | None = None,
|
||||
deductive: int | None = None,
|
||||
) -> None:
|
||||
"""Set the number of observations to retrieve for each reasoning level.
|
||||
|
||||
Args:
|
||||
explicit: Number of explicit observations to retrieve
|
||||
deductive: Number of deductive observations to retrieve
|
||||
"""
|
||||
if explicit is not None:
|
||||
self.explicit_observations_count = explicit
|
||||
if deductive is not None:
|
||||
self.deductive_observations_count = deductive
|
||||
|
||||
@conditional_observe
|
||||
async def save_unified_observations(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
"""
|
||||
Reciprocal Rank Fusion (RRF) utilities for combining search results.
|
||||
|
||||
RRF is a method to combine multiple ranked lists by computing the reciprocal
|
||||
of each item's rank in each list, then summing these reciprocal ranks.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy import Select, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
from src.config import settings
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ValidationException
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def reciprocal_rank_fusion(*ranked_lists: list[T], k: int = 60, limit: int) -> list[T]:
|
||||
"""
|
||||
Combine multiple ranked lists using Reciprocal Rank Fusion (RRF).
|
||||
|
||||
RRF assigns a score to each item based on the formula:
|
||||
RRF_score = sum(1 / (k + rank_i)) for all lists where the item appears
|
||||
|
||||
Where:
|
||||
- k is a constant (typically 60) that controls the impact of high-ranked items
|
||||
- rank_i is the rank of the item in list i (1-indexed)
|
||||
|
||||
Args:
|
||||
*ranked_lists: Variable number of ranked lists to combine
|
||||
k: RRF constant parameter (default: 60)
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
list of items ranked by RRF score (highest score first)
|
||||
"""
|
||||
if not ranked_lists:
|
||||
return []
|
||||
|
||||
# dictionary to store RRF scores for each item
|
||||
rrf_scores: dict[T, float] = {}
|
||||
|
||||
# Process each ranked list
|
||||
for ranked_list in ranked_lists:
|
||||
for rank, item in enumerate(ranked_list, 1): # 1-indexed ranking
|
||||
if item not in rrf_scores:
|
||||
rrf_scores[item] = 0.0
|
||||
# Add reciprocal rank contribution from this list
|
||||
rrf_scores[item] += 1.0 / (k + rank)
|
||||
|
||||
# Sort items by RRF score (descending order)
|
||||
sorted_items = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Extract just the items (not the scores)
|
||||
result = [item for item, _ in sorted_items]
|
||||
|
||||
return result[:limit]
|
||||
|
||||
|
||||
async def _semantic_search(
|
||||
db: AsyncSession,
|
||||
query: str,
|
||||
stmt: Select[tuple[models.Message]],
|
||||
limit: int,
|
||||
) -> list[models.Message]:
|
||||
"""
|
||||
Perform semantic search using message embeddings.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
query: Search query
|
||||
stmt: Base SQL query conditions
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
list of messages ordered by semantic similarity
|
||||
"""
|
||||
try:
|
||||
embedding_query = await embedding_client.embed(query)
|
||||
except ValueError as e:
|
||||
raise ValidationException(
|
||||
f"Query exceeds maximum token limit of {settings.MAX_EMBEDDING_TOKENS}."
|
||||
) from e
|
||||
|
||||
# Use cosine distance for semantic search on MessageEmbedding table
|
||||
semantic_query = stmt.join(
|
||||
models.MessageEmbedding,
|
||||
models.Message.public_id == models.MessageEmbedding.message_id,
|
||||
).order_by(models.MessageEmbedding.embedding.cosine_distance(embedding_query))
|
||||
|
||||
semantic_query = semantic_query.limit(limit)
|
||||
|
||||
result = await db.execute(semantic_query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def _fulltext_search(
|
||||
db: AsyncSession,
|
||||
query: str,
|
||||
stmt: Select[tuple[models.Message]],
|
||||
limit: int,
|
||||
) -> list[models.Message]:
|
||||
"""
|
||||
Perform full-text search using PostgreSQL FTS and ILIKE fallback.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
query: Search query
|
||||
stmt: Base SQL query conditions
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
list of messages ordered by text search relevance
|
||||
"""
|
||||
# Check if query contains special characters that FTS might not handle well
|
||||
has_special_chars = bool(
|
||||
re.search(r'[~`!@#$%^&*()_+=\[\]{};\':"\\|,.<>/?-]', query)
|
||||
)
|
||||
|
||||
if has_special_chars:
|
||||
# For queries with special characters, use exact string matching (ILIKE)
|
||||
search_condition = models.Message.content.ilike(f"%{query}%")
|
||||
fulltext_query = stmt.where(search_condition).order_by(
|
||||
models.Message.created_at.desc()
|
||||
)
|
||||
else:
|
||||
# For natural language queries, use full text search with ranking
|
||||
fts_condition = func.to_tsvector("english", models.Message.content).op("@@")(
|
||||
func.plainto_tsquery("english", query)
|
||||
)
|
||||
|
||||
# Combine FTS with ILIKE as fallback for better coverage
|
||||
combined_condition = or_(
|
||||
fts_condition, models.Message.content.ilike(f"%{query}%")
|
||||
)
|
||||
|
||||
fulltext_query = stmt.where(combined_condition).order_by(
|
||||
# Order by FTS relevance first, then by creation time
|
||||
func.coalesce(
|
||||
func.ts_rank(
|
||||
func.to_tsvector("english", models.Message.content),
|
||||
func.plainto_tsquery("english", query),
|
||||
),
|
||||
0,
|
||||
).desc(),
|
||||
models.Message.created_at.desc(),
|
||||
)
|
||||
|
||||
fulltext_query = fulltext_query.limit(limit)
|
||||
|
||||
result = await db.execute(fulltext_query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def search(
|
||||
db: AsyncSession,
|
||||
query: str,
|
||||
*,
|
||||
filters: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
) -> list[models.Message]:
|
||||
"""
|
||||
Search across message content using a hybrid approach with Reciprocal Rank Fusion (RRF).
|
||||
|
||||
This function combines semantic search and full-text search results using RRF when both
|
||||
are available, providing better search results than either method alone.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
query: Search query to match against message content
|
||||
filters: Optional filters to scope search
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
list of messages that match the search query, ordered by RRF relevance or individual search relevance
|
||||
|
||||
Raises:
|
||||
ValidationException: If query exceeds maximum token limit for embeddings
|
||||
"""
|
||||
# Base query conditions
|
||||
stmt = select(models.Message)
|
||||
stmt = apply_filter(stmt, models.Message, filters)
|
||||
|
||||
search_results: list[list[models.Message]] = []
|
||||
|
||||
# Perform semantic search if enabled
|
||||
if settings.EMBED_MESSAGES:
|
||||
# Get more results for fusion
|
||||
semantic_limit = limit * 2
|
||||
semantic_results = await _semantic_search(
|
||||
db=db, query=query, stmt=stmt, limit=semantic_limit
|
||||
)
|
||||
search_results.append(semantic_results)
|
||||
|
||||
# Perform full-text search
|
||||
# Get more results for fusion
|
||||
fulltext_limit = limit * 2
|
||||
fulltext_results = await _fulltext_search(
|
||||
db=db, query=query, stmt=stmt, limit=fulltext_limit
|
||||
)
|
||||
search_results.append(fulltext_results)
|
||||
|
||||
# Combine results using RRF if we have multiple search methods
|
||||
if len(search_results) > 1:
|
||||
# Use RRF to combine semantic and full-text results
|
||||
combined_results = reciprocal_rank_fusion(*search_results, limit=limit)
|
||||
elif len(search_results) == 1:
|
||||
# Single search method - apply limit directly
|
||||
combined_results = search_results[0]
|
||||
combined_results = combined_results[:limit]
|
||||
else:
|
||||
# No search results
|
||||
combined_results = []
|
||||
|
||||
return combined_results
|
||||
|
|
@ -6,9 +6,9 @@ from __future__ import annotations
|
|||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class ReasoningLevel(str, Enum):
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import datetime
|
|||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import TypedDict
|
||||
|
||||
from mirascope import llm
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# Webhooks
|
||||
|
||||
Webhooks are used to deliver event notifications to user-configured URLs.
|
||||
|
||||
## System Architecture
|
||||
|
||||
The webhooks system consists of several key components:
|
||||
|
||||
* **API Endpoints (`routers/webhooks.py`):** Provides endpoints for users to create, list, delete, and test their webhook subscriptions.
|
||||
* **Event Publishing (`webhooks/events.py`):** Defines the event types and allows us to publish new events to the processing queue.
|
||||
* **Webhook Delivery (`webhooks/webhook_delivery.py`):** Contains the logic for sending the webhook to the subscriber's URL.
|
||||
|
||||
## Event Flow
|
||||
|
||||
1. An event is triggered within the application by calling `publish_webhook_event` with a defined event payload.
|
||||
2. This function creates a `QueueItem` and stores it in the database.
|
||||
3. The `QueueManager` background process polls the database for new items.
|
||||
4. When a new event is found, it is passed to the `deliver_webhook` function.
|
||||
5. `deliver_webhook` fetches all subscriber URLs for the event's workspace, signs the payload with a secret key, and sends an HTTP POST request to each URL.
|
||||
|
||||
Note that the webhooks require the *deriver* process to be running to facilitate the delivery of the webhook.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import logging
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.queue_payload import create_webhook_payload
|
||||
from src.deriver.utils import get_work_unit_key
|
||||
from src.models import QueueItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebhookEventType(str, Enum):
|
||||
QUEUE_EMPTY = "queue.empty"
|
||||
TEST = "test.event"
|
||||
|
||||
|
||||
class BaseWebhookEvent(BaseModel):
|
||||
"""Base class for all webhook events."""
|
||||
|
||||
workspace_id: str
|
||||
|
||||
|
||||
class QueueEmptyEvent(BaseWebhookEvent):
|
||||
"""Webhook event for when a queue becomes empty."""
|
||||
|
||||
type: Literal[WebhookEventType.QUEUE_EMPTY] = WebhookEventType.QUEUE_EMPTY
|
||||
queue_type: str
|
||||
session_id: str | None = None
|
||||
sender_name: str | None = None
|
||||
observer_name: str | None = None
|
||||
|
||||
|
||||
class TestEvent(BaseWebhookEvent):
|
||||
"""Webhook event for testing."""
|
||||
|
||||
type: Literal[WebhookEventType.TEST] = WebhookEventType.TEST
|
||||
|
||||
|
||||
# Union type for all webhook events
|
||||
WebhookEvent = QueueEmptyEvent | TestEvent
|
||||
|
||||
|
||||
async def publish_webhook_event(event: WebhookEvent) -> None:
|
||||
"""
|
||||
Add a webhook event to our DB queue.
|
||||
|
||||
Args:
|
||||
event: The webhook event to publish.
|
||||
"""
|
||||
try:
|
||||
payload = create_webhook_payload(
|
||||
workspace_name=event.workspace_id,
|
||||
event_type=event.type.value,
|
||||
data=event.model_dump(mode="json", exclude={"type"}),
|
||||
)
|
||||
|
||||
async with tracked_db("publish_webhook_event") as db:
|
||||
queue_item = QueueItem(
|
||||
work_unit_key=get_work_unit_key(
|
||||
"webhook", {"workspace_name": event.workspace_id}
|
||||
),
|
||||
payload=payload,
|
||||
session_id=None,
|
||||
task_type="webhook",
|
||||
)
|
||||
db.add(queue_item)
|
||||
await db.commit()
|
||||
logger.debug(
|
||||
"Published webhook event '%s' for workspace '%s'",
|
||||
event.type,
|
||||
event.workspace_id,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to publish webhook event %s",
|
||||
event.type,
|
||||
)
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
from src.crud.webhook import list_webhook_endpoints
|
||||
from src.deriver.queue_payload import WebhookPayload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def deliver_webhook(db: AsyncSession, payload: WebhookPayload) -> None:
|
||||
"""
|
||||
Deliver a single webhook event to its configured endpoints.
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
webhook_urls = await _get_webhook_urls(db, payload.workspace_name)
|
||||
if not webhook_urls:
|
||||
logger.info(
|
||||
f"No webhook endpoints for workspace {payload.workspace_name}, skipping."
|
||||
)
|
||||
return
|
||||
|
||||
event_payload = {
|
||||
"type": payload.event_type,
|
||||
"data": payload.data,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
event_json = json.dumps(
|
||||
event_payload, separators=(",", ":"), sort_keys=True
|
||||
)
|
||||
|
||||
try:
|
||||
signature = _generate_webhook_signature(event_json)
|
||||
except ValueError:
|
||||
logger.exception("Failed to generate webhook signature")
|
||||
return
|
||||
|
||||
tasks = [
|
||||
client.post(
|
||||
url=url,
|
||||
content=event_json,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Honcho-Signature": signature,
|
||||
},
|
||||
)
|
||||
for url in webhook_urls
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for url, result in zip(webhook_urls, results, strict=False):
|
||||
if isinstance(result, httpx.Response):
|
||||
if 200 <= result.status_code < 300:
|
||||
logger.info(
|
||||
f"Successfully delivered webhook {payload.event_type} to {url}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed delivery for {payload.event_type} to {url}. Status: {result.status_code}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed delivery for {payload.event_type} to {url}. Exception: {result}"
|
||||
)
|
||||
|
||||
except httpx.RequestError:
|
||||
logger.exception(f"Error sending webhook for {payload.workspace_name}.")
|
||||
except Exception:
|
||||
logger.exception("Unexpected error delivering webhook.")
|
||||
|
||||
|
||||
async def _get_webhook_urls(db: AsyncSession, workspace_name: str) -> list[str]:
|
||||
"""
|
||||
Get all webhook endpoint URLs for a workspace.
|
||||
"""
|
||||
try:
|
||||
endpoints = await list_webhook_endpoints(db, workspace_name)
|
||||
result = await db.execute(endpoints)
|
||||
return [endpoint.url for endpoint in result.scalars().all()]
|
||||
except Exception:
|
||||
logger.exception(f"Error fetching endpoints for {workspace_name}")
|
||||
return []
|
||||
|
||||
|
||||
def _generate_webhook_signature(payload: str) -> str:
|
||||
"""
|
||||
Generate HMAC-SHA256 signature for webhook payload using WEBHOOK_SECRET.
|
||||
"""
|
||||
webhook_secret = settings.WEBHOOK.SECRET
|
||||
if not webhook_secret:
|
||||
raise ValueError("WEBHOOK_SECRET not found - cannot sign webhook")
|
||||
|
||||
return hmac.new(
|
||||
webhook_secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
|
@ -18,13 +18,14 @@ import os
|
|||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, TypedDict
|
||||
from typing import Any
|
||||
|
||||
import tiktoken
|
||||
from anthropic import AsyncAnthropic
|
||||
from dotenv import load_dotenv
|
||||
from honcho import Honcho
|
||||
from honcho.session import SessionPeerConfig
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
|
|
|||
|
|
@ -377,6 +377,132 @@ def mock_mirascope_functions():
|
|||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_honcho_llm_call():
|
||||
"""Generic mock for the honcho_llm_call decorator to avoid actual LLM calls during tests"""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from src.utils.shared_models import (
|
||||
DeductiveObservation,
|
||||
ReasoningResponse,
|
||||
ReasoningResponseWithThinking,
|
||||
SemanticQueries,
|
||||
)
|
||||
|
||||
def create_mock_response(
|
||||
response_model: Any = None,
|
||||
stream: bool = False,
|
||||
return_call_response: bool = False,
|
||||
) -> Any:
|
||||
"""Create a mock response based on the expected return type"""
|
||||
if stream:
|
||||
# For streaming responses, return an async mock
|
||||
mock_stream = AsyncMock()
|
||||
mock_stream.__aiter__.return_value = iter([])
|
||||
return mock_stream
|
||||
elif response_model:
|
||||
# For structured responses, create appropriate mock objects
|
||||
if getattr(response_model, "__name__", "") == "ReasoningResponse":
|
||||
mock_response = MagicMock(spec=ReasoningResponse)
|
||||
mock_response.explicit = ["Test explicit observation"]
|
||||
mock_response.deductive = [
|
||||
DeductiveObservation(
|
||||
conclusion="Test deductive conclusion",
|
||||
premises=["Test premise 1", "Test premise 2"],
|
||||
)
|
||||
]
|
||||
# Add the _response attribute that contains thinking (used in the actual code)
|
||||
mock_response._response = MagicMock()
|
||||
mock_response._response.thinking = "Test thinking content"
|
||||
return mock_response
|
||||
elif (
|
||||
getattr(response_model, "__name__", "")
|
||||
== "ReasoningResponseWithThinking"
|
||||
):
|
||||
mock_response = MagicMock(spec=ReasoningResponseWithThinking)
|
||||
mock_response.thinking = "Test thinking content"
|
||||
mock_response.explicit = ["Test explicit observation"]
|
||||
mock_response.deductive = [
|
||||
DeductiveObservation(
|
||||
conclusion="Test deductive conclusion",
|
||||
premises=["Test premise 1", "Test premise 2"],
|
||||
)
|
||||
]
|
||||
return mock_response
|
||||
elif getattr(response_model, "__name__", "") == "SemanticQueries":
|
||||
return SemanticQueries(queries=["test query 1", "test query 2"])
|
||||
else:
|
||||
# Generic response model mock
|
||||
mock_response = MagicMock(spec=response_model)
|
||||
# Set some default attributes for common use cases
|
||||
if hasattr(mock_response, "content"):
|
||||
mock_response.content = "Test response content"
|
||||
return mock_response
|
||||
elif return_call_response:
|
||||
# For CallResponse objects, create a mock with content and usage
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = "Test response content"
|
||||
mock_response.usage = MagicMock()
|
||||
mock_response.usage.input_tokens = 100
|
||||
mock_response.usage.output_tokens = 50
|
||||
return mock_response
|
||||
else:
|
||||
# For string responses, return a simple string
|
||||
return "Test response content"
|
||||
|
||||
# Patch the honcho_llm_call decorator to prevent actual LLM calls at module level
|
||||
original_decorator = None
|
||||
try:
|
||||
import src.utils.clients
|
||||
|
||||
original_decorator = src.utils.clients.honcho_llm_call
|
||||
src.utils.clients.honcho_llm_call = lambda *args, **kwargs: lambda func: func # pyright: ignore[reportUnknownLambdaType]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def decorator_factory(*args: Any, **kwargs: Any) -> Callable[..., Any]: # pyright: ignore[reportUnusedParameter]
|
||||
"""Factory function that creates the mock decorator"""
|
||||
|
||||
def mock_llm_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
async def async_wrapper(*func_args: Any, **func_kwargs: Any) -> Any: # pyright: ignore[reportUnusedParameter]
|
||||
# Create and return appropriate mock response
|
||||
return create_mock_response(
|
||||
response_model=kwargs.get("response_model"),
|
||||
stream=kwargs.get("stream", False),
|
||||
return_call_response=kwargs.get("return_call_response", False),
|
||||
)
|
||||
|
||||
def sync_wrapper(*func_args: Any, **func_kwargs: Any) -> Any: # pyright: ignore[reportUnusedParameter]
|
||||
# Create and return appropriate mock response
|
||||
return create_mock_response(
|
||||
response_model=kwargs.get("response_model"),
|
||||
stream=kwargs.get("stream", False),
|
||||
return_call_response=kwargs.get("return_call_response", False),
|
||||
)
|
||||
|
||||
# Check if the original function is async
|
||||
import inspect
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
else:
|
||||
return sync_wrapper
|
||||
|
||||
return mock_llm_decorator
|
||||
|
||||
with patch("src.utils.clients.honcho_llm_call", side_effect=decorator_factory):
|
||||
yield decorator_factory
|
||||
|
||||
# Restore the original decorator
|
||||
if original_decorator:
|
||||
try:
|
||||
import src.utils.clients
|
||||
|
||||
src.utils.clients.honcho_llm_call = original_decorator
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_tracked_db(db_session: AsyncSession):
|
||||
"""Mock tracked_db to use the test database session"""
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue