diff --git a/.env.template b/.env.template
index c7590dfc..692db30e 100644
--- a/.env.template
+++ b/.env.template
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b01e7715..0814ebff 100644
--- a/CHANGELOG.md
+++ b/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
diff --git a/CLAUDE.md b/CLAUDE.md
index d82b34d3..803c285f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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)
diff --git a/README.md b/README.md
index 02db4c1b..3d82a0f7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 🫡 Honcho
-
+
[](https://discord.gg/plasticlabs)
[](https://arxiv.org/abs/2310.06983)

diff --git a/config.toml.example b/config.toml.example
index aa67f8df..b1036d85 100644
--- a/config.toml.example
+++ b/config.toml.example
@@ -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]
diff --git a/docs/changelog/compatibility-guide.mdx b/docs/changelog/compatibility-guide.mdx
index 2e11aa23..6f897c57 100644
--- a/docs/changelog/compatibility-guide.mdx
+++ b/docs/changelog/compatibility-guide.mdx
@@ -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)
+
+
+
+ **Compatible Version:** v1.3.0
+
+ Install with:
+ ```bash
+ npm install @honcho-ai/sdk@1.3.0
+ ```
+
+
+ **Compatible Version:** v1.3.0
+
+ Install with:
+ ```bash
+ pip install honcho-ai==1.3.0
+ ```
+
+
+
+### Honcho API v2.1.2
@@ -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 |
diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx
index 7676a917..225563a0 100644
--- a/docs/changelog/introduction.mdx
+++ b/docs/changelog/introduction.mdx
@@ -27,7 +27,31 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
-
+
+ ### 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
+
+
+
+
### Fixed
- Summarizer module to ignore empty summaries and pass appropriate one to get_context
diff --git a/docs/docs.json b/docs/docs.json
index 44489566..cd22de07 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -19,7 +19,7 @@
"navigation": {
"versions": [
{
- "version": "v2.1.2",
+ "version": "v2.2.0",
"api": {
"openapi": [
"openapi.documented.yml"
diff --git a/docs/v1/guides/discord.mdx b/docs/v1/guides/discord.mdx
index f9bfdb9c..938a7365 100644
--- a/docs/v1/guides/discord.mdx
+++ b/docs/v1/guides/discord.mdx
@@ -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)
diff --git a/docs/v1/openapi.json b/docs/v1/openapi.json
index bc67bae5..4d42e1e2 100644
--- a/docs/v1/openapi.json
+++ b/docs/v1/openapi.json
@@ -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": {
diff --git a/docs/v2/contributing/configuration.mdx b/docs/v2/contributing/configuration.mdx
index 53814fc9..26a4b9b2 100644
--- a/docs/v2/contributing/configuration.mdx
+++ b/docs/v2/contributing/configuration.mdx
@@ -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
diff --git a/docs/v2/contributing/self-hosting.mdx b/docs/v2/contributing/self-hosting.mdx
index 93fe9dbc..3e5e1e42 100644
--- a/docs/v2/contributing/self-hosting.mdx
+++ b/docs/v2/contributing/self-hosting.mdx
@@ -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
diff --git a/docs/v2/documentation/core-concepts/architecture.mdx b/docs/v2/documentation/core-concepts/architecture.mdx
index f48e9b98..57b67871 100644
--- a/docs/v2/documentation/core-concepts/architecture.mdx
+++ b/docs/v2/documentation/core-concepts/architecture.mdx
@@ -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.
```
diff --git a/docs/v2/documentation/core-concepts/configuration.mdx b/docs/v2/documentation/core-concepts/configuration.mdx
new file mode 100644
index 00000000..ff57dc8e
--- /dev/null
+++ b/docs/v2/documentation/core-concepts/configuration.mdx
@@ -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.
+
+
+```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 } });
+})();
+```
+
+
+### 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.
+
+
+```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 } });
+})();
+```
+
+
+### 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.
+
+
+
+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.
+
+
+```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);
+})();
+```
+
diff --git a/docs/v2/documentation/core-concepts/features.mdx b/docs/v2/documentation/core-concepts/features.mdx
index 831b724a..cc1d402e 100644
--- a/docs/v2/documentation/core-concepts/features.mdx
+++ b/docs/v2/documentation/core-concepts/features.mdx
@@ -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.
diff --git a/docs/v2/documentation/core-concepts/glossary.mdx b/docs/v2/documentation/core-concepts/glossary.mdx
index 5ef7fa33..75707f75 100644
--- a/docs/v2/documentation/core-concepts/glossary.mdx
+++ b/docs/v2/documentation/core-concepts/glossary.mdx
@@ -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.
\ No newline at end of file
+uncertainty.
diff --git a/docs/v2/documentation/core-concepts/summarizer.mdx b/docs/v2/documentation/core-concepts/summarizer.mdx
index 75fbabd8..0e14d1ac 100644
--- a/docs/v2/documentation/core-concepts/summarizer.mdx
+++ b/docs/v2/documentation/core-concepts/summarizer.mdx
@@ -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.
\ No newline at end of file
+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.
diff --git a/docs/v2/documentation/introduction/quickstart.mdx b/docs/v2/documentation/introduction/quickstart.mdx
index d6e76ac8..4418d29c 100644
--- a/docs/v2/documentation/introduction/quickstart.mdx
+++ b/docs/v2/documentation/introduction/quickstart.mdx
@@ -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."
+})();
```
diff --git a/docs/v2/documentation/reference/sdk.mdx b/docs/v2/documentation/reference/sdk.mdx
index 63711ccf..135cfd1c 100644
--- a/docs/v2/documentation/reference/sdk.mdx
+++ b/docs/v2/documentation/reference/sdk.mdx
@@ -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 } }
});
```
diff --git a/docs/v2/guides/dialectic-endpoint.mdx b/docs/v2/guides/dialectic-endpoint.mdx
index c9e89dc2..6c8d40d7 100644
--- a/docs/v2/guides/dialectic-endpoint.mdx
+++ b/docs/v2/guides/dialectic-endpoint.mdx
@@ -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();
diff --git a/docs/v2/guides/discord.mdx b/docs/v2/guides/discord.mdx
index 737df751..a87826ac 100644
--- a/docs/v2/guides/discord.mdx
+++ b/docs/v2/guides/discord.mdx
@@ -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,
)
diff --git a/docs/v2/guides/file-uploads.mdx b/docs/v2/guides/file-uploads.mdx
index 85473353..c138aff3 100644
--- a/docs/v2/guides/file-uploads.mdx
+++ b/docs/v2/guides/file-uploads.mdx
@@ -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
-
```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`);
-```
-
-
-### Upload to Peer's Global Representation
-
-
-```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`);
+})();
```
@@ -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);
+})();
```
@@ -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);
+})();
```
diff --git a/docs/v2/guides/get-context.mdx b/docs/v2/guides/get-context.mdx
index fe86ad2d..3afed4c7 100644
--- a/docs/v2/guides/get-context.mdx
+++ b/docs/v2/guides/get-context.mdx
@@ -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();
+})();
```
@@ -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 });
+})();
```
@@ -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
+ });
+})();
```
@@ -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!"}
+ // ]
+})();
```
@@ -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);
+})();
```
@@ -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)
+ ]);
+})();
```
@@ -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();
+})();
```
@@ -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`);
+})();
```
@@ -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
+})();
```
@@ -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 });
+})();
```
@@ -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
+})();
```
@@ -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
+ }
+})();
```
diff --git a/docs/v2/guides/search.mdx b/docs/v2/guides/search.mdx
index 82a300a5..943b96f9 100644
--- a/docs/v2/guides/search.mdx
+++ b/docs/v2/guides/search.mdx
@@ -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}`);
+ }
+})();
```
@@ -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}`);
+ }
+})();
```
@@ -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}`);
+ }
+})();
```
-## 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.
```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 });
+})();
```
-### 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.
```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 } });
+})();
```
-### Search with Context Building
+### Filter results by time range
```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" } } });
+})();
```
-## Best Practices
+### Filter results by metadata
+
+
+```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" } } });
+})();
+```
+
+
+### 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");
+ }
+})();
```
diff --git a/docs/v2/guides/streaming-response.mdx b/docs/v2/guides/streaming-response.mdx
index ef4d0594..3c29fda8 100644
--- a/docs/v2/guides/streaming-response.mdx
+++ b/docs/v2/guides/streaming-response.mdx
@@ -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")
+ ]);
+})();
```
@@ -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
+ }
+})();
```
@@ -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();
+})();
```
diff --git a/docs/v2/guides/telegram.mdx b/docs/v2/guides/telegram.mdx
index 160eb416..5c79c632 100644
--- a/docs/v2/guides/telegram.mdx
+++ b/docs/v2/guides/telegram.mdx
@@ -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,
)
diff --git a/docs/v2/guides/using-filters.mdx b/docs/v2/guides/using-filters.mdx
index f22fa0b2..6c5143d0 100644
--- a/docs/v2/guides/using-filters.mdx
+++ b/docs/v2/guides/using-filters.mdx
@@ -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"
+ }
+ });
+})();
```
@@ -74,7 +76,7 @@ Use AND to require all conditions to be true:
```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" } }
+ ]
+ }
+ });
+})();
```
@@ -101,7 +105,7 @@ Use OR to match any of the specified conditions:
```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 } }
+ ]
+ }
+ });
+})();
```
@@ -149,14 +155,14 @@ Use NOT to exclude specific conditions:
```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" } }
+ ]
+ }
+ });
+})();
```
@@ -191,7 +199,7 @@ Create sophisticated queries by combining different logical operators:
```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 } }
+ ]
+ }
]
}
- ]
- }
-});
+ });
+})();
```
@@ -239,12 +249,12 @@ Use comparison operators for range queries and advanced matching:
```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 }
+ }
+ }
+ });
+})();
```
@@ -295,19 +307,19 @@ const sessions = await honcho.getSessions({
```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" }
+ }
+ }
+ });
+})();
```
@@ -351,7 +365,7 @@ Metadata filtering is particularly powerful in Honcho, supporting nested conditi
```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"
+ }
+ }
+ }
+ });
+})();
```
@@ -403,7 +419,7 @@ If you want to do advanced queries like these, make sure not to create metadata
```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" } }
+ ]
+ }
+ });
+})();
```
@@ -453,17 +471,17 @@ Use wildcards (*) to match any value for a field:
```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
+ }
+ }
+ });
+})();
```
@@ -505,12 +525,12 @@ const sessions = await honcho.getSessions({
```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"] }
+ }
+ }
+ });
+})();
```
@@ -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 } } }
+ ]
+ }
+ });
+})();
```
@@ -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}`);
-}
+})();
```
diff --git a/docs/v2/openapi.documented.yml b/docs/v2/openapi.documented.yml
index dbe5e636..ebc1df20 100644
--- a/docs/v2/openapi.documented.yml
+++ b/docs/v2/openapi.documented.yml
@@ -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
\ No newline at end of file
+ scheme: bearer
diff --git a/mcp/.gitignore b/mcp/.gitignore
index 83036c9e..51fb8691 100644
--- a/mcp/.gitignore
+++ b/mcp/.gitignore
@@ -24,4 +24,4 @@ dist/
Thumbs.db
# Logs
-*.log
\ No newline at end of file
+*.log
diff --git a/mcp/README.md b/mcp/README.md
index 1496538e..26986fe4 100644
--- a/mcp/README.md
+++ b/mcp/README.md
@@ -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 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: )
- `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
\ No newline at end of file
+- **Network errors**: Verify the worker is deployed and accessible
diff --git a/mcp/instructions.md b/mcp/instructions.md
index e9432c1d..8e5db2bf 100644
--- a/mcp/instructions.md
+++ b/mcp/instructions.md
@@ -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**
\ No newline at end of file
+6. **The system maintains context automatically between sessions**
diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json
index a364d373..0cac208a 100644
--- a/mcp/tsconfig.json
+++ b/mcp/tsconfig.json
@@ -11,4 +11,4 @@
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
-}
\ No newline at end of file
+}
diff --git a/mcp/worker.ts b/mcp/worker.ts
index a12dc9b7..74dcc5bd 100644
--- a/mcp/worker.ts
+++ b/mcp/worker.ts
@@ -1341,4 +1341,4 @@ export default {
return createErrorResponse(requestData.id ?? null, -32603, errorMessage);
}
},
-};
\ No newline at end of file
+};
diff --git a/mcp/wrangler.toml b/mcp/wrangler.toml
index 8a443964..219a261d 100644
--- a/mcp/wrangler.toml
+++ b/mcp/wrangler.toml
@@ -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
\ No newline at end of file
+enabled = true
diff --git a/migrations/versions/05486ce795d5_make_session_name_required_on_messages.py b/migrations/versions/05486ce795d5_make_session_name_required_on_messages.py
index 59ba15ed..6f239da9 100644
--- a/migrations/versions/05486ce795d5_make_session_name_required_on_messages.py
+++ b/migrations/versions/05486ce795d5_make_session_name_required_on_messages.py
@@ -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
""")
diff --git a/migrations/versions/88b0fb10906f_add_webhooks_table.py b/migrations/versions/88b0fb10906f_add_webhooks_table.py
new file mode 100644
index 00000000..da607938
--- /dev/null
+++ b/migrations/versions/88b0fb10906f_add_webhooks_table.py
@@ -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)
diff --git a/pyproject.toml b/pyproject.toml
index e63045a0..2766f5fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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",
diff --git a/scripts/generate_jwt_secret.py b/scripts/generate_jwt_secret.py
index 097f7d49..449d4cc7 100755
--- a/scripts/generate_jwt_secret.py
+++ b/scripts/generate_jwt_secret.py
@@ -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__":
diff --git a/scripts/update_version.py b/scripts/update_version.py
index 986a1e93..a07a007e 100755
--- a/scripts/update_version.py
+++ b/scripts/update_version.py
@@ -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:
diff --git a/sdks/python/CHANGELOG.md b/sdks/python/CHANGELOG.md
index 1e8a03c3..6e29a94b 100644
--- a/sdks/python/CHANGELOG.md
+++ b/sdks/python/CHANGELOG.md
@@ -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
diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml
index 3e329952..2fd3cd1f 100644
--- a/sdks/python/pyproject.toml
+++ b/sdks/python/pyproject.toml
@@ -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",
]
diff --git a/sdks/python/src/honcho/async_client/client.py b/sdks/python/src/honcho/async_client/client.py
index 16b52b9d..e1c1e788 100644
--- a/sdks/python/src/honcho/async_client/client.py
+++ b/sdks/python/src/honcho/async_client/client.py
@@ -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(
diff --git a/sdks/python/src/honcho/async_client/peer.py b/sdks/python/src/honcho/async_client/peer.py
index dd780480..b3c0eeb8 100644
--- a/sdks/python/src/honcho/async_client/peer.py
+++ b/sdks/python/src/honcho/async_client/peer.py
@@ -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:
"""
diff --git a/sdks/python/src/honcho/async_client/session.py b/sdks/python/src/honcho/async_client/session.py
index 4db58b1f..da95859c 100644
--- a/sdks/python/src/honcho/async_client/session.py
+++ b/sdks/python/src/honcho/async_client/session.py
@@ -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.
diff --git a/sdks/python/src/honcho/client.py b/sdks/python/src/honcho/client.py
index 8e299a20..78d799e9 100644
--- a/sdks/python/src/honcho/client.py
+++ b/sdks/python/src/honcho/client.py
@@ -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(
diff --git a/sdks/python/src/honcho/peer.py b/sdks/python/src/honcho/peer.py
index 1ce44cc3..58f40dea 100644
--- a/sdks/python/src/honcho/peer.py
+++ b/sdks/python/src/honcho/peer.py
@@ -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:
"""
diff --git a/sdks/python/src/honcho/py.typed b/sdks/python/src/honcho/py.typed
deleted file mode 100644
index e69de29b..00000000
diff --git a/sdks/python/src/honcho/session.py b/sdks/python/src/honcho/session.py
index e3ea7e6c..29216ce7 100644
--- a/sdks/python/src/honcho/session.py
+++ b/sdks/python/src/honcho/session.py
@@ -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.
diff --git a/sdks/python/src/honcho/session_context.py b/sdks/python/src/honcho/session_context.py
index 45ad4c2c..87480802 100644
--- a/sdks/python/src/honcho/session_context.py
+++ b/sdks/python/src/honcho/session_context.py
@@ -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"{self.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"{self.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})"
diff --git a/sdks/python/uv.lock b/sdks/python/uv.lock
index dcd4a269..c8f8e68e 100644
--- a/sdks/python/uv.lock
+++ b/sdks/python/uv.lock
@@ -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]]
diff --git a/sdks/typescript/CHANGELOG.md b/sdks/typescript/CHANGELOG.md
index 2fbb20eb..ad57256e 100644
--- a/sdks/typescript/CHANGELOG.md
+++ b/sdks/typescript/CHANGELOG.md
@@ -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
diff --git a/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts b/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts
index 658d6787..1e8e369f 100644
--- a/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts
+++ b/sdks/typescript/__tests__/__mocks__/@honcho-ai/core.ts
@@ -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) {
diff --git a/sdks/typescript/__tests__/client.test.ts b/sdks/typescript/__tests__/client.test.ts
index 1eb996c4..1a571680 100644
--- a/sdks/typescript/__tests__/client.test.ts
+++ b/sdks/typescript/__tests__/client.test.ts
@@ -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();
});
});
});
diff --git a/sdks/typescript/__tests__/integration.test.ts b/sdks/typescript/__tests__/integration.test.ts
index 7148244a..1c64a5e4 100644
--- a/sdks/typescript/__tests__/integration.test.ts
+++ b/sdks/typescript/__tests__/integration.test.ts
@@ -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: 'Friendly greeting' },
+ { role: 'user', content: 'Hello', name: 'user' },
+ { role: 'assistant', content: 'Hi there!', name: 'assistant' },
]);
expect(anthropicFormat).toEqual([
- { role: 'user', content: 'Hello' },
+ { role: 'user', content: 'Friendly greeting' },
+ { 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',
});
diff --git a/sdks/typescript/__tests__/pagination.test.ts b/sdks/typescript/__tests__/pagination.test.ts
index 4be9e7d0..6986c4d9 100644
--- a/sdks/typescript/__tests__/pagination.test.ts
+++ b/sdks/typescript/__tests__/pagination.test.ts
@@ -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 () => {
diff --git a/sdks/typescript/__tests__/peer.test.ts b/sdks/typescript/__tests__/peer.test.ts
index e6779d2e..9cfde7ea 100644
--- a/sdks/typescript/__tests__/peer.test.ts
+++ b/sdks/typescript/__tests__/peer.test.ts
@@ -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();
});
});
});
diff --git a/sdks/typescript/__tests__/session.test.ts b/sdks/typescript/__tests__/session.test.ts
index 8f735f19..c95f4445 100644
--- a/sdks/typescript/__tests__/session.test.ts
+++ b/sdks/typescript/__tests__/session.test.ts
@@ -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();
});
});
});
diff --git a/sdks/typescript/__tests__/session_context.test.ts b/sdks/typescript/__tests__/session_context.test.ts
index 91883212..2af28b4d 100644
--- a/sdks/typescript/__tests__/session_context.test.ts
+++ b/sdks/typescript/__tests__/session_context.test.ts
@@ -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: 'This is a 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: 'This is a 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' },
]);
});
});
diff --git a/sdks/typescript/bun.lock b/sdks/typescript/bun.lock
index 25f1e1ac..cccce038 100644
--- a/sdks/typescript/bun.lock
+++ b/sdks/typescript/bun.lock
@@ -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=="],
}
}
diff --git a/sdks/typescript/examples/get_context.ts b/sdks/typescript/examples/get_context.ts
index e0c22e4f..453ba85e 100644
--- a/sdks/typescript/examples/get_context.ts
+++ b/sdks/typescript/examples/get_context.ts
@@ -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(
diff --git a/sdks/typescript/examples/get_representation.ts b/sdks/typescript/examples/get_representation.ts
index 8746f908..3f10fb57 100644
--- a/sdks/typescript/examples/get_representation.ts
+++ b/sdks/typescript/examples/get_representation.ts
@@ -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(
diff --git a/sdks/typescript/examples/search.ts b/sdks/typescript/examples/search.ts
index ab6818e4..372bf71b 100644
--- a/sdks/typescript/examples/search.ts
+++ b/sdks/typescript/examples/search.ts
@@ -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})`);
}
diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json
index 884bbea8..032c227a 100644
--- a/sdks/typescript/package.json
+++ b/sdks/typescript/package.json
@@ -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 ",
"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",
diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts
index 7e83199f..058eade6 100644
--- a/sdks/typescript/src/client.ts
+++ b/sdks/typescript/src/client.ts
@@ -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
+ /**
+ * 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 {
+ 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
- defaultQuery?: Record
- }) {
+ 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 }): 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 {
+ 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> {
+ async getPeers(filters?: Filters): Promise> {
+ 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 }): 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 {
+ 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> {
+ async getSessions(filters?: Filters): Promise> {
+ 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> {
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): Promise {
- await this._client.workspaces.update(this.workspaceId, { metadata })
+ async setMetadata(metadata: WorkspaceMetadata): Promise {
+ 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 {
- const workspacesPage = await this._client.workspaces.list({ filter })
+ async getWorkspaces(filters?: Filters): Promise {
+ 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> {
- 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 {
+ 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
+ sessions?: Record
}> {
- 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
+ sessions?: Record
}> {
- 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}')`
+ }
}
diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts
index f47768f0..f4f1da9d 100644
--- a/sdks/typescript/src/index.ts
+++ b/sdks/typescript/src/index.ts
@@ -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'
diff --git a/sdks/typescript/src/peer.ts b/sdks/typescript/src/peer.ts
index b591517e..4769dbb5 100644
--- a/sdks/typescript/src/peer.ts
+++ b/sdks/typescript/src/peer.ts
@@ -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) {
+ 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 {
- 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> {
- const sessionsPage = await this._honcho[
- '_client'
- ].workspaces.peers.sessions.list(this._honcho.workspaceId, this.id, {
- filter,
- })
+ async getSessions(filters?: Filters | null): Promise> {
+ 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 }): any {
+ message(
+ content: string,
+ options?: { metadata?: Record }
+ ): 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> {
- 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): Promise {
- 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> {
+ 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): Promise {
+ 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> {
- 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 {
+ 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 {
- // 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}')`
}
}
diff --git a/sdks/typescript/src/session.ts b/sdks/typescript/src/session.ts
index b65c970f..7d7c8262 100644
--- a/sdks/typescript/src/session.ts
+++ b/sdks/typescript/src/session.ts
@@ -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) {
+ 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: List of peer IDs and/or Peer objects
+ * - [string | Peer, SessionPeerConfig]: Single peer with session config
+ * - Array: 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, SessionPeerConfig]
- | Array<[string | Peer, SessionPeerConfig]>
- | Array
- ): Promise {
+ async addPeers(peers: PeerAddition): Promise {
+ const validatedPeers = PeerAdditionSchema.parse(peers)
const peerDict: Record = {}
- 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: List of peer IDs and/or Peer objects
+ * - [string | Peer, SessionPeerConfig]: Single peer with session config
+ * - Array: Mixed list
+ * of peers and peer+config combinations
*/
- async setPeers(
- peers:
- | string
- | Peer
- | Array
- | [string | Peer, SessionPeerConfig]
- | Array<[string | Peer, SessionPeerConfig]>
- | Array
- ): Promise {
+ async setPeers(peers: PeerAddition): Promise {
+ const validatedPeers = PeerAdditionSchema.parse(peers)
const peerDict: Record = {}
- 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: List of peer IDs and/or Peer objects
*/
- async removePeers(
- peers: string | Peer | Array
- ): Promise {
- 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 {
+ 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 {
- 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 {
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 {
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 {
- 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 {
+ 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
- }): Promise> {
- const messagesPage = await (
- this._honcho['_client'] as any
- ).workspaces.sessions.messages.list(
- this._honcho.workspaceId,
+ async getMessages(filters?: Filters): Promise> {
+ 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> {
- 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): Promise {
- 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 {
- 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> {
- 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 {
+ 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 {
- // 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 {
+ 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> {
- 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}')`
}
}
diff --git a/sdks/typescript/src/session_context.ts b/sdks/typescript/src/session_context.ts
index 08992164..958c538c 100644
--- a/sdks/typescript/src/session_context.ts
+++ b/sdks/typescript/src/session_context.ts
@@ -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: `${this.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: `${this.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})`
}
}
diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts
new file mode 100644
index 00000000..c9e40535
--- /dev/null
+++ b/sdks/typescript/src/validation.ts
@@ -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
+export type PeerMetadata = z.infer
+export type PeerConfig = z.infer
+export type SessionMetadata = z.infer
+export type SessionConfig = z.infer
+export type SessionPeerConfig = z.infer
+export type MessageCreate = z.infer
+export type Filters = z.infer
+export type ChatQuery = z.infer
+export type ContextParams = z.infer
+export type DeriverStatusOptions = z.infer
+export type FileUpload = z.infer
+export type WorkingRepParams = z.infer
+export type PeerAddition = z.infer
+export type PeerRemoval = z.infer
+export type MessageAddition = z.infer
+export type WorkspaceMetadata = z.infer
+export type Limit = z.infer
diff --git a/sdks/typescript/tsconfig.json b/sdks/typescript/tsconfig.json
index 91a7c411..afc11a42 100644
--- a/sdks/typescript/tsconfig.json
+++ b/sdks/typescript/tsconfig.json
@@ -15,11 +15,11 @@
]
},
"include": [
- "src/**/*",
+ "src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
- "baseUrl": ".",
+ "baseUrl": "."
}
diff --git a/src/config.py b/src/config.py
index 1cc8f453..3f7ea425 100644
--- a/src/config.py
+++ b/src/config.py
@@ -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:
diff --git a/src/crud/__init__.py b/src/crud/__init__.py
index 4c7be31f..3dc4eb51 100644
--- a/src/crud/__init__.py
+++ b/src/crud/__init__.py
@@ -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",
diff --git a/src/crud/deriver.py b/src/crud/deriver.py
index 811b218c..6066b878 100644
--- a/src/crud/deriver.py
+++ b/src/crud/deriver.py
@@ -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)
diff --git a/src/crud/message.py b/src/crud/message.py
index c4de9c3e..4b37972b 100644
--- a/src/crud/message.py
+++ b/src/crud/message.py
@@ -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
diff --git a/src/crud/webhook.py b/src/crud/webhook.py
new file mode 100644
index 00000000..d2216cb2
--- /dev/null
+++ b/src/crud/webhook.py
@@ -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")
diff --git a/src/crud/workspace.py b/src/crud/workspace.py
index f4f4287e..0f3e63b4 100644
--- a/src/crud/workspace.py
+++ b/src/crud/workspace.py
@@ -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:
diff --git a/src/deriver/consumer.py b/src/deriver/consumer.py
index 8d166ff0..a62b0481 100644
--- a/src/deriver/consumer.py
+++ b/src/deriver/consumer.py
@@ -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)
diff --git a/src/deriver/deriver.py b/src/deriver/deriver.py
index fc9a1f76..649a53bd 100644
--- a/src/deriver/deriver.py
+++ b/src/deriver/deriver.py
@@ -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,
diff --git a/src/deriver/enqueue.py b/src/deriver/enqueue.py
index 3fa4343e..de813386 100644
--- a/src/deriver/enqueue.py
+++ b/src/deriver/enqueue.py
@@ -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",
}
diff --git a/src/deriver/queue_manager.py b/src/deriver/queue_manager.py
index 9cf83e7c..5c29378d 100644
--- a/src/deriver/queue_manager.py
+++ b/src/deriver/queue_manager.py
@@ -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()
diff --git a/src/deriver/queue_payload.py b/src/deriver/queue_payload.py
index 0beaa888..1eb85332 100644
--- a/src/deriver/queue_payload.py
+++ b/src/deriver/queue_payload.py
@@ -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
diff --git a/src/deriver/utils.py b/src/deriver/utils.py
new file mode 100644
index 00000000..d29aa8cd
--- /dev/null
+++ b/src/deriver/utils.py
@@ -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}")
diff --git a/src/main.py b/src/main.py
index ce2b0274..44b2fa39 100644
--- a/src/main.py
+++ b/src/main.py
@@ -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
diff --git a/src/models.py b/src/models.py
index e3065fb1..cf885ce5 100644
--- a/src/models.py
+++ b/src/models.py
@@ -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
diff --git a/src/routers/messages.py b/src/routers/messages.py
index 01075c31..3c59dbb3 100644
--- a/src/routers/messages.py
+++ b/src/routers/messages.py
@@ -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
diff --git a/src/routers/peers.py b/src/routers/peers.py
index c8098ef4..eabde2ee 100644
--- a/src/routers/peers.py
+++ b/src/routers/peers.py
@@ -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)
diff --git a/src/routers/sessions.py b/src/routers/sessions.py
index 93a1ee08..f93b7f04 100644
--- a/src/routers/sessions.py
+++ b/src/routers/sessions.py
@@ -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)
diff --git a/src/routers/webhooks.py b/src/routers/webhooks.py
new file mode 100644
index 00000000..c6ae2017
--- /dev/null
+++ b/src/routers/webhooks.py
@@ -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)
diff --git a/src/routers/workspaces.py b/src/routers/workspaces.py
index f9a5d622..40bebbb2 100644
--- a/src/routers/workspaces.py
+++ b/src/routers/workspaces.py
@@ -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(
diff --git a/src/schemas.py b/src/schemas.py
index d1f742cf..60a5a5bd 100644
--- a/src/schemas.py
+++ b/src/schemas.py
@@ -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
diff --git a/src/security.py b/src/security.py
index 80c294ac..bc91e39b 100644
--- a/src/security.py
+++ b/src/security.py
@@ -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:
diff --git a/src/utils/embedding_store.py b/src/utils/embedding_store.py
index e8afa47e..84d41759 100644
--- a/src/utils/embedding_store.py
+++ b/src/utils/embedding_store.py
@@ -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(
diff --git a/src/utils/search.py b/src/utils/search.py
new file mode 100644
index 00000000..a79d2cac
--- /dev/null
+++ b/src/utils/search.py
@@ -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
diff --git a/src/utils/shared_models.py b/src/utils/shared_models.py
index c6009f66..76d736b7 100644
--- a/src/utils/shared_models.py
+++ b/src/utils/shared_models.py
@@ -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):
diff --git a/src/utils/summarizer.py b/src/utils/summarizer.py
index e9c0a61f..e90547ae 100644
--- a/src/utils/summarizer.py
+++ b/src/utils/summarizer.py
@@ -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
diff --git a/src/webhooks/README.md b/src/webhooks/README.md
new file mode 100644
index 00000000..4c85d010
--- /dev/null
+++ b/src/webhooks/README.md
@@ -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.
diff --git a/src/webhooks/events.py b/src/webhooks/events.py
new file mode 100644
index 00000000..236521ae
--- /dev/null
+++ b/src/webhooks/events.py
@@ -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,
+ )
diff --git a/src/webhooks/webhook_delivery.py b/src/webhooks/webhook_delivery.py
new file mode 100644
index 00000000..c9a1261d
--- /dev/null
+++ b/src/webhooks/webhook_delivery.py
@@ -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()
diff --git a/tests/bench/run_tests.py b/tests/bench/run_tests.py
index 2b698ece..c7510e61 100644
--- a/tests/bench/run_tests.py
+++ b/tests/bench/run_tests.py
@@ -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()
diff --git a/tests/conftest.py b/tests/conftest.py
index a12f55fa..78a5213d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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"""
diff --git a/tests/crud/test_session.py b/tests/crud/test_session.py
new file mode 100644
index 00000000..8b4b5795
--- /dev/null
+++ b/tests/crud/test_session.py
@@ -0,0 +1,134 @@
+import pytest
+from nanoid import generate as generate_nanoid
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src import crud, models, schemas
+from src.exceptions import ResourceNotFoundException
+
+
+class TestSessionCRUD:
+ """Test suite for session CRUD operations"""
+
+ @pytest.mark.asyncio
+ async def test_get_session_peer_configuration(
+ self,
+ db_session: AsyncSession,
+ sample_data: tuple[models.Workspace, models.Peer],
+ ):
+ """Test retrieving peer configuration data from session"""
+ test_workspace, test_peer = sample_data
+
+ # Create another peer
+ peer2 = models.Peer(
+ name=str(generate_nanoid()), workspace_name=test_workspace.name
+ )
+ db_session.add(peer2)
+ await db_session.flush()
+
+ # Create session
+ test_session = models.Session(
+ name=str(generate_nanoid()), workspace_name=test_workspace.name
+ )
+ db_session.add(test_session)
+ await db_session.flush()
+
+ # Add peers to session with different configurations
+ peer_configs = {
+ test_peer.name: schemas.SessionPeerConfig(
+ observe_others=True, observe_me=False
+ ),
+ peer2.name: schemas.SessionPeerConfig(
+ observe_others=False, observe_me=True
+ ),
+ }
+
+ # Set up peers in session
+ await crud.set_peers_for_session(
+ db_session,
+ workspace_name=test_workspace.name,
+ session_name=test_session.name,
+ peer_names=peer_configs,
+ )
+
+ # Test the get_session_peer_configuration function
+ stmt = await crud.get_session_peer_configuration(
+ workspace_name=test_workspace.name,
+ session_name=test_session.name,
+ )
+ result = await db_session.execute(stmt)
+ configurations = result.all()
+
+ # Should return configurations for all active peers
+ assert len(configurations) == 2
+
+ # Verify the structure of returned data
+ for peer_name, peer_config, session_peer_config, is_active in configurations:
+ assert isinstance(peer_name, str)
+ assert isinstance(peer_config, dict) or peer_config is None
+ assert isinstance(session_peer_config, dict)
+ assert isinstance(is_active, bool)
+
+ # Check that session_peer_config matches what we set
+ expected_config = peer_configs[peer_name]
+ assert (
+ session_peer_config["observe_others"] == expected_config.observe_others
+ )
+ assert session_peer_config["observe_me"] == expected_config.observe_me
+
+ @pytest.mark.asyncio
+ async def test_get_session_not_found(self, db_session: AsyncSession):
+ """Test get_session with non-existent session raises ResourceNotFoundException"""
+ with pytest.raises(ResourceNotFoundException):
+ await crud.get_session(db_session, "nonexistent", "nonexistent_workspace")
+
+ @pytest.mark.asyncio
+ async def test_get_peer_config_not_found(
+ self,
+ db_session: AsyncSession,
+ sample_data: tuple[models.Workspace, models.Peer],
+ ):
+ """Test get_peer_config with non-existent peer raises ResourceNotFoundException"""
+ test_workspace, _test_peer = sample_data
+
+ # Create session
+ test_session = models.Session(
+ name=str(generate_nanoid()), workspace_name=test_workspace.name
+ )
+ db_session.add(test_session)
+ await db_session.flush()
+
+ with pytest.raises(ResourceNotFoundException):
+ await crud.get_peer_config(
+ db_session, test_workspace.name, test_session.name, "nonexistent_peer"
+ )
+
+ @pytest.mark.asyncio
+ async def test_clone_session_not_found(self, db_session: AsyncSession):
+ """Test clone_session with non-existent session raises ResourceNotFoundException"""
+ with pytest.raises(ResourceNotFoundException):
+ await crud.clone_session(db_session, "workspace", "nonexistent_session")
+
+ @pytest.mark.asyncio
+ async def test_clone_session_invalid_cutoff_message(
+ self,
+ db_session: AsyncSession,
+ sample_data: tuple[models.Workspace, models.Peer],
+ ):
+ """Test clone_session with invalid cutoff message raises ValueError"""
+ test_workspace, _test_peer = sample_data
+
+ # Create session
+ test_session = models.Session(
+ name=str(generate_nanoid()), workspace_name=test_workspace.name
+ )
+ db_session.add(test_session)
+ await db_session.flush()
+
+ # Try to clone with invalid cutoff message ID
+ with pytest.raises(
+ ValueError,
+ match="Message not found or doesn't belong to the specified session",
+ ):
+ await crud.clone_session(
+ db_session, test_workspace.name, test_session.name, "invalid_message_id"
+ )
diff --git a/tests/deriver/README.md b/tests/deriver/README.md
new file mode 100644
index 00000000..a814e335
--- /dev/null
+++ b/tests/deriver/README.md
@@ -0,0 +1,72 @@
+# Deriver Testing
+
+This directory contains tests for the deriver system, which handles background processing of messages to extract insights and update working representations.
+
+## Structure
+
+- `conftest.py` - Shared fixtures for deriver testing
+- `test_queue_operations.py` - Tests for basic queue operations
+- `test_deriver_processing.py` - Tests for deriver processing logic
+- `test_queue_processing.py` - Tests for queue manager and work unit processing
+
+## Key Fixtures
+
+### Database Fixtures
+
+- `sample_session_with_peers` - Creates a session with multiple peers having different observation configurations
+- `sample_messages` - Creates sample messages for testing
+- `sample_queue_items` - Creates queue items with various payload types (representation, summary)
+
+### Queue Fixtures
+
+- `create_queue_payload` - Helper to create queue payloads for testing
+- `add_queue_items` - Helper to add queue items to the database
+- `create_active_queue_session` - Helper to create active queue sessions for work unit tracking
+
+### Mocking Fixtures
+
+- `mock_deriver_process` - Mocks the deriver process_message method
+- `mock_critical_analysis_call` - Mocks the critical analysis LLM call
+- `mock_queue_manager` - Mocks the queue manager for testing
+- `mock_embedding_store` - Mocks the embedding store operations
+
+## Testing Patterns
+
+### Creating Queue Items
+
+```python
+# Create representation payloads
+payload = create_queue_payload(
+ message=message,
+ task_type="representation",
+ sender_name=message.peer_name,
+ target_name=observer_peer.name,
+)
+
+# Add to queue
+queue_items = await add_queue_items([payload], session.id)
+```
+
+### Testing Work Units
+
+```python
+# Create a work unit
+work_unit = WorkUnit(
+ session_id=session.id,
+ sender_name=sender.name,
+ target_name=target.name,
+ task_type="representation",
+)
+
+# Test string representation
+assert str(work_unit) == f"({session.id}, {sender.name}, {target.name}, representation)"
+```
+
+### Mocking Deriver Processing
+
+```python
+# Use the mock_deriver_process fixture to avoid actual LLM calls
+async def test_with_mocked_deriver(mock_deriver_process):
+ # Deriver processing will use the mock
+ await process_item(queue_item.payload)
+```
diff --git a/tests/deriver/__init__.py b/tests/deriver/__init__.py
new file mode 100644
index 00000000..6ced4f17
--- /dev/null
+++ b/tests/deriver/__init__.py
@@ -0,0 +1 @@
+# Tests for the deriver system.
diff --git a/tests/deriver/conftest.py b/tests/deriver/conftest.py
new file mode 100644
index 00000000..95386c3e
--- /dev/null
+++ b/tests/deriver/conftest.py
@@ -0,0 +1,312 @@
+import asyncio
+from collections.abc import Callable, Generator
+from datetime import datetime, timezone
+from typing import Any, Literal
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from nanoid import generate as generate_nanoid
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src import crud, models, schemas
+from src.deriver.queue_payload import create_payload
+from src.deriver.utils import get_work_unit_key
+
+
+@pytest.fixture
+def mock_deriver_process(monkeypatch: pytest.MonkeyPatch) -> Callable[..., Any]:
+ """Mock the deriver process_message method to avoid actual LLM calls"""
+ from src.deriver.deriver import Deriver
+
+ async def mock_process_message(
+ _self: Any, _task_type: str, _payload: dict[str, Any]
+ ) -> None:
+ # Simulate processing without making actual LLM calls
+ pass
+
+ monkeypatch.setattr(Deriver, "process_message", mock_process_message)
+
+ # Return the mock for further configuration if needed
+ return mock_process_message
+
+
+@pytest.fixture
+def mock_critical_analysis_call() -> Generator[Callable[..., Any], None, None]:
+ """Mock the critical analysis call to avoid actual LLM calls"""
+
+ async def mock_critical_analysis_call(*_args: Any, **_kwargs: Any) -> MagicMock:
+ # Create a mock response that matches the expected structure
+ mock_response = MagicMock()
+ mock_response.explicit = ["Test explicit observation"]
+ mock_response.deductive = []
+ mock_response.thinking = "Test thinking content"
+ mock_response._response = MagicMock()
+ mock_response._response.thinking = "Test thinking content"
+ return mock_response
+
+ # Patch the actual function in the deriver module
+ with patch(
+ "src.deriver.deriver.critical_analysis_call", mock_critical_analysis_call
+ ):
+ yield mock_critical_analysis_call
+
+
+@pytest.fixture
+async def sample_session_with_peers(
+ db_session: AsyncSession,
+ sample_data: tuple[models.Workspace, models.Peer],
+) -> tuple[models.Session, list[models.Peer]]:
+ """Create a sample session with multiple peers for testing deriver functionality"""
+ workspace, peer1 = sample_data
+
+ # Create additional peers
+ peer2 = models.Peer(name=str(generate_nanoid()), workspace_name=workspace.name)
+ peer3 = models.Peer(name=str(generate_nanoid()), workspace_name=workspace.name)
+ db_session.add_all([peer2, peer3])
+ await db_session.flush()
+
+ # Create session with peer configurations
+ session = await crud.get_or_create_session(
+ db_session,
+ schemas.SessionCreate(
+ name=str(generate_nanoid()),
+ peers={
+ peer1.name: schemas.SessionPeerConfig(observe_me=True),
+ peer2.name: schemas.SessionPeerConfig(observe_others=True),
+ peer3.name: schemas.SessionPeerConfig(), # No special observation settings
+ },
+ ),
+ workspace.name,
+ )
+ await db_session.commit()
+
+ return session, [peer1, peer2, peer3]
+
+
+@pytest.fixture
+async def sample_messages(
+ db_session: AsyncSession,
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+) -> list[models.Message]:
+ """Create sample messages for testing deriver functionality"""
+ session, peers = sample_session_with_peers
+ peer1, peer2, peer3 = peers
+
+ # Create multiple messages from different peers
+ messages_data = [
+ {
+ "session_name": session.name,
+ "content": "Hello, this is the first message from peer1",
+ "peer_name": peer1.name,
+ "workspace_name": session.workspace_name,
+ },
+ {
+ "session_name": session.name,
+ "content": "Hi there! This is a response from peer2",
+ "peer_name": peer2.name,
+ "workspace_name": session.workspace_name,
+ },
+ {
+ "session_name": session.name,
+ "content": "I'm just observing this conversation as peer3",
+ "peer_name": peer3.name,
+ "workspace_name": session.workspace_name,
+ },
+ ]
+
+ messages: list[models.Message] = []
+ for msg_data in messages_data:
+ message = models.Message(**msg_data)
+ db_session.add(message)
+ messages.append(message)
+
+ await db_session.commit()
+
+ # Query the messages again to get the committed versions
+ result = await db_session.execute(
+ select(models.Message)
+ .where(models.Message.session_name == session.name)
+ .order_by(models.Message.id)
+ )
+ messages = list(result.scalars().all())
+
+ return messages
+
+
+@pytest.fixture
+def create_queue_payload() -> Callable[..., Any]:
+ """Helper function to create queue payloads for testing"""
+
+ def _create_payload(
+ message: models.Message,
+ task_type: Literal["representation", "summary"],
+ sender_name: str | None = None,
+ target_name: str | None = None,
+ message_seq_in_session: int | None = None,
+ ) -> dict[str, Any]:
+ """Create a queue payload for testing"""
+ message_dict = {
+ "workspace_name": message.workspace_name,
+ "session_name": message.session_name,
+ "message_id": message.id,
+ "content": message.content,
+ "created_at": message.created_at or datetime.now(timezone.utc),
+ }
+
+ return create_payload(
+ message=message_dict,
+ task_type=task_type,
+ sender_name=sender_name,
+ target_name=target_name,
+ message_seq_in_session=message_seq_in_session,
+ )
+
+ return _create_payload
+
+
+@pytest.fixture
+async def add_queue_items(
+ db_session: AsyncSession,
+) -> Callable[[list[dict[str, Any]], str], Any]:
+ """Helper function to add queue items to the database"""
+
+ async def _add_items(
+ payloads: list[dict[str, Any]], session_id: str
+ ) -> list[models.QueueItem]:
+ """Add queue items to the database and return them"""
+ queue_items: list[models.QueueItem] = []
+ for payload in payloads:
+ # Generate work_unit_key from the payload
+ task_type = payload.get("task_type", "unknown")
+ work_unit_key = get_work_unit_key(task_type, payload)
+
+ queue_item = models.QueueItem(
+ session_id=session_id,
+ task_type=task_type,
+ work_unit_key=work_unit_key,
+ payload=payload,
+ processed=False,
+ )
+ db_session.add(queue_item)
+ queue_items.append(queue_item)
+
+ await db_session.commit()
+
+ # Refresh to get the actual IDs
+ for item in queue_items:
+ await db_session.refresh(item)
+
+ return queue_items
+
+ return _add_items
+
+
+@pytest.fixture
+async def sample_queue_items(
+ db_session: AsyncSession, # noqa: ARG001 # pyright: ignore[reportUnusedParameter]
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ sample_messages: list[models.Message],
+ create_queue_payload: Callable[..., Any],
+ add_queue_items: Callable[..., Any],
+) -> list[models.QueueItem]:
+ """Create sample queue items for testing"""
+ session, peers = sample_session_with_peers
+ _peer1, peer2, _peer3 = peers
+ messages = sample_messages
+
+ # Create various types of queue payloads
+ payloads: list[dict[str, Any]] = []
+
+ # Create representation payloads for each message
+ for message in messages:
+ # Self-representation (peer observing themselves)
+ payload1 = create_queue_payload(
+ message=message,
+ task_type="representation",
+ sender_name=message.peer_name,
+ target_name=message.peer_name,
+ )
+ payloads.append(payload1)
+
+ # Representation for observer peer
+ payload2 = create_queue_payload(
+ message=message,
+ task_type="representation",
+ sender_name=message.peer_name,
+ target_name=peer2.name, # peer2 observes others
+ )
+ payloads.append(payload2)
+
+ # Create summary payloads for session
+ for i, message in enumerate(messages):
+ payload = create_queue_payload(
+ message=message,
+ task_type="summary",
+ message_seq_in_session=i + 1,
+ )
+ payloads.append(payload)
+
+ # Add all payloads as queue items
+ queue_items = await add_queue_items(payloads, session.id)
+
+ return queue_items
+
+
+@pytest.fixture
+async def create_active_queue_session(db_session: AsyncSession) -> Callable[..., Any]:
+ """Helper function to create active queue sessions for testing work unit tracking"""
+
+ async def _create_active_session(
+ work_unit_key: str,
+ ) -> models.ActiveQueueSession:
+ """Create an active queue session"""
+ active_session = models.ActiveQueueSession(
+ work_unit_key=work_unit_key,
+ )
+ db_session.add(active_session)
+ await db_session.commit()
+ await db_session.refresh(active_session)
+ return active_session
+
+ return _create_active_session
+
+
+@pytest.fixture
+def mock_queue_manager(monkeypatch: pytest.MonkeyPatch) -> AsyncMock: # pyright: ignore[reportUnusedParameter]
+ """Mock the queue manager to avoid actual queue processing"""
+ from src.deriver.queue_manager import QueueManager
+
+ # Create a mock queue manager
+ mock_manager = AsyncMock(spec=QueueManager)
+
+ # Mock the methods we might need to test
+ mock_manager.initialize = AsyncMock()
+ mock_manager.shutdown = AsyncMock()
+ mock_manager.process_work_unit = AsyncMock()
+ mock_manager.get_available_work_units = AsyncMock(return_value=[])
+ mock_manager.add_task = MagicMock()
+ mock_manager.track_work_unit = MagicMock()
+ mock_manager.untrack_work_unit = MagicMock()
+
+ # Mock the attributes
+ mock_manager.shutdown_event = asyncio.Event()
+ mock_manager.active_tasks = set()
+ mock_manager.owned_work_units = set()
+ mock_manager.queue_empty_flag = asyncio.Event()
+ mock_manager.workers = 1
+ mock_manager.semaphore = asyncio.Semaphore(1)
+
+ return mock_manager
+
+
+@pytest.fixture
+def mock_embedding_store(monkeypatch: pytest.MonkeyPatch) -> AsyncMock: # pyright: ignore[reportUnusedParameter]
+ """Mock the embedding store to avoid actual embedding operations"""
+ from src.utils.embedding_store import EmbeddingStore
+
+ mock_store = AsyncMock(spec=EmbeddingStore)
+ mock_store.save_unified_observations = AsyncMock()
+ mock_store.get_relevant_observations = AsyncMock(return_value=MagicMock())
+
+ return mock_store
diff --git a/tests/deriver/test_deriver_processing.py b/tests/deriver/test_deriver_processing.py
new file mode 100644
index 00000000..28a4e123
--- /dev/null
+++ b/tests/deriver/test_deriver_processing.py
@@ -0,0 +1,116 @@
+import signal
+from collections.abc import Callable, Generator
+from typing import Any
+
+import pytest
+
+from src import models
+
+
+@pytest.mark.asyncio
+class TestDeriverProcessing:
+ """Test suite for deriver processing using the conftest fixtures"""
+
+ async def test_mock_deriver_process(
+ self,
+ mock_deriver_process: Callable[..., Any], # noqa: ARG001
+ sample_queue_items: list[models.QueueItem], # noqa: ARG001
+ ):
+ """Test that the deriver process is properly mocked"""
+ # The mock should be in place, so processing should not make real LLM calls
+ assert mock_deriver_process is not None
+
+ # Verify that we have queue items to process
+ assert len(sample_queue_items) > 0
+
+ # Verify the mock is working by checking the first queue item
+ first_item = sample_queue_items[0]
+ assert first_item.payload is not None
+
+ async def test_mock_critical_analysis_call(
+ self,
+ mock_critical_analysis_call: Generator[Callable[..., Any], None, None],
+ sample_messages: list[models.Message],
+ ):
+ """Test that the critical analysis call is properly mocked"""
+ assert mock_critical_analysis_call is not None
+ assert len(sample_messages) > 0 # Verify we have messages for testing
+
+ # The mock should be in place and return a predefined response
+ # This ensures no actual LLM calls are made during testing
+
+ async def test_work_unit_key_generation(
+ self,
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that work unit keys are generated correctly"""
+ from src.deriver.utils import get_work_unit_key
+
+ session, peers = sample_session_with_peers
+ peer1, peer2, _ = peers
+
+ # Create a payload for representation task
+ representation_payload = {
+ "workspace_name": "workspace1",
+ "session_name": session.name,
+ "sender_name": peer1.name,
+ "target_name": peer2.name,
+ "task_type": "representation",
+ }
+
+ # Generate work unit key for representation
+ work_unit_key = get_work_unit_key("representation", representation_payload)
+ expected_key = (
+ f"representation:workspace1:{session.name}:{peer1.name}:{peer2.name}"
+ )
+ assert work_unit_key == expected_key
+
+ # Create a payload for summary task (sender_name and target_name should be None)
+ summary_payload = {
+ "workspace_name": "workspace1",
+ "session_name": session.name,
+ "task_type": "summary",
+ }
+
+ # Generate work unit key for summary
+ summary_work_unit_key = get_work_unit_key("summary", summary_payload)
+ expected_summary_key = f"summary:workspace1:{session.name}:None:None"
+ assert summary_work_unit_key == expected_summary_key
+
+ async def test_mock_queue_manager(
+ self,
+ mock_queue_manager: Any, # AsyncMock object
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that the queue manager is properly mocked"""
+ session, peers = sample_session_with_peers
+ assert session is not None
+ assert len(peers) == 3
+
+ # Verify the mock has the expected attributes
+ assert mock_queue_manager is not None
+ assert hasattr(mock_queue_manager, "initialize")
+ assert hasattr(mock_queue_manager, "shutdown")
+ assert hasattr(mock_queue_manager, "process_work_unit")
+
+ # Verify we can call the mocked methods
+ await mock_queue_manager.initialize()
+ await mock_queue_manager.shutdown(signal.SIGTERM)
+
+ # Verify the mocked methods were called
+ mock_queue_manager.initialize.assert_called_once() # type: ignore[attr-defined]
+ mock_queue_manager.shutdown.assert_called_once() # type: ignore[attr-defined]
+
+ async def test_mock_embedding_store(
+ self,
+ mock_embedding_store: Any, # AsyncMock object
+ ):
+ """Test that the embedding store is properly mocked"""
+ assert mock_embedding_store is not None
+
+ # Verify we can call the mocked methods
+ await mock_embedding_store.save_unified_observations([])
+ mock_embedding_store.get_relevant_observations.return_value = [] # type: ignore[attr-defined]
+
+ # Verify the methods were called
+ assert mock_embedding_store.save_unified_observations.called # type: ignore[attr-defined]
diff --git a/tests/deriver/test_queue_operations.py b/tests/deriver/test_queue_operations.py
new file mode 100644
index 00000000..fe651842
--- /dev/null
+++ b/tests/deriver/test_queue_operations.py
@@ -0,0 +1,90 @@
+from collections.abc import Callable
+from typing import Any
+
+import pytest
+
+from src import models
+
+
+@pytest.mark.asyncio
+class TestQueueOperations:
+ """Test suite for queue operations using the deriver conftest fixtures"""
+
+ async def test_sample_queue_items_created(
+ self,
+ sample_queue_items: list[models.QueueItem],
+ ):
+ """Test that sample queue items are created correctly"""
+ # Should have 9 items: 3 messages * 2 representations + 3 summaries
+ assert len(sample_queue_items) == 9
+
+ # Check that we have the right mix of task types
+ representation_items = [
+ item
+ for item in sample_queue_items
+ if item.payload.get("task_type") == "representation"
+ ]
+ summary_items = [
+ item
+ for item in sample_queue_items
+ if item.payload.get("task_type") == "summary"
+ ]
+
+ assert len(representation_items) == 6
+ assert len(summary_items) == 3
+
+ # Check that all items are unprocessed
+ assert all(not item.processed for item in sample_queue_items)
+
+ async def test_sample_session_with_peers(
+ self,
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that sample session with peers is created correctly"""
+ session, peers = sample_session_with_peers
+ assert session is not None
+ assert len(peers) == 3
+
+ # Check that all peers have the same workspace
+ assert all(peer.workspace_name == session.workspace_name for peer in peers)
+
+ async def test_sample_messages(
+ self,
+ sample_messages: list[models.Message],
+ ):
+ """Test that sample messages are created correctly"""
+ assert len(sample_messages) == 3
+
+ # Check that all messages have content
+ assert all(message.content for message in sample_messages)
+
+ # Check that all messages have peer names
+ assert all(message.peer_name for message in sample_messages)
+
+ async def test_create_active_queue_session(
+ self,
+ create_active_queue_session: Callable[..., Any],
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that we can create active queue sessions"""
+ session, peers = sample_session_with_peers
+ peer1, peer2, _ = peers
+
+ # Create a work unit key for a representation task
+ work_unit_key = (
+ f"representation:workspace1:{session.name}:{peer1.name}:{peer2.name}"
+ )
+
+ # Create an active queue session
+ active_session = await create_active_queue_session(work_unit_key=work_unit_key)
+
+ assert active_session is not None
+ assert active_session.work_unit_key == work_unit_key
+
+ # Verify the work unit key has the expected format
+ parts = work_unit_key.split(":")
+ assert parts[0] == "representation"
+ assert parts[1] == "workspace1"
+ assert parts[2] == session.name
+ assert parts[3] == peer1.name
+ assert parts[4] == peer2.name
diff --git a/tests/deriver/test_queue_processing.py b/tests/deriver/test_queue_processing.py
new file mode 100644
index 00000000..080ac028
--- /dev/null
+++ b/tests/deriver/test_queue_processing.py
@@ -0,0 +1,141 @@
+from typing import Any
+
+import pytest
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from src import models
+from src.deriver.consumer import process_item
+from src.deriver.queue_manager import QueueManager
+
+
+@pytest.mark.asyncio
+class TestQueueProcessing:
+ """Test suite for queue processing functionality"""
+
+ async def test_get_available_work_units(
+ self,
+ db_session: AsyncSession,
+ sample_queue_items: list[models.QueueItem],
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that get_available_work_units correctly identifies unprocessed work"""
+ session, _peers = sample_session_with_peers # pyright: ignore[reportUnusedVariable]
+
+ # Verify we have queue items from our test setup
+ assert len(sample_queue_items) == 9 # 6 representation + 3 summary
+
+ # Create a queue manager instance
+ queue_manager = QueueManager()
+
+ # Get available work units
+ work_units = await queue_manager.get_available_work_units(db_session)
+
+ # Should have some work units available (may include items from other tests)
+ assert len(work_units) > 0
+
+ # Check that all work units have the expected structure
+ for work_unit in work_units:
+ assert isinstance(work_unit, str)
+ assert work_unit.split(":")[0] in ["representation", "summary"]
+
+ # The test is mainly verifying that get_available_work_units works without errors
+ # and returns properly structured work unit key strings
+
+ async def test_work_unit_claiming(
+ self,
+ db_session: AsyncSession,
+ sample_queue_items: list[models.QueueItem], # noqa: ARG001 # pyright: ignore[reportUnusedParameter]
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that work units can be claimed and are not available to other workers"""
+ _session, _peers = sample_session_with_peers
+
+ # Create a queue manager instance
+ queue_manager = QueueManager()
+
+ # Get available work units
+ work_units = await queue_manager.get_available_work_units(db_session)
+ assert len(work_units) > 0
+
+ # Claim a work unit by creating an ActiveQueueSession entry
+ work_unit = work_units[0]
+ active_session = models.ActiveQueueSession(
+ work_unit_key=work_unit,
+ )
+ db_session.add(active_session)
+ await db_session.commit()
+
+ # Get available work units again - the claimed one should not be available
+ remaining_work_units = await queue_manager.get_available_work_units(db_session)
+
+ # The claimed work unit should not be in the remaining list
+ assert work_unit not in remaining_work_units
+ # We can't assert exact count difference because work units are grouped by unique combinations
+
+ async def test_stale_work_unit_cleanup(
+ self,
+ db_session: AsyncSession,
+ sample_session_with_peers: tuple[models.Session, list[models.Peer]],
+ ):
+ """Test that stale work units are cleaned up properly"""
+ _session, _peers = sample_session_with_peers
+
+ # Create an active queue session with an old timestamp
+ # from datetime import datetime, timedelta, timezone
+
+ # datetime.now(timezone.utc) - timedelta(minutes=10)
+
+ # We'll test this by checking that the cleanup logic works in get_available_work_units
+ # which is called by the queue manager during normal operation
+ queue_manager = QueueManager()
+
+ # Get available work units - this should clean up stale entries
+ work_units = await queue_manager.get_available_work_units(db_session)
+
+ # This test ensures the cleanup logic doesn't break, though we don't have stale entries yet
+ assert isinstance(work_units, list)
+
+ async def test_process_item_with_mocked_deriver(
+ self,
+ mock_deriver_process: Any, # noqa: ARG001 # pyright: ignore[reportUnusedParameter]
+ sample_queue_items: list[models.QueueItem],
+ ):
+ """Test that process_item works with mocked deriver"""
+ # Take a sample queue item and process it
+ queue_item = sample_queue_items[0]
+
+ # This should not raise an exception since the deriver is mocked
+ await process_item(queue_item.task_type, queue_item.payload)
+
+ # The mock should have been called
+ # Note: We can't easily verify this since we're mocking the class method directly
+ # In a real test, we might want to mock at a different level
+
+ async def test_work_unit_key_format(
+ self, sample_session_with_peers: tuple[models.Session, list[models.Peer]]
+ ):
+ """Test that work unit keys have the correct format"""
+ session, peers = sample_session_with_peers
+ peer1, peer2, _ = peers
+
+ # Create a representation work unit key
+ # Format: task_type:workspace:session:sender:target
+ work_unit_key = (
+ f"representation:workspace1:{session.name}:{peer1.name}:{peer2.name}"
+ )
+
+ # Check that the key contains the expected information
+ assert session.name in work_unit_key
+ assert peer1.name in work_unit_key
+ assert peer2.name in work_unit_key
+ assert "representation" in work_unit_key
+ assert "workspace1" in work_unit_key
+
+ # Create a summary work unit key
+ # Summary work units use None for sender/target
+ summary_work_unit_key = f"summary:workspace1:{session.name}:None:None"
+
+ assert session.name in summary_work_unit_key
+ assert "None" in summary_work_unit_key
+ assert "summary" in summary_work_unit_key
+ assert "workspace1" in summary_work_unit_key
diff --git a/tests/integration/test_enqueue.py b/tests/integration/test_enqueue.py
index 0c3027bf..da7e78c7 100644
--- a/tests/integration/test_enqueue.py
+++ b/tests/integration/test_enqueue.py
@@ -1035,9 +1035,9 @@ class TestGetEffectiveObserveMeFunction:
sender_name = f"sender_{i}"
result = get_effective_observe_me(sender_name, peers_with_configuration)
- assert result == expected, (
- f"Test case {i} failed: peer_config={peer_config}, session_config={session_config}, expected={expected}, got={result}"
- )
+ assert (
+ result == expected
+ ), f"Test case {i} failed: peer_config={peer_config}, session_config={session_config}, expected={expected}, got={result}"
@pytest.mark.asyncio
diff --git a/tests/integration/test_message_embeddings.py b/tests/integration/test_message_embeddings.py
index 73f63d5f..35ef7980 100644
--- a/tests/integration/test_message_embeddings.py
+++ b/tests/integration/test_message_embeddings.py
@@ -12,9 +12,10 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src import models
-from src.crud import create_messages, search
+from src.crud import create_messages
from src.models import Peer, Workspace
from src.schemas import MessageCreate
+from src.utils.search import search
@pytest.mark.asyncio
@@ -243,21 +244,21 @@ async def test_semantic_search_when_embeddings_enabled(
# Check the call count before search
initial_call_count: int = mock_openai_embeddings["embed"].call_count
- search_stmt = await search(
+ search_results = await search(
+ db=db_session,
query=search_query,
- workspace_name=test_workspace.name,
- session_name=test_session.name,
+ filters={
+ "workspace_id": test_workspace.name,
+ "session_id": test_session.name,
+ },
)
# Verify that the embed method was called during search - e.g. we used semantic search
assert mock_openai_embeddings["embed"].call_count == initial_call_count + 1
- search_result = await db_session.execute(search_stmt)
- found_messages = list(search_result.scalars().all())
-
# Verify that our message was found via semantic search
- assert len(found_messages) > 0
- found_message_ids = [msg.public_id for msg in found_messages]
+ assert len(search_results) > 0
+ found_message_ids = [msg.public_id for msg in search_results]
assert created_message.public_id in found_message_ids
diff --git a/tests/routes/test_messages.py b/tests/routes/test_messages.py
index c67fd2f0..fd8e840e 100644
--- a/tests/routes/test_messages.py
+++ b/tests/routes/test_messages.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
import pytest
from fastapi.testclient import TestClient
from nanoid import generate as generate_nanoid
@@ -269,7 +271,7 @@ async def test_get_messages_with_empty_filter(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
- json={"filter": {}},
+ json={"filters": {}},
)
assert response.status_code == 200
data = response.json()
@@ -302,7 +304,7 @@ async def test_get_messages_with_null_filter(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
- json={"filter": None},
+ json={"filters": None},
)
assert response.status_code == 200
data = response.json()
@@ -375,7 +377,7 @@ async def test_get_filtered_messages(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
- json={"filter": {"metadata": {"key": "value2"}}},
+ json={"filters": {"metadata": {"key": "value2"}}},
)
assert response.status_code == 200
data = response.json()
@@ -430,7 +432,7 @@ async def test_get_filtered_messages_with_complex_filter(
# Test old-style filter (backward compatibility)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
- json={"filter": {"metadata": {"priority": "high", "category": "technical"}}},
+ json={"filters": {"metadata": {"priority": "high", "category": "technical"}}},
)
assert response.status_code == 200
data = response.json()
@@ -442,7 +444,7 @@ async def test_get_filtered_messages_with_complex_filter(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"metadata": {"priority": "high"}},
{"metadata": {"category": "technical"}},
@@ -459,7 +461,7 @@ async def test_get_filtered_messages_with_complex_filter(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
json={
- "filter": {
+ "filters": {
"OR": [
{"metadata": {"priority": "high"}},
{"metadata": {"type": "question"}},
@@ -793,3 +795,129 @@ async def test_create_batch_messages_max_limit(
assert len(data) == 100
assert data[0]["content"] == "Message 0"
assert data[99]["content"] == "Message 99"
+
+
+@pytest.mark.asyncio
+async def test_get_messages_handles_crud_value_error(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test that ValueError from CRUD is properly handled in get_messages"""
+ test_workspace, _test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Mock the CRUD function to raise ValueError
+ with patch("src.routers.messages.crud.get_messages") as mock_get:
+ mock_get.side_effect = ValueError("Test CRUD error")
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
+ json={},
+ )
+
+ # Should raise ResourceNotFoundException which gets converted to 404
+ assert response.status_code == 404
+
+
+@pytest.mark.asyncio
+async def test_get_message_handles_not_found(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test that ResourceNotFoundException is properly handled in get_message"""
+ test_workspace, _ = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Try to get a non-existent message
+ with patch("src.routers.messages.crud.get_message") as mock_get:
+ mock_get.return_value = None
+
+ response = client.get(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/nonexistent"
+ )
+
+ # Should raise ResourceNotFoundException which gets converted to 404
+ assert response.status_code == 404
+
+
+@pytest.mark.asyncio
+async def test_update_message_handles_crud_value_error(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test that ValueError from CRUD is properly handled in update_message"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session and message
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ test_message = models.Message(
+ session_name=test_session.name,
+ content="Test message",
+ workspace_name=test_workspace.name,
+ peer_name=test_peer.name,
+ )
+ db_session.add(test_message)
+ await db_session.commit()
+
+ # Mock the CRUD function to raise ValueError
+ with patch("src.routers.messages.crud.update_message") as mock_update:
+ mock_update.side_effect = ValueError("Test CRUD error")
+
+ response = client.put(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
+ json={"metadata": {"key": "value"}},
+ )
+
+ # Should raise ResourceNotFoundException which gets converted to 404
+ assert response.status_code == 404
+
+
+@pytest.mark.asyncio
+async def test_create_messages_with_file_too_large(
+ client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
+):
+ """Test that FileTooLargeError is properly handled in create_messages_with_file"""
+ test_workspace, test_peer = sample_data
+
+ # Create a test session
+ test_session = models.Session(
+ workspace_name=test_workspace.name, name=str(generate_nanoid())
+ )
+ db_session.add(test_session)
+ await db_session.commit()
+
+ # Create a large file that exceeds the size limit
+ import io
+
+ large_content = b"x" * (10 * 1024 * 1024) # 10MB file
+ file_data = io.BytesIO(large_content)
+
+ # Mock the settings to make the test deterministic
+ with patch(
+ "src.routers.messages.settings.MAX_FILE_SIZE", 5 * 1024 * 1024
+ ): # 5MB limit
+ files = {"file": ("large_file.txt", file_data, "text/plain")}
+ form_data = {"peer_id": test_peer.name}
+
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/upload",
+ files=files,
+ data=form_data,
+ )
+
+ # Should raise FileTooLargeError which gets converted to 413
+ assert response.status_code == 413
diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py
index 44aaaf8b..3f5d8256 100644
--- a/tests/routes/test_peers.py
+++ b/tests/routes/test_peers.py
@@ -1,4 +1,5 @@
-import pytest
+from typing import Any
+
from fastapi.testclient import TestClient
from nanoid import generate as generate_nanoid
@@ -114,7 +115,7 @@ def test_get_peers(client: TestClient, sample_data: tuple[Workspace, Peer]):
# Get peers with simple filter (backward compatibility)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"peer_key": "peer_value"}}},
+ json={"filters": {"metadata": {"peer_key": "peer_value"}}},
)
assert response.status_code == 200
data = response.json()
@@ -125,7 +126,7 @@ def test_get_peers(client: TestClient, sample_data: tuple[Workspace, Peer]):
# Test new filter with NOT operator
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"NOT": [{"metadata": {"peer_key": "peer_value2"}}]}},
+ json={"filters": {"NOT": [{"metadata": {"peer_key": "peer_value2"}}]}},
)
assert response.status_code == 200
data = response.json()
@@ -142,7 +143,7 @@ def test_get_peers_with_empty_filter(
test_workspace, _ = sample_data
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filter": {}}
+ f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filters": {}}
)
assert response.status_code == 200
data = response.json()
@@ -157,7 +158,7 @@ def test_get_peers_with_null_filter(
test_workspace, _ = sample_data
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filter": None}
+ f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filters": None}
)
assert response.status_code == 200
data = response.json()
@@ -296,7 +297,7 @@ def test_get_sessions_for_peer_with_empty_filter(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/sessions",
- json={"filter": {}},
+ json={"filters": {}},
)
assert response.status_code == 200
data = response.json()
@@ -392,17 +393,13 @@ def test_search_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
# Search with a query
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/search",
- json={"query": "search query"},
+ json={"query": "search query", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should have pagination structure
- assert "items" in data
- assert "total" in data
- assert "page" in data
- assert "size" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_peer_empty_query(
@@ -414,14 +411,13 @@ def test_search_peer_empty_query(
# Search with empty query
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/search",
- json={"query": ""},
+ json={"query": "", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should still have proper pagination structure
- assert "items" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_peer_nonexistent(
@@ -433,16 +429,18 @@ def test_search_peer_nonexistent(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{nonexistent_peer_id}/search",
- json={"query": "test query"},
+ json={"query": "test query", "limit": 10},
)
assert response.status_code == 200
- assert response.json()["items"] == []
+ data: list[dict[str, Any]] = response.json()
+ assert isinstance(data, list)
+ assert len(data) == 0
-def test_search_peer_with_semantic_search_false(
+def test_search_peer_with_messages(
client: TestClient, sample_data: tuple[Workspace, Peer]
):
- """Test peer search with semantic=false"""
+ """Test peer search with actual messages"""
test_workspace, test_peer = sample_data
# Add some messages to search through
@@ -456,31 +454,22 @@ def test_search_peer_with_semantic_search_false(
},
)
- # Search with semantic=false
+ # Search for content
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/search",
- json={"query": "search", "semantic": False},
+ json={"query": "search", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should have pagination structure
- assert "items" in data
- assert "total" in data
- assert "page" in data
- assert "size" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
-def test_search_peer_with_semantic_search_true_disabled(
- client: TestClient,
- sample_data: tuple[Workspace, Peer],
- monkeypatch: pytest.MonkeyPatch,
+def test_search_peer_with_limit(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
):
- """Test peer search with semantic=true when EMBED_MESSAGES is disabled"""
- # Override the EMBED_MESSAGES setting to False for this test
- monkeypatch.setattr("src.config.settings.EMBED_MESSAGES", False)
-
+ """Test peer search with custom limit"""
test_workspace, test_peer = sample_data
# Add some messages to search through
@@ -490,19 +479,72 @@ def test_search_peer_with_semantic_search_true_disabled(
"messages": [
{"content": "Search this content", "peer_id": test_peer.name},
{"content": "Another searchable message", "peer_id": test_peer.name},
+ {"content": "More searchable content", "peer_id": test_peer.name},
]
},
)
- # Search with semantic=true (should fail if EMBED_MESSAGES is disabled)
+ # Search with custom limit
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}/search",
- json={"query": "search", "semantic": True},
+ json={"query": "search", "limit": 2},
)
- assert response.status_code == 405
+ assert response.status_code == 200
+ data: list[dict[str, Any]] = response.json()
+ assert isinstance(data, list)
+ # Should not exceed the limit
+ assert len(data) <= 2
+
+def test_get_peers_with_complex_filter(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ """Test peer listing with complex filters"""
+ test_workspace, _ = sample_data
+
+ # Create peers with different metadata
+ for i in range(3):
+ client.post(
+ f"/v2/workspaces/{test_workspace.name}/peers",
+ json={
+ "name": str(generate_nanoid()),
+ "metadata": {"index": i, "type": "test"},
+ },
+ )
+
+ # Test complex filter combination
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/peers/list",
+ json={
+ "filter": {
+ "AND": [
+ {"metadata": {"type": "test"}},
+ {"metadata": {"index": {"gte": 1}}},
+ ]
+ }
+ },
+ )
+ assert response.status_code == 200
data = response.json()
- assert "Semantic search requires EMBED_MESSAGES flag to be enabled" in data.get(
- "detail", ""
+ assert "items" in data
+
+
+def test_update_peer_all_fields(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ """Test peer update with all possible fields"""
+ test_workspace, test_peer = sample_data
+
+ # Test updating both metadata and configuration
+ metadata = {"updated": True, "version": 2}
+ configuration = {"features": {"new_feature": True}}
+
+ response = client.put(
+ f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}",
+ json={"metadata": metadata, "configuration": configuration},
)
+ assert response.status_code == 200
+ data = response.json()
+ assert data["metadata"] == metadata
+ assert data["configuration"] == configuration
diff --git a/tests/routes/test_queue_status.py b/tests/routes/test_queue_status.py
index f9a57acf..441a5a0a 100644
--- a/tests/routes/test_queue_status.py
+++ b/tests/routes/test_queue_status.py
@@ -3,6 +3,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession
from src import models
+from src.deriver.utils import get_work_unit_key
@pytest.mark.asyncio
@@ -144,18 +145,23 @@ class TestDeriverStatusEndpoint:
await db_session.commit()
await db_session.refresh(session)
# Add queue items
- queue_items = [
- models.QueueItem(
+ queue_items: list[models.QueueItem] = []
+ for _ in range(5):
+ payload = {
+ "sender_name": peer.name,
+ "target_name": peer.name,
+ "task_type": "representation",
+ "workspace_name": workspace.name,
+ "session_name": session.name,
+ }
+ queue_item = models.QueueItem(
session_id=session.id,
- payload={
- "sender_name": peer.name,
- "target_name": peer.name,
- "task_type": "derive",
- },
+ task_type="representation",
+ work_unit_key=get_work_unit_key("representation", payload),
+ payload=payload,
processed=False,
)
- for _ in range(5)
- ]
+ queue_items.append(queue_item)
db_session.add_all(queue_items)
await db_session.commit()
# Test without parameters
@@ -213,18 +219,23 @@ class TestDeriverStatusEndpoint:
await db_session.refresh(s)
# Add queue items to different sessions
for i, session in enumerate(sessions):
- queue_items = [
- models.QueueItem(
+ queue_items: list[models.QueueItem] = []
+ for _ in range(i + 1): # 1,2,3 items respectively
+ payload = {
+ "sender_name": peer.name,
+ "target_name": peer.name,
+ "task_type": "representation",
+ "workspace_name": workspace.name,
+ "session_name": session.name,
+ }
+ queue_item = models.QueueItem(
session_id=session.id,
- payload={
- "sender_name": peer.name,
- "target_name": peer.name,
- "task_type": "derive",
- },
+ task_type="representation",
+ work_unit_key=get_work_unit_key("representation", payload),
+ payload=payload,
processed=False,
)
- for _ in range(i + 1) # 1,2,3 items respectively
- ]
+ queue_items.append(queue_item)
db_session.add_all(queue_items)
await db_session.commit()
response = client.get(
@@ -271,13 +282,18 @@ class TestDeriverStatusEndpoint:
db_session.add(session)
await db_session.commit()
await db_session.refresh(session)
+ payload = {
+ "sender_name": peer.name,
+ "target_name": peer.name,
+ "task_type": "representation",
+ "workspace_name": workspace.name,
+ "session_name": session.name,
+ }
queue_item = models.QueueItem(
session_id=session.id,
- payload={
- "sender_name": peer.name,
- "target_name": peer.name,
- "task_type": "derive",
- },
+ task_type="representation",
+ work_unit_key=get_work_unit_key("representation", payload),
+ payload=payload,
processed=False,
)
db_session.add(queue_item)
diff --git a/tests/routes/test_scoped_api.py b/tests/routes/test_scoped_api.py
index 7e7fa531..507147ac 100644
--- a/tests/routes/test_scoped_api.py
+++ b/tests/routes/test_scoped_api.py
@@ -168,7 +168,7 @@ def test_get_peer_by_name_with_auth(
# Use POST /list endpoint to get peers
response = auth_client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"id": test_peer.name}},
+ json={"filters": {"id": test_peer.name}},
)
# Admin JWT or JWT with matching workspace should be allowed
diff --git a/tests/routes/test_sessions.py b/tests/routes/test_sessions.py
index 73dd6682..a81046cc 100644
--- a/tests/routes/test_sessions.py
+++ b/tests/routes/test_sessions.py
@@ -146,7 +146,7 @@ def test_create_session_with_too_many_peers(
session_response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"id": "test_session"}},
+ json={"filters": {"id": "test_session"}},
)
assert session_response.status_code == 200
assert len(session_response.json()["items"]) == 0
@@ -186,7 +186,7 @@ def test_get_sessions(client: TestClient, sample_data: tuple[Workspace, Peer]):
assert data["workspace_id"] == test_workspace.name
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"metadata": {"test_key": "test_value"}}},
+ json={"filters": {"metadata": {"test_key": "test_value"}}},
)
assert response.status_code == 200
data = response.json()
@@ -203,7 +203,7 @@ def test_get_sessions_with_empty_filter(
test_workspace, _ = sample_data
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/sessions/list", json={"filter": {}}
+ f"/v2/workspaces/{test_workspace.name}/sessions/list", json={"filters": {}}
)
assert response.status_code == 200
data = response.json()
@@ -356,7 +356,7 @@ def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer])
# Check that session is marked as inactive
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"is_active": False}},
+ json={"filters": {"is_active": False}},
)
data = response.json()
# Find our session in the inactive sessions
@@ -773,17 +773,13 @@ def test_search_session(client: TestClient, sample_data: tuple[Workspace, Peer])
# Search with a query
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/search",
- json={"query": "search query"},
+ json={"query": "search query", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should have pagination structure
- assert "items" in data
- assert "total" in data
- assert "page" in data
- assert "size" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_session_empty_query(
@@ -802,14 +798,13 @@ def test_search_session_empty_query(
# Search with empty query
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/search",
- json={"query": ""},
+ json={"query": "", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should still have proper pagination structure
- assert "items" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_session_nonexistent(
@@ -821,23 +816,25 @@ def test_search_session_nonexistent(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/search",
- json={"query": "test query"},
+ json={"query": "test query", "limit": 10},
)
assert response.status_code == 200
- assert response.json()["items"] == []
+ data: list[dict[str, Any]] = response.json()
+ assert isinstance(data, list)
+ assert len(data) == 0
-def test_search_session_with_semantic_search_false(
+def test_search_session_with_messages(
client: TestClient, sample_data: tuple[Workspace, Peer]
):
- """Test session search with semantic=false"""
+ """Test session search with actual messages"""
test_workspace, test_peer = sample_data
session_id = str(generate_nanoid())
# Create session
client.post(
f"/v2/workspaces/{test_workspace.name}/sessions",
- json={"id": session_id, "peer_names": {test_peer.name: {}}},
+ json={"id": session_id, "peers": {test_peer.name: {}}},
)
# Add messages to search through
@@ -851,38 +848,29 @@ def test_search_session_with_semantic_search_false(
},
)
- # Search with semantic=false
+ # Search for content
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/search",
- json={"query": "search", "semantic": False},
+ json={"query": "search", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should have pagination structure
- assert "items" in data
- assert "total" in data
- assert "page" in data
- assert "size" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
-def test_search_session_with_semantic_search_true_disabled(
- client: TestClient,
- sample_data: tuple[Workspace, Peer],
- monkeypatch: pytest.MonkeyPatch,
+def test_search_session_with_limit(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
):
- """Test session search with semantic=true when EMBED_MESSAGES is disabled"""
- # Override the EMBED_MESSAGES setting to False for this test
- monkeypatch.setattr("src.config.settings.EMBED_MESSAGES", False)
-
+ """Test session search with custom limit"""
test_workspace, test_peer = sample_data
session_id = str(generate_nanoid())
# Create session
client.post(
f"/v2/workspaces/{test_workspace.name}/sessions",
- json={"id": session_id, "peer_names": {test_peer.name: {}}},
+ json={"id": session_id, "peers": {test_peer.name: {}}},
)
# Add messages to search through
@@ -892,19 +880,19 @@ def test_search_session_with_semantic_search_true_disabled(
"messages": [
{"content": "Search this content", "peer_id": test_peer.name},
{"content": "Another message to find", "peer_id": test_peer.name},
+ {"content": "More searchable content", "peer_id": test_peer.name},
]
},
)
- # Search with semantic=true (should fail if EMBED_MESSAGES is disabled)
+ # Search with custom limit
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/search",
- json={"query": "search", "semantic": True},
+ json={"query": "search", "limit": 2},
)
- assert response.status_code == 405
-
- data = response.json()
- assert "Semantic search requires EMBED_MESSAGES flag to be enabled" in data.get(
- "detail", ""
- )
+ assert response.status_code == 200
+ data: list[dict[str, Any]] = response.json()
+ assert isinstance(data, list)
+ # Should not exceed the limit
+ assert len(data) <= 2
diff --git a/tests/routes/test_validation_api.py b/tests/routes/test_validation_api.py
index 70f4627a..d238f263 100644
--- a/tests/routes/test_validation_api.py
+++ b/tests/routes/test_validation_api.py
@@ -220,9 +220,9 @@ def test_filter_validations_api(
# Test invalid filter type in message list (at session level)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": "not a dict"},
+ json={"filters": "not a dict"},
)
assert response.status_code == 422
error = response.json()["detail"][0]
- assert error["loc"] == ["body", "filter"]
+ assert error["loc"] == ["body", "filters"]
assert error["type"] == "dict_type"
diff --git a/tests/routes/test_webhooks.py b/tests/routes/test_webhooks.py
new file mode 100644
index 00000000..ed176473
--- /dev/null
+++ b/tests/routes/test_webhooks.py
@@ -0,0 +1,285 @@
+from typing import Any
+
+import pytest
+from fastapi.testclient import TestClient
+
+from src.config import settings
+from src.models import Peer, Workspace
+
+
+@pytest.mark.asyncio
+async def test_create_webhook_endpoint(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "http://example.com/webhook",
+ },
+ )
+ assert response.status_code == 200
+ response_json = response.json()
+ assert response_json["url"] == "http://example.com/webhook"
+ assert "id" in response_json
+ assert response_json["workspace_id"] == test_workspace.name
+ assert "created_at" in response_json
+
+
+@pytest.mark.asyncio
+async def test_create_webhook_endpoint_invalid_url(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "192.168.1.1/webhook",
+ },
+ )
+ assert response.status_code == 422
+ error = response.json()["detail"][0]
+ assert "Invalid URL format" in error["msg"]
+ assert error["type"] == "value_error"
+
+
+@pytest.mark.asyncio
+async def test_create_webhook_endpoint_missing_workspace(client: TestClient):
+ response = client.post(
+ "/v2/workspaces/nonexistent-workspace/webhooks",
+ json={
+ "url": "http://example.com/webhook",
+ },
+ )
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Workspace nonexistent-workspace not found"}
+
+
+@pytest.mark.asyncio
+async def test_list_webhook_endpoints_with_data(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ initial_count = len(list_response.json()["items"])
+
+ # Create first endpoint
+ response1 = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "http://example1.com/webhook",
+ },
+ )
+ assert response1.status_code == 200
+
+ # Create second endpoint
+ response2 = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "http://example2.com/webhook",
+ },
+ )
+ assert response2.status_code == 200
+
+ # List endpoints
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ assert list_response.status_code == 200
+ response_data = list_response.json()
+ endpoints = response_data["items"]
+ assert len(endpoints) == initial_count + 2
+
+ # Verify both endpoints are returned
+ endpoint_urls = [ep["url"] for ep in endpoints]
+ assert "http://example1.com/webhook" in endpoint_urls
+ assert "http://example2.com/webhook" in endpoint_urls
+
+
+@pytest.mark.asyncio
+async def test_list_webhook_endpoints_missing_workspace(client: TestClient):
+ response = client.get("/v2/workspaces/nonexistent-workspace/webhooks")
+ assert response.status_code == 404
+ assert response.json() == {"detail": "Workspace nonexistent-workspace not found"}
+
+
+@pytest.mark.asyncio
+async def test_delete_webhook_endpoint(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+
+ # Create webhook endpoint
+ create_response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "http://example.com/webhook",
+ },
+ )
+ assert create_response.status_code == 200
+ endpoint = create_response.json()
+ endpoint_id = endpoint["id"]
+
+ # Delete webhook endpoint
+ delete_response = client.delete(
+ f"/v2/workspaces/{test_workspace.name}/webhooks/{endpoint_id}"
+ )
+ assert delete_response.status_code == 200
+
+ # Verify endpoint is deleted
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ assert list_response.status_code == 200
+ response_data = list_response.json()
+ assert response_data["items"] == []
+
+
+@pytest.mark.asyncio
+async def test_delete_webhook_endpoint_not_found(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+ response = client.delete(
+ f"/v2/workspaces/{test_workspace.name}/webhooks/nonexistent-id"
+ )
+ assert response.status_code == 404
+ assert "not found" in response.json()["detail"]
+
+
+@pytest.mark.asyncio
+async def test_multiple_endpoints_per_workspace(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ """Test that workspaces can have multiple webhook endpoints"""
+ test_workspace, _ = sample_data
+
+ # Create multiple endpoints
+ urls = [
+ "http://app1.com/webhook",
+ "http://app2.com/webhook",
+ "http://app3.com/webhook",
+ ]
+
+ initial_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ initial_count = len(initial_response.json()["items"])
+
+ created_endpoints: list[Any] = []
+ for url in urls:
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={"url": url},
+ )
+ assert response.status_code == 200
+ created_endpoints.append(response.json())
+
+ # List all endpoints
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ assert list_response.status_code == 200
+ response_data = list_response.json()
+ endpoints = response_data["items"]
+ assert len(endpoints) == initial_count + 3
+
+ # Verify all URLs are present
+ returned_urls = [ep["url"] for ep in endpoints]
+ for url in urls:
+ assert url in returned_urls
+
+ # Delete one endpoint
+ delete_response = client.delete(
+ f"/v2/workspaces/{test_workspace.name}/webhooks/{created_endpoints[0]['id']}"
+ )
+ assert delete_response.status_code == 200
+
+ # Verify only 2 endpoints remain
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ assert list_response.status_code == 200
+ response_data = list_response.json()
+ endpoints = response_data["items"]
+ assert len(endpoints) == initial_count + 2
+
+
+@pytest.mark.asyncio
+async def test_create_duplicate_webhook_endpoint(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+ url = "http://example.com/duplicate"
+
+ # Create the endpoint first
+ response1 = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={"url": url},
+ )
+ assert response1.status_code == 200
+
+ # Try to create it again
+ response2 = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={"url": url},
+ )
+ assert response2.status_code == 200
+ assert response1.json() == response2.json()
+
+ # Verify only one endpoint exists
+ list_response = client.get(f"/v2/workspaces/{test_workspace.name}/webhooks")
+ assert list_response.status_code == 200
+ response_data = list_response.json()
+ endpoints = response_data["items"]
+ assert len(endpoints) == 1
+ assert endpoints[0]["url"] == url
+
+
+@pytest.mark.asyncio
+async def test_max_webhook_endpoints_per_workspace(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ test_workspace, _ = sample_data
+ limit: int = settings.WEBHOOK.MAX_WORKSPACE_LIMIT
+
+ # Create endpoints up to the limit
+ for i in range(limit):
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": f"http://example{i}.com/webhook",
+ },
+ )
+ assert response.status_code == 200
+
+ # Try to create one more
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/webhooks",
+ json={
+ "url": "http://extra.com/webhook",
+ },
+ )
+ assert response.status_code == 409
+
+
+@pytest.mark.asyncio
+async def test_same_endpoint_in_different_workspaces(
+ client: TestClient, sample_data: tuple[Workspace, Peer]
+):
+ ws1, _ = sample_data
+ ws2_response = client.post("/v2/workspaces", json={"name": "workspace-2"})
+ assert ws2_response.status_code == 200
+ ws2 = ws2_response.json()
+
+ url = "http://example.com/shared"
+
+ # Create endpoint in workspace 1
+ response1 = client.post(
+ f"/v2/workspaces/{ws1.name}/webhooks",
+ json={"url": url},
+ )
+ assert response1.status_code == 200
+
+ # Create same endpoint in workspace 2
+ response2 = client.post(
+ f"/v2/workspaces/{ws2['id']}/webhooks",
+ json={"url": url},
+ )
+ assert response2.status_code == 200
+
+ # Verify they are different resources
+ assert response1.json()["id"] != response2.json()["id"]
+ assert response1.json()["workspace_id"] == ws1.name
+ assert response2.json()["workspace_id"] == ws2["id"]
diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py
index 52f8a10f..7cdaa335 100644
--- a/tests/routes/test_workspaces.py
+++ b/tests/routes/test_workspaces.py
@@ -1,3 +1,5 @@
+from typing import Any
+
import pytest
from fastapi.testclient import TestClient
from nanoid import generate as generate_nanoid
@@ -91,7 +93,7 @@ async def test_get_all_workspaces(client: TestClient):
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"metadata": {"test_key": "test_value"}}},
+ json={"filters": {"metadata": {"test_key": "test_value"}}},
)
assert response.status_code == 200
data = response.json()
@@ -103,7 +105,7 @@ async def test_get_all_workspaces(client: TestClient):
@pytest.mark.asyncio
async def test_get_all_workspaces_with_empty_filter(client: TestClient):
"""Test workspace listing with empty filter object"""
- response = client.post("/v2/workspaces/list", json={"filter": {}})
+ response = client.post("/v2/workspaces/list", json={"filters": {}})
assert response.status_code == 200
data = response.json()
assert "items" in data
@@ -113,7 +115,7 @@ async def test_get_all_workspaces_with_empty_filter(client: TestClient):
@pytest.mark.asyncio
async def test_get_all_workspaces_with_null_filter(client: TestClient):
"""Test workspace listing with null filter"""
- response = client.post("/v2/workspaces/list", json={"filter": None})
+ response = client.post("/v2/workspaces/list", json={"filters": None})
assert response.status_code == 200
data = response.json()
assert "items" in data
@@ -222,17 +224,14 @@ def test_search_workspace(client: TestClient, sample_data: tuple[Workspace, Peer
# Test search with a query
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/search", json="test search query"
+ f"/v2/workspaces/{test_workspace.name}/search",
+ json={"query": "test search query", "limit": 10},
)
assert response.status_code == 200
data = response.json()
- # Response should have pagination structure
- assert "items" in data
- assert "total" in data
- assert "page" in data
- assert "size" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_workspace_empty_query(
@@ -242,13 +241,14 @@ def test_search_workspace_empty_query(
test_workspace, _ = sample_data
# Test search with empty query
- response = client.post(f"/v2/workspaces/{test_workspace.name}/search", json="")
+ response = client.post(
+ f"/v2/workspaces/{test_workspace.name}/search", json={"query": "", "limit": 10}
+ )
assert response.status_code == 200
data = response.json()
- # Response should still have proper pagination structure
- assert "items" in data
- assert isinstance(data["items"], list)
+ # Response should be a direct list of messages
+ assert isinstance(data, list)
def test_search_workspace_nonexistent(client: TestClient):
@@ -256,6 +256,11 @@ def test_search_workspace_nonexistent(client: TestClient):
nonexistent_workspace_id = str(generate_nanoid())
response = client.post(
- f"/v2/workspaces/{nonexistent_workspace_id}/search", json="test query"
+ f"/v2/workspaces/{nonexistent_workspace_id}/search",
+ json={"query": "test query", "limit": 10},
)
assert response.status_code == 200
+ data: list[dict[str, Any]] = response.json()
+ # Should return empty list for nonexistent workspace
+ assert isinstance(data, list)
+ assert len(data) == 0
diff --git a/tests/sdk/test_client.py b/tests/sdk/test_client.py
index 028854e8..daff2157 100644
--- a/tests/sdk/test_client.py
+++ b/tests/sdk/test_client.py
@@ -172,9 +172,9 @@ async def test_workspace_search(client_fixture: tuple[Honcho | AsyncHoncho, str]
await session.add_messages([user.message(search_query)])
search_results = await honcho_client.search(search_query)
- results = search_results.items
- assert len(results) >= 1
- assert search_query in results[0].content
+ assert isinstance(search_results, list)
+ assert len(search_results) >= 1
+ assert search_query in search_results[0].content
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="search-session-ws")
@@ -184,9 +184,9 @@ async def test_workspace_search(client_fixture: tuple[Honcho | AsyncHoncho, str]
session.add_messages([user.message(search_query)])
search_results = honcho_client.search(search_query)
- results = list(search_results)
- assert len(results) >= 1
- assert search_query in results[0].content
+ assert isinstance(search_results, list)
+ assert len(search_results) >= 1
+ assert search_query in search_results[0].content
@pytest.mark.asyncio
diff --git a/tests/sdk/test_session.py b/tests/sdk/test_session.py
index c4dea44e..ba9797e2 100644
--- a/tests/sdk/test_session.py
+++ b/tests/sdk/test_session.py
@@ -257,9 +257,9 @@ async def test_session_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
await session.add_messages([user.message(search_query)])
search_results = await session.search(search_query)
- results = search_results.items
- assert len(results) >= 1
- assert search_query in results[0].content
+ assert isinstance(search_results, list)
+ assert len(search_results) >= 1
+ assert search_query in search_results[0].content
else:
assert isinstance(honcho_client, Honcho)
session = honcho_client.session(id="search-session-s")
@@ -269,9 +269,9 @@ async def test_session_search(client_fixture: tuple[Honcho | AsyncHoncho, str]):
session.add_messages([user.message(search_query)])
search_results = session.search(search_query)
- results = list(search_results)
- assert len(results) >= 1
- assert search_query in results[0].content
+ assert isinstance(search_results, list)
+ assert len(search_results) >= 1
+ assert search_query in search_results[0].content
@pytest.mark.asyncio
diff --git a/tests/test_advanced_filters.py b/tests/test_advanced_filters.py
index fa545c5b..8afab352 100644
--- a/tests/test_advanced_filters.py
+++ b/tests/test_advanced_filters.py
@@ -97,7 +97,7 @@ async def test_logical_operators_and_filters(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": combined_filter},
+ json={"filters": combined_filter},
)
assert response.status_code == 200, f"Failed testing {description}"
@@ -208,7 +208,7 @@ async def test_comparison_operators_filters(
# Test the filter configuration
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": filter_config},
+ json={"filters": filter_config},
)
assert response.status_code == 200, f"Failed testing {description}"
@@ -259,7 +259,7 @@ async def test_wildcard_filters(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"id": "*"},
{"metadata": {"type": "bot"}},
@@ -277,7 +277,7 @@ async def test_wildcard_filters(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"id": {"in": ["*"]} # Wildcard in comparison should also match all
}
},
@@ -347,11 +347,11 @@ async def test_complex_nested_filters(
},
)
- # Complex filter: (urgent OR normal priority) AND open status AND NOT assigned to charlie
+ # Complex filters: (urgent OR normal priority) AND open status AND NOT assigned to charlie
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
json={
- "filter": {
+ "filters": {
"AND": [
{
"OR": [
@@ -397,7 +397,7 @@ async def test_filters_across_different_models(
response = client.post(
"/v2/workspaces/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"metadata": {"environment": "production"}},
{"metadata": {"version": {"gte": "2.0"}}},
@@ -434,7 +434,7 @@ async def test_filters_across_different_models(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
json={
- "filter": {
+ "filters": {
"OR": [
{"metadata": {"duration": {"gte": 45}}},
{"metadata": {"type": "meeting"}},
@@ -460,7 +460,7 @@ async def test_filter_edge_cases(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [] # Empty AND should not crash
}
},
@@ -470,14 +470,14 @@ async def test_filter_edge_cases(
# Test nested empty operators
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"OR": [{"AND": []}, {"id": test_peer.name}]}},
+ json={"filters": {"OR": [{"AND": []}, {"id": test_peer.name}]}},
)
assert response.status_code == 200
# Test filter with non-existent columns (should be ignored gracefully)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"non_existent_column": "value"}},
+ json={"filters": {"non_existent_column": "value"}},
)
assert response.status_code == 422
@@ -485,7 +485,7 @@ async def test_filter_edge_cases(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"id": "*"}, # Wildcard
{"id": test_peer.name}, # Regular value
@@ -520,7 +520,7 @@ async def test_backward_compatibility(
# Test old-style simple equality filter
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"role": "admin"}}},
+ json={"filters": {"metadata": {"role": "admin"}}},
)
assert response.status_code == 200
data = response.json()
@@ -530,7 +530,7 @@ async def test_backward_compatibility(
# Test multiple field simple filter (implicit AND)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"role": "admin"}, "id": peer_name}},
+ json={"filters": {"metadata": {"role": "admin"}, "id": peer_name}},
)
assert response.status_code == 200
data = response.json()
@@ -579,7 +579,7 @@ async def test_range_queries_with_dates(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
json={
- "filter": {
+ "filters": {
"metadata": {"created_date": {"gte": "2024-02-01", "lte": "2024-02-28"}}
}
},
@@ -595,7 +595,7 @@ async def test_range_queries_with_dates(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"metadata": {"created_date": {"gte": "2024-01-01"}}},
{"metadata": {"priority": {"gt": 4}}},
@@ -638,7 +638,7 @@ async def test_all_workspace_columns_filtering(client: TestClient):
# Test filtering by id (maps to name internally)
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"id": workspace1_name}},
+ json={"filters": {"id": workspace1_name}},
)
assert response.status_code == 200
data = response.json()
@@ -648,7 +648,7 @@ async def test_all_workspace_columns_filtering(client: TestClient):
# Test filtering by id with comparison operators
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"id": {"in": [workspace1_name, workspace2_name]}}},
+ json={"filters": {"id": {"in": [workspace1_name, workspace2_name]}}},
)
assert response.status_code == 200
data = response.json()
@@ -659,7 +659,7 @@ async def test_all_workspace_columns_filtering(client: TestClient):
# Test filtering by metadata (maps to h_metadata internally)
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"metadata": {"env": "dev"}}},
+ json={"filters": {"metadata": {"env": "dev"}}},
)
assert response.status_code == 200
data = response.json()
@@ -670,7 +670,7 @@ async def test_all_workspace_columns_filtering(client: TestClient):
# Test filtering by metadata with comparison operators
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"metadata": {"version": {"gte": "2.0"}}}},
+ json={"filters": {"metadata": {"version": {"gte": "2.0"}}}},
)
assert response.status_code == 200
data = response.json()
@@ -681,7 +681,7 @@ async def test_all_workspace_columns_filtering(client: TestClient):
# Test filtering by created_at (datetime field)
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"created_at": {"gte": "2020-01-01"}}},
+ json={"filters": {"created_at": {"gte": "2020-01-01"}}},
)
assert response.status_code == 200
# Should return all workspaces created after 2020
@@ -718,7 +718,7 @@ async def test_all_peer_columns_filtering(
# Test filtering by id (maps to name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"id": peer1_name}},
+ json={"filters": {"id": peer1_name}},
)
assert response.status_code == 200
data = response.json()
@@ -728,7 +728,7 @@ async def test_all_peer_columns_filtering(
# Test filtering by workspace_id (maps to workspace_name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -738,7 +738,7 @@ async def test_all_peer_columns_filtering(
# Test filtering by metadata
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"role": "admin"}}},
+ json={"filters": {"metadata": {"role": "admin"}}},
)
assert response.status_code == 200
data = response.json()
@@ -749,7 +749,7 @@ async def test_all_peer_columns_filtering(
# Test filtering by metadata with comparison operators
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"level": {"gte": 3}}}},
+ json={"filters": {"metadata": {"level": {"gte": 3}}}},
)
assert response.status_code == 200
data = response.json()
@@ -760,7 +760,7 @@ async def test_all_peer_columns_filtering(
# Test filtering by created_at
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"created_at": {"gte": "2020-01-01"}}},
+ json={"filters": {"created_at": {"gte": "2020-01-01"}}},
)
assert response.status_code == 200
# Should return all peers created after 2020
@@ -799,7 +799,7 @@ async def test_all_session_columns_filtering(
# Test filtering by id (maps to name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"id": session1_id}},
+ json={"filters": {"id": session1_id}},
)
assert response.status_code == 200
data = response.json()
@@ -809,7 +809,7 @@ async def test_all_session_columns_filtering(
# Test filtering by workspace_id (maps to workspace_name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -819,7 +819,7 @@ async def test_all_session_columns_filtering(
# Test filtering by is_active (boolean field)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"is_active": True}},
+ json={"filters": {"is_active": True}},
)
assert response.status_code == 200
data = response.json()
@@ -830,7 +830,7 @@ async def test_all_session_columns_filtering(
# Test filtering by metadata
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"metadata": {"type": "chat"}}},
+ json={"filters": {"metadata": {"type": "chat"}}},
)
assert response.status_code == 200
data = response.json()
@@ -840,7 +840,7 @@ async def test_all_session_columns_filtering(
# Test filtering by metadata with comparison operators
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"metadata": {"priority": {"gte": 3}}}},
+ json={"filters": {"metadata": {"priority": {"gte": 3}}}},
)
assert response.status_code == 200
data = response.json()
@@ -851,7 +851,7 @@ async def test_all_session_columns_filtering(
# Test filtering by created_at
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"created_at": {"gte": "2020-01-01"}}},
+ json={"filters": {"created_at": {"gte": "2020-01-01"}}},
)
assert response.status_code == 200
# Should return all sessions created after 2020
@@ -900,7 +900,7 @@ async def test_all_message_columns_filtering(
# Test filtering by session_id (maps to session_name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"session_id": session_id}},
+ json={"filters": {"session_id": session_id}},
)
assert response.status_code == 200
data = response.json()
@@ -909,7 +909,7 @@ async def test_all_message_columns_filtering(
# Test filtering by peer_id (maps to peer_name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"peer_id": test_peer.name}},
+ json={"filters": {"peer_id": test_peer.name}},
)
assert response.status_code == 200
data = response.json()
@@ -918,7 +918,7 @@ async def test_all_message_columns_filtering(
# Test filtering by workspace_id (maps to workspace_name internally)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -927,21 +927,21 @@ async def test_all_message_columns_filtering(
# Test filtering by content (text field) (not allowed)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"content": "Hello world message"}},
+ json={"filters": {"content": "Hello world message"}},
)
assert response.status_code == 422
# Test filtering by content with contains operator (not allowed)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"content": {"contains": "support"}}},
+ json={"filters": {"content": {"contains": "support"}}},
)
assert response.status_code == 422
# Test filtering by metadata
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"type": "greeting"}}},
+ json={"filters": {"metadata": {"type": "greeting"}}},
)
assert response.status_code == 200
data = response.json()
@@ -950,7 +950,7 @@ async def test_all_message_columns_filtering(
# Test filtering by metadata with comparison operators
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"priority": {"gte": 3}}}},
+ json={"filters": {"metadata": {"priority": {"gte": 3}}}},
)
assert response.status_code == 200
data = response.json()
@@ -959,7 +959,7 @@ async def test_all_message_columns_filtering(
# Test filtering by token_count (integer field) - this should exist after message creation
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"token_count": {"gte": 0}}},
+ json={"filters": {"token_count": {"gte": 0}}},
)
assert response.status_code == 200
data = response.json()
@@ -968,7 +968,7 @@ async def test_all_message_columns_filtering(
# Test filtering by created_at
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"created_at": {"gte": "2020-01-01"}}},
+ json={"filters": {"created_at": {"gte": "2020-01-01"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1025,7 +1025,7 @@ async def test_id_field_interpolation_consistency(client: TestClient):
# Workspace: id should map to name
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"id": workspace_name}},
+ json={"filters": {"id": workspace_name}},
)
assert response.status_code == 200
data = response.json()
@@ -1035,7 +1035,7 @@ async def test_id_field_interpolation_consistency(client: TestClient):
# Peer: id should map to name
response = client.post(
f"/v2/workspaces/{workspace_name}/peers/list",
- json={"filter": {"id": peer_name}},
+ json={"filters": {"id": peer_name}},
)
assert response.status_code == 200
data = response.json()
@@ -1045,7 +1045,7 @@ async def test_id_field_interpolation_consistency(client: TestClient):
# Session: id should map to name
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/list",
- json={"filter": {"id": session_id}},
+ json={"filters": {"id": session_id}},
)
assert response.status_code == 200
data = response.json()
@@ -1055,7 +1055,7 @@ async def test_id_field_interpolation_consistency(client: TestClient):
# Message: id is not allowed to be filtered on
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/{session_id}/messages/list",
- json={"filter": {"id": message_id}},
+ json={"filters": {"id": message_id}},
)
assert response.status_code == 422
@@ -1102,7 +1102,7 @@ async def test_foreign_key_field_interpolation(
# Test workspace_id filtering for peers (maps to workspace_name)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -1113,7 +1113,7 @@ async def test_foreign_key_field_interpolation(
# Test workspace_id filtering for sessions (maps to workspace_name)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -1123,7 +1123,7 @@ async def test_foreign_key_field_interpolation(
# Test session_id filtering for messages (maps to session_name)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"session_id": session_id}},
+ json={"filters": {"session_id": session_id}},
)
assert response.status_code == 200
data = response.json()
@@ -1132,7 +1132,7 @@ async def test_foreign_key_field_interpolation(
# Test peer_id filtering for messages (maps to peer_name)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"peer_id": peer_name}},
+ json={"filters": {"peer_id": peer_name}},
)
assert response.status_code == 200
data = response.json()
@@ -1141,7 +1141,7 @@ async def test_foreign_key_field_interpolation(
# Test workspace_id filtering for messages (maps to workspace_name)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"workspace_id": test_workspace.name}},
+ json={"filters": {"workspace_id": test_workspace.name}},
)
assert response.status_code == 200
data = response.json()
@@ -1203,7 +1203,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Workspace metadata
response = client.post(
"/v2/workspaces/list",
- json={"filter": {"metadata": {"env": "test"}}},
+ json={"filters": {"metadata": {"env": "test"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1213,7 +1213,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Peer metadata
response = client.post(
f"/v2/workspaces/{workspace_name}/peers/list",
- json={"filter": {"metadata": {"role": "user"}}},
+ json={"filters": {"metadata": {"role": "user"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1223,7 +1223,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Session metadata
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/list",
- json={"filter": {"metadata": {"type": "chat"}}},
+ json={"filters": {"metadata": {"type": "chat"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1233,7 +1233,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Message metadata
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"sentiment": "positive"}}},
+ json={"filters": {"metadata": {"sentiment": "positive"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1244,7 +1244,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Numeric metadata comparison
response = client.post(
f"/v2/workspaces/{workspace_name}/peers/list",
- json={"filter": {"metadata": {"level": {"gte": 3}}}},
+ json={"filters": {"metadata": {"level": {"gte": 3}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1254,7 +1254,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Boolean metadata comparison
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/list",
- json={"filter": {"metadata": {"archived": {"ne": True}}}},
+ json={"filters": {"metadata": {"archived": {"ne": True}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1264,7 +1264,7 @@ async def test_metadata_field_interpolation(client: TestClient):
# Float metadata comparison
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"confidence": {"gte": 0.9}}}},
+ json={"filters": {"metadata": {"confidence": {"gte": 0.9}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1288,7 +1288,7 @@ async def test_nonexistent_columns_ignored_gracefully(
# Test filtering by non-existent columns
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"nonexistent_column": "value"}},
+ json={"filters": {"nonexistent_column": "value"}},
)
assert response.status_code == 422
@@ -1296,7 +1296,7 @@ async def test_nonexistent_columns_ignored_gracefully(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"metadata": {"role": "test"}}, # Real filter
{"fake_column": "fake_value"}, # Non-existent filter
@@ -1310,7 +1310,7 @@ async def test_nonexistent_columns_ignored_gracefully(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"OR": [
{"nonexistent_field": "value"},
{"metadata": {"role": "test"}},
@@ -1358,7 +1358,7 @@ async def test_not_logic_correctness(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"NOT": [
{"metadata": {"role": "admin"}},
{"metadata": {"department": "engineering"}},
@@ -1426,7 +1426,7 @@ async def test_jsonb_type_casting_edge_cases(
# Test boolean comparisons - PostgreSQL stores booleans as "true"/"false" strings
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"active": {"ne": False}}}},
+ json={"filters": {"metadata": {"active": {"ne": False}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1436,7 +1436,7 @@ async def test_jsonb_type_casting_edge_cases(
# Test string vs numeric comparison
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"score": {"gt": 10}}}},
+ json={"filters": {"metadata": {"score": {"gt": 10}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1447,7 +1447,7 @@ async def test_jsonb_type_casting_edge_cases(
# Test large number handling
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"score": {"gte": 999999999}}}},
+ json={"filters": {"metadata": {"score": {"gte": 999999999}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1488,7 +1488,7 @@ async def test_real_datetime_column_filtering(
# Test ISO format
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"created_at": {"gte": one_minute_ago.isoformat()}}},
+ json={"filters": {"created_at": {"gte": one_minute_ago.isoformat()}}},
)
assert response.status_code == 200
data = response.json()
@@ -1500,7 +1500,7 @@ async def test_real_datetime_column_filtering(
today = datetime.now(timezone.utc).date().isoformat()
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"created_at": {"gte": today}}},
+ json={"filters": {"created_at": {"gte": today}}},
)
assert response.status_code == 200
@@ -1523,7 +1523,7 @@ async def test_invalid_datetime_handling(
for malicious_dt in malicious_datetimes:
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"created_at": {"gte": malicious_dt}}},
+ json={"filters": {"created_at": {"gte": malicious_dt}}},
)
assert response.status_code == 422
@@ -1573,7 +1573,7 @@ async def test_nested_jsonb_filtering(
# Test nested object filtering - this might not work as expected
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"user": {"profile": {"premium": True}}}}},
+ json={"filters": {"metadata": {"user": {"profile": {"premium": True}}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1626,7 +1626,7 @@ async def test_multiple_operators_same_field(
# Test range query: score >= 3 AND score <= 8
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"score": {"gte": 3, "lte": 8}}}},
+ json={"filters": {"metadata": {"score": {"gte": 3, "lte": 8}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1643,7 +1643,7 @@ async def test_empty_and_null_filter_handling(
# Test completely empty filter
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filter": {}}
+ f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filters": {}}
)
assert response.status_code == 200
data = response.json()
@@ -1652,14 +1652,14 @@ async def test_empty_and_null_filter_handling(
# Test null filter
response = client.post(
- f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filter": None}
+ f"/v2/workspaces/{test_workspace.name}/peers/list", json={"filters": None}
)
assert response.status_code == 200
# Test empty comparison dict
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"role": {}}}},
+ json={"filters": {"metadata": {"role": {}}}},
)
assert response.status_code == 200
@@ -1687,7 +1687,7 @@ async def test_unicode_and_special_characters(
# Test unicode in metadata contains
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"description": {"icontains": "wörld"}}}},
+ json={"filters": {"metadata": {"description": {"icontains": "wörld"}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1724,7 +1724,7 @@ async def test_case_sensitivity_edge_cases(
# Test exact case matching (should be case sensitive for JSONB)
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"Role": "Admin"}}},
+ json={"filters": {"metadata": {"Role": "Admin"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1735,7 +1735,7 @@ async def test_case_sensitivity_edge_cases(
# Test icontains for case insensitive search
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": {"metadata": {"Department": {"icontains": "engineering"}}}},
+ json={"filters": {"metadata": {"Department": {"icontains": "engineering"}}}},
)
assert response.status_code == 200
data = response.json()
@@ -1764,7 +1764,7 @@ async def test_malformed_filter_structures(
for malformed_filter in malformed_filters:
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": malformed_filter},
+ json={"filters": malformed_filter},
)
assert response.status_code == 422
@@ -1813,7 +1813,7 @@ async def test_performance_with_complex_filters(
# This should complete without timeout
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": complex_filter},
+ json={"filters": complex_filter},
)
assert response.status_code == 200
# Should return results in reasonable time (this is more of a performance test)
@@ -1854,7 +1854,7 @@ async def test_filter_precedence_and_grouping(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [
{
"OR": [
@@ -1928,7 +1928,7 @@ async def test_jsonb_contains_vs_equality_semantics(
# Test JSONB contains behavior - should match both exact and superset
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"role": "admin"}}},
+ json={"filters": {"metadata": {"role": "admin"}}},
)
assert response.status_code == 200
data = response.json()
@@ -1955,7 +1955,7 @@ async def test_wildcard_edge_cases_comprehensive(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {"metadata": {"level": {"gte": "*"}}} # Wildcard in comparison
+ "filters": {"metadata": {"level": {"gte": "*"}}} # Wildcard in comparison
},
)
assert response.status_code == 200
@@ -1967,7 +1967,7 @@ async def test_wildcard_edge_cases_comprehensive(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"metadata": {"role": {"in": ["*", "admin"]}}
} # Mixed wildcard and value
},
@@ -1981,7 +1981,7 @@ async def test_wildcard_edge_cases_comprehensive(
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"id": "*"},
{"metadata": {"role": "*"}},
@@ -2013,7 +2013,7 @@ async def test_error_logging_and_debugging(
for filter_dict in problematic_filters:
response = client.post(
f"/v2/workspaces/{test_workspace.name}/peers/list",
- json={"filter": filter_dict},
+ json={"filters": filter_dict},
)
assert response.status_code == 422
@@ -2061,7 +2061,7 @@ async def test_boundary_conditions_numeric(
# Test boundary conditions
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"score": {"gte": 0}}}},
+ json={"filters": {"metadata": {"score": {"gte": 0}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2073,7 +2073,7 @@ async def test_boundary_conditions_numeric(
# Test floating point precision
response = client.post(
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/messages/list",
- json={"filter": {"metadata": {"score": {"gt": 3.14}}}},
+ json={"filters": {"metadata": {"score": {"gt": 3.14}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2165,7 +2165,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test exact equality - this may or may not work due to floating point precision
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"value": 0.3}}},
+ json={"filters": {"metadata": {"value": 0.3}}},
)
assert response.status_code == 200
data = response.json()
@@ -2179,7 +2179,7 @@ async def test_float_precision_edge_cases(client: TestClient):
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
json={
- "filter": {
+ "filters": {
"AND": [
{"metadata": {"value": {"gte": 0.3 - epsilon}}},
{"metadata": {"value": {"lte": 0.3 + epsilon}}},
@@ -2197,7 +2197,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test greater than with floating point precision
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"value": {"gt": 0.3}}}},
+ json={"filters": {"metadata": {"value": {"gt": 0.3}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2208,7 +2208,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test very small numbers and scientific notation
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"value": {"lt": 1e-9}}}},
+ json={"filters": {"metadata": {"value": {"lt": 1e-9}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2218,7 +2218,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test large number precision
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"value": {"gte": 999999.0}}}},
+ json={"filters": {"metadata": {"value": {"gte": 999999.0}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2228,7 +2228,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test repeating decimal precision
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"value": {"gte": 0.333}}}},
+ json={"filters": {"metadata": {"value": {"gte": 0.333}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2238,7 +2238,7 @@ async def test_float_precision_edge_cases(client: TestClient):
# Test floating point comparison with string representation
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/ilovefloatingpoints/messages/list",
- json={"filter": {"metadata": {"calculation": "0.1 + 0.2"}}},
+ json={"filters": {"metadata": {"calculation": "0.1 + 0.2"}}},
)
assert response.status_code == 200
data = response.json()
@@ -2336,7 +2336,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test string vs numeric equality
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": 5}}},
+ json={"filters": {"metadata": {"priority": 5}}},
)
assert response.status_code == 200
data = response.json()
@@ -2348,7 +2348,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test string number comparison with numeric operator
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": {"gte": 5}}}},
+ json={"filters": {"metadata": {"priority": {"gte": 5}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2359,7 +2359,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test string boolean vs actual boolean
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"active": True}}},
+ json={"filters": {"metadata": {"active": True}}},
)
assert response.status_code == 200
data = response.json()
@@ -2370,7 +2370,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test explicit string matching
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": "5"}}},
+ json={"filters": {"metadata": {"priority": "5"}}},
)
assert response.status_code == 200
data = response.json()
@@ -2380,7 +2380,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test numeric comparison with string numbers
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"score": {"gt": 10}}}},
+ json={"filters": {"metadata": {"score": {"gt": 10}}}},
)
assert response.status_code == 200
data = response.json()
@@ -2390,7 +2390,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test zero comparisons (string "0" vs integer 0)
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"count": 0}}},
+ json={"filters": {"metadata": {"count": 0}}},
)
assert response.status_code == 200
data = response.json()
@@ -2400,7 +2400,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test null vs string "null"
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"count": None}}},
+ json={"filters": {"metadata": {"count": None}}},
)
assert response.status_code == 200
data = response.json()
@@ -2410,7 +2410,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test leading zeros handling
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": "05"}}},
+ json={"filters": {"metadata": {"priority": "05"}}},
)
assert response.status_code == 200
data = response.json()
@@ -2420,7 +2420,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test case sensitivity for string booleans
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"active": "TRUE"}}},
+ json={"filters": {"metadata": {"active": "TRUE"}}},
)
assert response.status_code == 200
data = response.json()
@@ -2430,7 +2430,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test empty string vs other falsy values
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": ""}}},
+ json={"filters": {"metadata": {"priority": ""}}},
)
assert response.status_code == 200
data = response.json()
@@ -2440,7 +2440,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test special string values
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"score": "NaN"}}},
+ json={"filters": {"metadata": {"score": "NaN"}}},
)
assert response.status_code == 200
data = response.json()
@@ -2450,7 +2450,7 @@ async def test_mixed_type_comparisons(client: TestClient):
# Test mixed type in operator
response = client.post(
f"/v2/workspaces/{workspace_name}/sessions/mixedtypes/messages/list",
- json={"filter": {"metadata": {"priority": {"in": [5, "5", 5.0]}}}},
+ json={"filters": {"metadata": {"priority": {"in": [5, "5", 5.0]}}}},
)
assert response.status_code == 200
data = response.json()
diff --git a/tests/test_llm_mock.py b/tests/test_llm_mock.py
new file mode 100644
index 00000000..7bdab32a
--- /dev/null
+++ b/tests/test_llm_mock.py
@@ -0,0 +1,65 @@
+from datetime import datetime, timezone
+from typing import cast
+from unittest.mock import MagicMock
+
+import pytest
+
+from src.models import Message
+
+
+@pytest.mark.asyncio
+async def test_generic_honcho_llm_call_mock():
+ """Test that the generic honcho_llm_call mock is working for existing decorated functions"""
+ # Import a function that we know is decorated with honcho_llm_call
+ from src.deriver.deriver import critical_analysis_call
+
+ # Call the decorated function - this should use our mock
+ result = await critical_analysis_call(
+ peer_name="test_peer",
+ message_created_at=datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
+ context="test context",
+ history="test history",
+ new_turn="test new turn",
+ )
+
+ # Verify that we get a mock result, not an actual LLM call
+ assert result is not None
+ # The result should have the attributes we expect from our mock
+ assert hasattr(result, "explicit")
+ assert hasattr(result, "deductive")
+ assert hasattr(result, "_response")
+
+
+@pytest.mark.asyncio
+async def test_summarizer_decorated_functions_with_mock():
+ """Test that summarizer decorated functions work with our mock"""
+ # Import functions that we know are decorated with honcho_llm_call
+ from src.utils.summarizer import create_long_summary, create_short_summary
+
+ # Create mock messages for testing
+ mock_message = MagicMock(spec=Message)
+ mock_message.content = "Test message content"
+ mock_message.peer_name = "test_peer"
+ mock_messages = cast(list[Message], [mock_message])
+
+ # Call the decorated functions - these should use our mock
+ short_result = await create_short_summary(
+ messages=mock_messages, input_tokens=100, previous_summary="Previous summary"
+ )
+
+ long_result = await create_long_summary(
+ messages=mock_messages, previous_summary="Previous summary"
+ )
+
+ # Verify that we get mock results, not actual LLM calls
+ assert short_result is not None
+ assert long_result is not None
+ # For functions with return_call_response=True, we should get a string or object with content
+ # The existing mock returns a string, so we check if it's a string
+ assert isinstance(short_result, str | object)
+ assert isinstance(long_result, str | object)
+ # If it's not a string, check for content attribute
+ if not isinstance(short_result, str):
+ assert hasattr(short_result, "content")
+ if not isinstance(long_result, str):
+ assert hasattr(long_result, "content")
diff --git a/uv.lock b/uv.lock
index f158bcef..fb42a7a8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -33,7 +33,7 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.60.0"
+version = "0.61.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -44,14 +44,14 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4e/03/3334921dc54ed822b3dd993ae72d823a7402588521bbba3e024b3333a1fd/anthropic-0.60.0.tar.gz", hash = "sha256:a22ba187c6f4fd5afecb2fc913b960feccf72bc0d25c1b7ce0345e87caede577", size = 425983, upload-time = "2025-07-28T19:53:47.685Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7a/9a/b384758ef93b8f931a523efc8782f7191b175714b3952ff11002899f638b/anthropic-0.61.0.tar.gz", hash = "sha256:af4b3b8f3bc4626cca6af2d412e301974da1747179341ad9e271bdf5cbd2f008", size = 426606, upload-time = "2025-08-05T16:29:37.958Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/da/bb/d84f287fb1c217b30c328af987cf8bbe3897edf0518dcc5fa39412f794ec/anthropic-0.60.0-py3-none-any.whl", hash = "sha256:65ad1f088a960217aaf82ba91ff743d6c89e9d811c6d64275b9a7c59ee9ac3c6", size = 293116, upload-time = "2025-07-28T19:53:45.944Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/07/c7907eee22f5c27a53118dd2576267052ae01f52811dbb06a2848012639e/anthropic-0.61.0-py3-none-any.whl", hash = "sha256:798c8e6cc61e6315143c3f5847d2f220c45f1e69f433436872a237413ca58803", size = 294935, upload-time = "2025-08-05T16:29:36.379Z" },
]
[[package]]
name = "anyio"
-version = "4.9.0"
+version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
@@ -59,9 +59,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
]
[[package]]
@@ -93,14 +93,14 @@ wheels = [
[[package]]
name = "basedpyright"
-version = "1.31.0"
+version = "1.31.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/20/23/6dc0df43c62fdec401b1ec3aea698ba50c5abfca25259e9f0208b34d7abe/basedpyright-1.31.0.tar.gz", hash = "sha256:900a573a525a0f66f884075c2a98711bb9478e44dc60ffdf182ef681bf8e2c76", size = 22062384, upload-time = "2025-07-16T11:37:29.189Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/39/e2870a3739dce055a5b7822d027843c9ba9b3453dcb4b226d9b0e9d486f4/basedpyright-1.31.1.tar.gz", hash = "sha256:4e4d922a385f45dc93e50738d1131ec4533fee5d338b700ef2d28e2e0412e642", size = 22067890, upload-time = "2025-08-03T13:41:15.405Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/85/cb46707458c514ae959fe139135d8f7231d95faf1c383a56979a3436b965/basedpyright-1.31.0-py3-none-any.whl", hash = "sha256:d7460ddcd3a2332b1c3fd738735d18bf2966d49aed67237efa1f19635199d414", size = 11538999, upload-time = "2025-07-16T11:37:26.446Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/cc/8bca3b3a48d6a03a4b857a297fb1473ed1b9fa111be2d20c01f11112e75c/basedpyright-1.31.1-py3-none-any.whl", hash = "sha256:8b647bf07fff929892db4be83a116e6e1e59c13462ecb141214eb271f6785ee5", size = 11540576, upload-time = "2025-08-03T13:41:11.571Z" },
]
[[package]]
@@ -114,11 +114,11 @@ wheels = [
[[package]]
name = "certifi"
-version = "2025.7.14"
+version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
@@ -271,87 +271,87 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.10.1"
+version = "7.10.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/87/0e/66dbd4c6a7f0758a8d18044c048779ba21fb94856e1edcf764bd5403e710/coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57", size = 819938, upload-time = "2025-07-27T14:13:39.045Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/e7/0f4e35a15361337529df88151bddcac8e8f6d6fd01da94a4b7588901c2fe/coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372", size = 214627, upload-time = "2025-07-27T14:11:01.211Z" },
- { url = "https://files.pythonhosted.org/packages/e0/fd/17872e762c408362072c936dbf3ca28c67c609a1f5af434b1355edcb7e12/coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b", size = 215015, upload-time = "2025-07-27T14:11:03.988Z" },
- { url = "https://files.pythonhosted.org/packages/54/50/c9d445ba38ee5f685f03876c0f8223469e2e46c5d3599594dca972b470c8/coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a", size = 241995, upload-time = "2025-07-27T14:11:05.983Z" },
- { url = "https://files.pythonhosted.org/packages/cc/83/4ae6e0f60376af33de543368394d21b9ac370dc86434039062ef171eebf8/coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f", size = 243253, upload-time = "2025-07-27T14:11:07.424Z" },
- { url = "https://files.pythonhosted.org/packages/49/90/17a4d9ac7171be364ce8c0bb2b6da05e618ebfe1f11238ad4f26c99f5467/coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440", size = 245110, upload-time = "2025-07-27T14:11:09.152Z" },
- { url = "https://files.pythonhosted.org/packages/e1/f7/edc3f485d536ed417f3af2b4969582bcb5fab456241721825fa09354161e/coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8", size = 243056, upload-time = "2025-07-27T14:11:10.586Z" },
- { url = "https://files.pythonhosted.org/packages/58/2c/c4c316a57718556b8d0cc8304437741c31b54a62934e7c8c551a7915c2f4/coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c", size = 241731, upload-time = "2025-07-27T14:11:12.145Z" },
- { url = "https://files.pythonhosted.org/packages/f7/93/c78e144c6f086043d0d7d9237c5b880e71ac672ed2712c6f8cca5544481f/coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc", size = 242023, upload-time = "2025-07-27T14:11:13.573Z" },
- { url = "https://files.pythonhosted.org/packages/8f/e1/34e8505ca81fc144a612e1cc79fadd4a78f42e96723875f4e9f1f470437e/coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef", size = 217130, upload-time = "2025-07-27T14:11:15.11Z" },
- { url = "https://files.pythonhosted.org/packages/75/2b/82adfce6edffc13d804aee414e64c0469044234af9296e75f6d13f92f6a2/coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed", size = 218015, upload-time = "2025-07-27T14:11:16.836Z" },
- { url = "https://files.pythonhosted.org/packages/20/8e/ef088112bd1b26e2aa931ee186992b3e42c222c64f33e381432c8ee52aae/coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f", size = 214747, upload-time = "2025-07-27T14:11:18.217Z" },
- { url = "https://files.pythonhosted.org/packages/2d/76/a1e46f3c6e0897758eb43af88bb3c763cb005f4950769f7b553e22aa5f89/coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1", size = 215128, upload-time = "2025-07-27T14:11:19.706Z" },
- { url = "https://files.pythonhosted.org/packages/78/4d/903bafb371a8c887826ecc30d3977b65dfad0e1e66aa61b7e173de0828b0/coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437", size = 245140, upload-time = "2025-07-27T14:11:21.261Z" },
- { url = "https://files.pythonhosted.org/packages/55/f1/1f8f09536f38394a8698dd08a0e9608a512eacee1d3b771e2d06397f77bf/coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7", size = 246977, upload-time = "2025-07-27T14:11:23.15Z" },
- { url = "https://files.pythonhosted.org/packages/57/cc/ed6bbc5a3bdb36ae1bca900bbbfdcb23b260ef2767a7b2dab38b92f61adf/coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770", size = 249140, upload-time = "2025-07-27T14:11:24.743Z" },
- { url = "https://files.pythonhosted.org/packages/10/f5/e881ade2d8e291b60fa1d93d6d736107e940144d80d21a0d4999cff3642f/coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262", size = 246869, upload-time = "2025-07-27T14:11:26.156Z" },
- { url = "https://files.pythonhosted.org/packages/53/b9/6a5665cb8996e3cd341d184bb11e2a8edf01d8dadcf44eb1e742186cf243/coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3", size = 244899, upload-time = "2025-07-27T14:11:27.622Z" },
- { url = "https://files.pythonhosted.org/packages/27/11/24156776709c4e25bf8a33d6bb2ece9a9067186ddac19990f6560a7f8130/coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0", size = 245507, upload-time = "2025-07-27T14:11:29.544Z" },
- { url = "https://files.pythonhosted.org/packages/43/db/a6f0340b7d6802a79928659c9a32bc778ea420e87a61b568d68ac36d45a8/coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be", size = 217167, upload-time = "2025-07-27T14:11:31.349Z" },
- { url = "https://files.pythonhosted.org/packages/f5/6f/1990eb4fd05cea4cfabdf1d587a997ac5f9a8bee883443a1d519a2a848c9/coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c", size = 218054, upload-time = "2025-07-27T14:11:33.202Z" },
- { url = "https://files.pythonhosted.org/packages/b4/4d/5e061d6020251b20e9b4303bb0b7900083a1a384ec4e5db326336c1c4abd/coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293", size = 216483, upload-time = "2025-07-27T14:11:34.663Z" },
- { url = "https://files.pythonhosted.org/packages/a5/3f/b051feeb292400bd22d071fdf933b3ad389a8cef5c80c7866ed0c7414b9e/coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4", size = 214934, upload-time = "2025-07-27T14:11:36.096Z" },
- { url = "https://files.pythonhosted.org/packages/f8/e4/a61b27d5c4c2d185bdfb0bfe9d15ab4ac4f0073032665544507429ae60eb/coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e", size = 215173, upload-time = "2025-07-27T14:11:38.005Z" },
- { url = "https://files.pythonhosted.org/packages/8a/01/40a6ee05b60d02d0bc53742ad4966e39dccd450aafb48c535a64390a3552/coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4", size = 246190, upload-time = "2025-07-27T14:11:39.887Z" },
- { url = "https://files.pythonhosted.org/packages/11/ef/a28d64d702eb583c377255047281305dc5a5cfbfb0ee36e721f78255adb6/coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a", size = 248618, upload-time = "2025-07-27T14:11:41.841Z" },
- { url = "https://files.pythonhosted.org/packages/6a/ad/73d018bb0c8317725370c79d69b5c6e0257df84a3b9b781bda27a438a3be/coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe", size = 250081, upload-time = "2025-07-27T14:11:43.705Z" },
- { url = "https://files.pythonhosted.org/packages/2d/dd/496adfbbb4503ebca5d5b2de8bed5ec00c0a76558ffc5b834fd404166bc9/coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386", size = 247990, upload-time = "2025-07-27T14:11:45.244Z" },
- { url = "https://files.pythonhosted.org/packages/18/3c/a9331a7982facfac0d98a4a87b36ae666fe4257d0f00961a3a9ef73e015d/coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6", size = 246191, upload-time = "2025-07-27T14:11:47.093Z" },
- { url = "https://files.pythonhosted.org/packages/62/0c/75345895013b83f7afe92ec595e15a9a525ede17491677ceebb2ba5c3d85/coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f", size = 247400, upload-time = "2025-07-27T14:11:48.643Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a9/98b268cfc5619ef9df1d5d34fee408ecb1542d9fd43d467e5c2f28668cd4/coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca", size = 217338, upload-time = "2025-07-27T14:11:50.258Z" },
- { url = "https://files.pythonhosted.org/packages/fe/31/22a5440e4d1451f253c5cd69fdcead65e92ef08cd4ec237b8756dc0b20a7/coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3", size = 218125, upload-time = "2025-07-27T14:11:52.034Z" },
- { url = "https://files.pythonhosted.org/packages/d6/2b/40d9f0ce7ee839f08a43c5bfc9d05cec28aaa7c9785837247f96cbe490b9/coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4", size = 216523, upload-time = "2025-07-27T14:11:53.965Z" },
- { url = "https://files.pythonhosted.org/packages/ef/72/135ff5fef09b1ffe78dbe6fcf1e16b2e564cd35faeacf3d63d60d887f12d/coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39", size = 214960, upload-time = "2025-07-27T14:11:55.959Z" },
- { url = "https://files.pythonhosted.org/packages/b1/aa/73a5d1a6fc08ca709a8177825616aa95ee6bf34d522517c2595484a3e6c9/coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7", size = 215220, upload-time = "2025-07-27T14:11:57.899Z" },
- { url = "https://files.pythonhosted.org/packages/8d/40/3124fdd45ed3772a42fc73ca41c091699b38a2c3bd4f9cb564162378e8b6/coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892", size = 245772, upload-time = "2025-07-27T14:12:00.422Z" },
- { url = "https://files.pythonhosted.org/packages/42/62/a77b254822efa8c12ad59e8039f2bc3df56dc162ebda55e1943e35ba31a5/coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7", size = 248116, upload-time = "2025-07-27T14:12:03.099Z" },
- { url = "https://files.pythonhosted.org/packages/1d/01/8101f062f472a3a6205b458d18ef0444a63ae5d36a8a5ed5dd0f6167f4db/coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994", size = 249554, upload-time = "2025-07-27T14:12:04.668Z" },
- { url = "https://files.pythonhosted.org/packages/8f/7b/e51bc61573e71ff7275a4f167aecbd16cb010aefdf54bcd8b0a133391263/coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0", size = 247766, upload-time = "2025-07-27T14:12:06.234Z" },
- { url = "https://files.pythonhosted.org/packages/4b/71/1c96d66a51d4204a9d6d12df53c4071d87e110941a2a1fe94693192262f5/coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7", size = 245735, upload-time = "2025-07-27T14:12:08.305Z" },
- { url = "https://files.pythonhosted.org/packages/13/d5/efbc2ac4d35ae2f22ef6df2ca084c60e13bd9378be68655e3268c80349ab/coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7", size = 247118, upload-time = "2025-07-27T14:12:09.903Z" },
- { url = "https://files.pythonhosted.org/packages/d1/22/073848352bec28ca65f2b6816b892fcf9a31abbef07b868487ad15dd55f1/coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7", size = 217381, upload-time = "2025-07-27T14:12:11.535Z" },
- { url = "https://files.pythonhosted.org/packages/b7/df/df6a0ff33b042f000089bd11b6bb034bab073e2ab64a56e78ed882cba55d/coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e", size = 218152, upload-time = "2025-07-27T14:12:13.182Z" },
- { url = "https://files.pythonhosted.org/packages/30/e3/5085ca849a40ed6b47cdb8f65471c2f754e19390b5a12fa8abd25cbfaa8f/coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4", size = 216559, upload-time = "2025-07-27T14:12:14.807Z" },
- { url = "https://files.pythonhosted.org/packages/cc/93/58714efbfdeb547909feaabe1d67b2bdd59f0597060271b9c548d5efb529/coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72", size = 215677, upload-time = "2025-07-27T14:12:16.68Z" },
- { url = "https://files.pythonhosted.org/packages/c0/0c/18eaa5897e7e8cb3f8c45e563e23e8a85686b4585e29d53cacb6bc9cb340/coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af", size = 215899, upload-time = "2025-07-27T14:12:18.758Z" },
- { url = "https://files.pythonhosted.org/packages/84/c1/9d1affacc3c75b5a184c140377701bbf14fc94619367f07a269cd9e4fed6/coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7", size = 257140, upload-time = "2025-07-27T14:12:20.357Z" },
- { url = "https://files.pythonhosted.org/packages/3d/0f/339bc6b8fa968c346df346068cca1f24bdea2ddfa93bb3dc2e7749730962/coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759", size = 259005, upload-time = "2025-07-27T14:12:22.007Z" },
- { url = "https://files.pythonhosted.org/packages/c8/22/89390864b92ea7c909079939b71baba7e5b42a76bf327c1d615bd829ba57/coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324", size = 261143, upload-time = "2025-07-27T14:12:23.746Z" },
- { url = "https://files.pythonhosted.org/packages/2c/56/3d04d89017c0c41c7a71bd69b29699d919b6bbf2649b8b2091240b97dd6a/coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53", size = 258735, upload-time = "2025-07-27T14:12:25.73Z" },
- { url = "https://files.pythonhosted.org/packages/cb/40/312252c8afa5ca781063a09d931f4b9409dc91526cd0b5a2b84143ffafa2/coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f", size = 256871, upload-time = "2025-07-27T14:12:27.767Z" },
- { url = "https://files.pythonhosted.org/packages/1f/2b/564947d5dede068215aaddb9e05638aeac079685101462218229ddea9113/coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd", size = 257692, upload-time = "2025-07-27T14:12:29.347Z" },
- { url = "https://files.pythonhosted.org/packages/93/1b/c8a867ade85cb26d802aea2209b9c2c80613b9c122baa8c8ecea6799648f/coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c", size = 218059, upload-time = "2025-07-27T14:12:31.076Z" },
- { url = "https://files.pythonhosted.org/packages/a1/fe/cd4ab40570ae83a516bf5e754ea4388aeedd48e660e40c50b7713ed4f930/coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18", size = 219150, upload-time = "2025-07-27T14:12:32.746Z" },
- { url = "https://files.pythonhosted.org/packages/8d/16/6e5ed5854be6d70d0c39e9cb9dd2449f2c8c34455534c32c1a508c7dbdb5/coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4", size = 217014, upload-time = "2025-07-27T14:12:34.406Z" },
- { url = "https://files.pythonhosted.org/packages/54/8e/6d0bfe9c3d7121cf936c5f8b03e8c3da1484fb801703127dba20fb8bd3c7/coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c", size = 214951, upload-time = "2025-07-27T14:12:36.069Z" },
- { url = "https://files.pythonhosted.org/packages/f2/29/e3e51a8c653cf2174c60532aafeb5065cea0911403fa144c9abe39790308/coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e", size = 215229, upload-time = "2025-07-27T14:12:37.759Z" },
- { url = "https://files.pythonhosted.org/packages/e0/59/3c972080b2fa18b6c4510201f6d4dc87159d450627d062cd9ad051134062/coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b", size = 245738, upload-time = "2025-07-27T14:12:39.453Z" },
- { url = "https://files.pythonhosted.org/packages/2e/04/fc0d99d3f809452654e958e1788454f6e27b34e43f8f8598191c8ad13537/coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41", size = 248045, upload-time = "2025-07-27T14:12:41.387Z" },
- { url = "https://files.pythonhosted.org/packages/5e/2e/afcbf599e77e0dfbf4c97197747250d13d397d27e185b93987d9eaac053d/coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f", size = 249666, upload-time = "2025-07-27T14:12:43.056Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ae/bc47f7f8ecb7a06cbae2bf86a6fa20f479dd902bc80f57cff7730438059d/coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1", size = 247692, upload-time = "2025-07-27T14:12:44.83Z" },
- { url = "https://files.pythonhosted.org/packages/b6/26/cbfa3092d31ccba8ba7647e4d25753263e818b4547eba446b113d7d1efdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2", size = 245536, upload-time = "2025-07-27T14:12:46.527Z" },
- { url = "https://files.pythonhosted.org/packages/56/77/9c68e92500e6a1c83d024a70eadcc9a173f21aadd73c4675fe64c9c43fdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4", size = 246954, upload-time = "2025-07-27T14:12:49.279Z" },
- { url = "https://files.pythonhosted.org/packages/7f/a5/ba96671c5a669672aacd9877a5987c8551501b602827b4e84256da2a30a7/coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613", size = 217616, upload-time = "2025-07-27T14:12:51.214Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3c/e1e1eb95fc1585f15a410208c4795db24a948e04d9bde818fe4eb893bc85/coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e", size = 218412, upload-time = "2025-07-27T14:12:53.429Z" },
- { url = "https://files.pythonhosted.org/packages/b0/85/7e1e5be2cb966cba95566ba702b13a572ca744fbb3779df9888213762d67/coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652", size = 216776, upload-time = "2025-07-27T14:12:55.482Z" },
- { url = "https://files.pythonhosted.org/packages/62/0f/5bb8f29923141cca8560fe2217679caf4e0db643872c1945ac7d8748c2a7/coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894", size = 215698, upload-time = "2025-07-27T14:12:57.225Z" },
- { url = "https://files.pythonhosted.org/packages/80/29/547038ffa4e8e4d9e82f7dfc6d152f75fcdc0af146913f0ba03875211f03/coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5", size = 215902, upload-time = "2025-07-27T14:12:59.071Z" },
- { url = "https://files.pythonhosted.org/packages/e1/8a/7aaa8fbfaed900147987a424e112af2e7790e1ac9cd92601e5bd4e1ba60a/coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2", size = 257230, upload-time = "2025-07-27T14:13:01.248Z" },
- { url = "https://files.pythonhosted.org/packages/e5/1d/c252b5ffac44294e23a0d79dd5acf51749b39795ccc898faeabf7bee903f/coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb", size = 259194, upload-time = "2025-07-27T14:13:03.247Z" },
- { url = "https://files.pythonhosted.org/packages/16/ad/6c8d9f83d08f3bac2e7507534d0c48d1a4f52c18e6f94919d364edbdfa8f/coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b", size = 261316, upload-time = "2025-07-27T14:13:04.957Z" },
- { url = "https://files.pythonhosted.org/packages/d6/4e/f9bbf3a36c061e2e0e0f78369c006d66416561a33d2bee63345aee8ee65e/coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea", size = 258794, upload-time = "2025-07-27T14:13:06.715Z" },
- { url = "https://files.pythonhosted.org/packages/87/82/e600bbe78eb2cb0541751d03cef9314bcd0897e8eea156219c39b685f869/coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd", size = 256869, upload-time = "2025-07-27T14:13:08.933Z" },
- { url = "https://files.pythonhosted.org/packages/ce/5d/2fc9a9236c5268f68ac011d97cd3a5ad16cc420535369bedbda659fdd9b7/coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d", size = 257765, upload-time = "2025-07-27T14:13:10.778Z" },
- { url = "https://files.pythonhosted.org/packages/8a/05/b4e00b2bd48a2dc8e1c7d2aea7455f40af2e36484ab2ef06deb85883e9fe/coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47", size = 218420, upload-time = "2025-07-27T14:13:12.882Z" },
- { url = "https://files.pythonhosted.org/packages/77/fb/d21d05f33ea27ece327422240e69654b5932b0b29e7fbc40fbab3cf199bf/coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651", size = 219536, upload-time = "2025-07-27T14:13:14.718Z" },
- { url = "https://files.pythonhosted.org/packages/a6/68/7fea94b141281ed8be3d1d5c4319a97f2befc3e487ce33657fc64db2c45e/coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab", size = 217190, upload-time = "2025-07-27T14:13:16.85Z" },
- { url = "https://files.pythonhosted.org/packages/0f/64/922899cff2c0fd3496be83fa8b81230f5a8d82a2ad30f98370b133c2c83b/coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7", size = 206597, upload-time = "2025-07-27T14:13:37.221Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" },
+ { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" },
+ { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" },
+ { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" },
+ { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" },
+ { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" },
+ { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" },
+ { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" },
+ { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" },
+ { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" },
+ { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" },
+ { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" },
+ { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" },
+ { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" },
+ { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" },
+ { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" },
+ { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" },
+ { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" },
+ { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" },
+ { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" },
]
[package.optional-dependencies]
@@ -361,49 +361,49 @@ toml = [
[[package]]
name = "cryptography"
-version = "45.0.5"
+version = "45.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" },
- { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" },
- { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" },
- { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" },
- { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" },
- { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" },
- { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" },
- { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" },
- { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" },
- { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" },
- { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" },
- { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" },
- { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" },
- { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" },
- { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" },
- { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" },
- { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" },
- { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" },
- { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" },
- { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" },
- { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" },
- { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762, upload-time = "2025-07-02T13:05:53.166Z" },
- { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" },
- { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" },
- { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" },
- { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" },
- { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362, upload-time = "2025-07-02T13:06:04.463Z" },
- { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" },
- { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" },
- { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" },
- { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" },
- { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" },
- { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" },
+ { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" },
+ { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" },
+ { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" },
+ { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" },
+ { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" },
+ { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d2/4482d97c948c029be08cb29854a91bd2ae8da7eb9c4152461f1244dcea70/cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", size = 3576812, upload-time = "2025-08-05T23:59:04.833Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694, upload-time = "2025-08-05T23:59:06.66Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010, upload-time = "2025-08-05T23:59:08.14Z" },
+ { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377, upload-time = "2025-08-05T23:59:09.584Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609, upload-time = "2025-08-05T23:59:11.923Z" },
+ { url = "https://files.pythonhosted.org/packages/78/6d/c49ccf243f0a1b0781c2a8de8123ee552f0c8a417c6367a24d2ecb7c11b3/cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", size = 3322156, upload-time = "2025-08-05T23:59:13.597Z" },
+ { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" },
+ { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" },
]
[[package]]
@@ -460,7 +460,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@@ -591,7 +591,7 @@ grpc = [
[[package]]
name = "google-api-python-client"
-version = "2.177.0"
+version = "2.178.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
@@ -600,9 +600,9 @@ dependencies = [
{ name = "httplib2" },
{ name = "uritemplate" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7a/75/a89cad519fa8910132e3b08571d0e682ae1163643da6f963f1930f3dc788/google_api_python_client-2.177.0.tar.gz", hash = "sha256:9ffd2b57d68f5afa7e6ac64e2c440534eaa056cbb394812a62ff94723c31b50e", size = 13184405, upload-time = "2025-07-23T16:22:46.321Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/99/98/916385a87d145a27661b630c480fadf9db32bb1ad9fb1b13e8dbcbe2af70/google_api_python_client-2.178.0.tar.gz", hash = "sha256:99cba921eb471bb5973b780c653ac54d96eef8a42f1b7375b7ab98f257a4414c", size = 13282628, upload-time = "2025-08-06T14:04:51.062Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/47/f5/121248e18ca605a11720c81ae1b52a5a8cb690af9f01887c56de23cd9a5a/google_api_python_client-2.177.0-py3-none-any.whl", hash = "sha256:f2f50f11105ab883eb9b6cf38ec54ea5fd4b429249f76444bec90deba5be79b3", size = 13709470, upload-time = "2025-07-23T16:22:44.081Z" },
+ { url = "https://files.pythonhosted.org/packages/29/34/8ae31410a2d3f28b16b7135931133caf759d3aa0653f8397e344acec5a88/google_api_python_client-2.178.0-py3-none-any.whl", hash = "sha256:f420adcd050150ff1baefa817e96e1ffa16872744f53471cd34096612e580c34", size = 13809959, upload-time = "2025-08-06T14:04:47.94Z" },
]
[[package]]
@@ -634,7 +634,7 @@ wheels = [
[[package]]
name = "google-genai"
-version = "1.27.0"
+version = "1.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -646,9 +646,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9a/37/6c0ececc3a7a629029b5beed2ceb9f28f73292236eb96272355636769b0d/google_genai-1.27.0.tar.gz", hash = "sha256:15a13ffe7b3938da50b9ab77204664d82122617256f55b5ce403d593848ef635", size = 220099, upload-time = "2025-07-23T22:00:46.145Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/23/f1/039bb08df4670e204c55b5da0b2fa5228dff3346bda01389a86b300f6f58/google_genai-1.28.0.tar.gz", hash = "sha256:e93053c02e616842679ba5ecce5b99db8c0ca6310623c55ff6245b5b1d293138", size = 221029, upload-time = "2025-07-30T21:39:57.002Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/12/279afe7357af73f9737a3412b6f0bc1482075b896340eb46a2f9cb0fd791/google_genai-1.27.0-py3-none-any.whl", hash = "sha256:afd6b4efaf8ec1d20a6e6657d768b68d998d60007c6e220e9024e23c913c1833", size = 218489, upload-time = "2025-07-23T22:00:44.879Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ea/b704df3b348d3ae3572b0db5b52438fa426900b0830cff664107abfdba69/google_genai-1.28.0-py3-none-any.whl", hash = "sha256:7fd506799005cc87d3c5704a2eb5a2cb020d45b4d216a802e606700308f7f2f3", size = 219384, upload-time = "2025-07-30T21:39:55.652Z" },
]
[[package]]
@@ -734,7 +734,7 @@ wheels = [
[[package]]
name = "groq"
-version = "0.30.0"
+version = "0.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -744,9 +744,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/b1/72ca20dc9b977b7f604648e8944c77b267bddeb90d8e16bda0cf0e397844/groq-0.30.0.tar.gz", hash = "sha256:919466e48fcbebef08fed3f71debb0f96b0ea8d2ec77842c384aa843019f6e2c", size = 134928, upload-time = "2025-07-11T20:28:36.583Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/a2/77fd1460e7d55859219223719aa44ae8902a3a1ad333cd5faf330eb0b894/groq-0.31.0.tar.gz", hash = "sha256:182252e9bf0d696df607c137cbafa851d2c84aaf94bcfe9165c0bc231043490c", size = 136237, upload-time = "2025-08-05T23:14:01.183Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/19/b8/5b90edf9fbd795597220e3d1b5534d845e69a73ffe1fdeb967443ed2a6cf/groq-0.30.0-py3-none-any.whl", hash = "sha256:6d9609a7778ba56432f45c1bac21b005f02c6c0aca9c1c094e65536f162c1e83", size = 131056, upload-time = "2025-07-11T20:28:35.591Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/f8/14672d69a91495f43462c5490067eeafc30346e81bda1a62848e897f9bc3/groq-0.31.0-py3-none-any.whl", hash = "sha256:5e3c7ec9728b7cccf913da982a9b5ebb46dc18a070b35e12a3d6a1e12d6b0f7f", size = 131365, upload-time = "2025-08-05T23:13:59.768Z" },
]
[[package]]
@@ -822,7 +822,7 @@ wheels = [
[[package]]
name = "honcho"
-version = "2.1.2"
+version = "2.2.0"
source = { virtual = "." }
dependencies = [
{ name = "alembic" },
@@ -845,6 +845,7 @@ dependencies = [
{ name = "sentry-sdk", extra = ["anthropic", "fastapi", "sqlalchemy"] },
{ name = "sqlalchemy" },
{ name = "tiktoken" },
+ { name = "typing-extensions" },
]
[package.dev-dependencies]
@@ -885,14 +886,15 @@ requires-dist = [
{ name = "sentry-sdk", extras = ["anthropic", "fastapi", "sqlalchemy"], specifier = ">=2.3.1" },
{ name = "sqlalchemy", specifier = ">=2.0.30" },
{ name = "tiktoken", specifier = ">=0.9.0" },
+ { name = "typing-extensions", specifier = ">=4.11.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "basedpyright", specifier = ">=1.29.4" },
{ name = "coverage", specifier = ">=7.6.0" },
- { name = "honcho-ai", specifier = ">=1.2.1" },
- { name = "honcho-core", specifier = ">=1.2.0" },
+ { name = "honcho-ai", specifier = "==1.3.0" },
+ { name = "honcho-core", specifier = "==1.3.0" },
{ name = "interrogate", specifier = ">=1.7.0" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "py-spy", specifier = ">=0.3.14" },
@@ -905,21 +907,21 @@ dev = [
[[package]]
name = "honcho-ai"
-version = "1.2.2"
+version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "honcho-core" },
{ name = "httpx" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/75/8f/e9e2bcad15eafc5e1d2ba5960dfc6790917eda2fde6cf33091bc20175a4b/honcho_ai-1.2.2.tar.gz", hash = "sha256:dd96b6641b16ebbcf0250f0e811632bf6f15640f0a5d9d5341e07ad99b53c25e", size = 24273, upload-time = "2025-07-21T17:39:26.452Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/d3/c882f95637e30124c57a3496ac8221865c2449323f5bd6823009658ce8f7/honcho_ai-1.3.0.tar.gz", hash = "sha256:674440071ae66f3382c7a2355e1c9e8a2c9f0f9482ca0633151d30d42b20ea2c", size = 25042, upload-time = "2025-08-06T21:49:15.655Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/6c/d8a465700dcd1bcc8ef400df9fa7e3cd2ecb1aab8225b6a9f04616fad91f/honcho_ai-1.2.2-py3-none-any.whl", hash = "sha256:c97ef2678bd522e470bedd4cfee17735a063e545346779498ec15b591ab23158", size = 30903, upload-time = "2025-07-21T17:39:25.36Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/f9/0d3c71f4a400da433119eabfb03837a00f1badd31994d578e4fdc9f95c1f/honcho_ai-1.3.0-py3-none-any.whl", hash = "sha256:cb610384956752a8a33f7dee1a01f10b7732c76417b72c87f0d91dea2acd1174", size = 32599, upload-time = "2025-08-06T21:49:14.418Z" },
]
[[package]]
name = "honcho-core"
-version = "1.2.0"
+version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -929,9 +931,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-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]]
@@ -1250,7 +1252,7 @@ wheels = [
[[package]]
name = "mirascope"
-version = "1.25.4"
+version = "1.25.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docstring-parser" },
@@ -1258,9 +1260,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/09/87a2227998cdcbe189a6444463d4e25edff210237caab05d60e7004ccaf7/mirascope-1.25.4.tar.gz", hash = "sha256:6316e988aa7286772488346dfd109b9a23796df162d426d676f74c29cd8653e5", size = 627260, upload-time = "2025-07-07T20:47:12.223Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/bf/7b2441d8740fe82fdd201bcdefacff300ac9f61245169d74d2ebfa607a25/mirascope-1.25.5.tar.gz", hash = "sha256:bcc2734e4f83d6a8d66c7fb1660a12285418ea8f4d52e8f341b8e47ee54c6667", size = 627330, upload-time = "2025-08-05T22:06:41.809Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/97/8d19eec6156dd02d2af384263426aa9f7820827af4830b6f8177e37f0af8/mirascope-1.25.4-py3-none-any.whl", hash = "sha256:ad724539f0307a8ea98425fd285969b608b62aee824967ed2a134147cb43bbf4", size = 373214, upload-time = "2025-07-07T20:47:10.057Z" },
+ { url = "https://files.pythonhosted.org/packages/64/2a/b5d57fcdaa7253cbfb6a2b3100d1e08a2133cf9f44964f8774541d09c36e/mirascope-1.25.5-py3-none-any.whl", hash = "sha256:b7f383ff6f4eeecd80f172fae51718103a473f8213859249014a65d30959226a", size = 373303, upload-time = "2025-08-05T22:06:40.077Z" },
]
[package.optional-dependencies]
@@ -1299,18 +1301,16 @@ wheels = [
[[package]]
name = "nodejs-wheel-binaries"
-version = "22.17.1"
+version = "22.18.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/48/27/97bf86b124b3e385fa39c22480d7fad934c28e6591c869ee8879260329f5/nodejs_wheel_binaries-22.17.1.tar.gz", hash = "sha256:0a8bf2a9d319988b8fa8b8b7bb5a7d986527672e6d6352735714f768af9828d0", size = 8065, upload-time = "2025-07-27T16:02:29.207Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0d/04/814f76f2e5f27eae05aaa6bb8840f01f378e9a441c5ccdbe7999efa7acae/nodejs_wheel_binaries-22.17.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f4d208c0c087a2909b6e9e6e0735da083dc997aa74e9b302703b0798b2faa4c", size = 51003618, upload-time = "2025-07-27T16:01:57.617Z" },
- { url = "https://files.pythonhosted.org/packages/47/db/d0edcebaa420b1079e180959bc841dce638ccc3e45a76b41c4ea7318aa0d/nodejs_wheel_binaries-22.17.1-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:457ada98c6e3e03c7fd3f7d6a55572b70af7155c8dd908246373c63697226db6", size = 51936048, upload-time = "2025-07-27T16:02:01.357Z" },
- { url = "https://files.pythonhosted.org/packages/72/e2/4dc2362718e880341db1105a56d7a58477c53edd36e8aac83a197ba7152d/nodejs_wheel_binaries-22.17.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79a87aeb2f1dfc3d36cd595921f7671a7342467f8b224b56e9049771e5ec20b", size = 57928386, upload-time = "2025-07-27T16:02:04.887Z" },
- { url = "https://files.pythonhosted.org/packages/cc/c2/9ef78ae76cfd4540c0b552f499d3f73b35bd9391ecf13bf558e8ea22cd00/nodejs_wheel_binaries-22.17.1-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05c4eafec348e439d069cd5a114f893c5f7ea898752e34a8aa43aacd39fcf9a3", size = 58461623, upload-time = "2025-07-27T16:02:08.595Z" },
- { url = "https://files.pythonhosted.org/packages/f0/65/7f2adb75571981615a22b4bc3852de1a8cc1cf416de12191665ce5e76119/nodejs_wheel_binaries-22.17.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fc9690ed95b186ef4bb8dd316e83878d016a0b6072454f8f68fa843c1016f85b", size = 59781116, upload-time = "2025-07-27T16:02:12.181Z" },
- { url = "https://files.pythonhosted.org/packages/5b/a9/d3c28d3626bb8639067d479bc9e30127673ce4bc477a2db2e8a4355cafa9/nodejs_wheel_binaries-22.17.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8200379c4c5ec957230285d2e7844f94de87ec0e8316b72b7a5f923cf19e88f2", size = 60832038, upload-time = "2025-07-27T16:02:16.516Z" },
- { url = "https://files.pythonhosted.org/packages/6d/aa/189ebcd6ad819dcacba9e117ae13f4a6d30397d12e862c6db261b63b348a/nodejs_wheel_binaries-22.17.1-py2.py3-none-win_amd64.whl", hash = "sha256:dadc1cf0d5dfacb4dbf2f339d8c070c58e48b37328a44f845de1d27fbbf2381f", size = 40120435, upload-time = "2025-07-27T16:02:23.079Z" },
- { url = "https://files.pythonhosted.org/packages/95/9e/6761d5af600a431862d7f8236e0a3ce326d40d3b33358a6e074d97bb53c6/nodejs_wheel_binaries-22.17.1-py2.py3-none-win_arm64.whl", hash = "sha256:fde8e767272620c58cb882df104462d8f62859223dbf9ab50678d9f0c09dbbf5", size = 38864712, upload-time = "2025-07-27T16:02:25.806Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/6d/773e09de4a052cc75c129c3766a3cf77c36bff8504a38693b735f4a1eb55/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b04495857755c5d5658f7ac969d84f25898fe0b0c1bdc41172e5e0ac6105ca", size = 50873051, upload-time = "2025-08-01T11:10:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/fc/3d6fd4ad5d26c9acd46052190d6a8895dc5050297b03d9cce03def53df0d/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bd4d016257d4dfe604ed526c19bd4695fdc4f4cc32e8afc4738111447aa96d03", size = 51814481, upload-time = "2025-08-01T11:10:33.086Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f9/7be44809a861605f844077f9e731a117b669d5ca6846a7820e7dd82c9fad/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b125f94f3f5e8ab9560d3bd637497f02e45470aeea74cf6fe60afe751cfa5f", size = 57804907, upload-time = "2025-08-01T11:10:36.83Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/67/563e74a0dff653ec7ddee63dc49b3f37a20df39f23675cfc801d7e8e4bb7/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bbb81b6e67c15f04e2a9c6c220d7615fb46ae8f1ad388df0d66abac6bed5f8", size = 58335587, upload-time = "2025-08-01T11:10:40.716Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/b1/ec45fefef60223dd40e7953e2ff087964e200d6ec2d04eae0171d6428679/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5d3ea8b7f957ae16b73241451f6ce831d6478156f363cce75c7ea71cbe6c6f7", size = 59662356, upload-time = "2025-08-01T11:10:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/ed/6de2c73499eebf49d0d20e0704f64566029a3441c48cd4f655d49befd28b/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bcda35b07677039670102a6f9b78c2313fd526111d407cb7ffc2a4c243a48ef9", size = 60706806, upload-time = "2025-08-01T11:10:48.985Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f5/487434b1792c4f28c63876e4a896f2b6e953e2dc1f0b3940e912bd087755/nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1", size = 39998139, upload-time = "2025-08-01T11:10:52.676Z" },
]
[[package]]
@@ -1465,7 +1465,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.97.1"
+version = "1.99.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -1477,9 +1477,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a6/57/1c471f6b3efb879d26686d31582997615e969f3bb4458111c9705e56332e/openai-1.97.1.tar.gz", hash = "sha256:a744b27ae624e3d4135225da9b1c89c107a2a7e5bc4c93e5b7b5214772ce7a4e", size = 494267, upload-time = "2025-07-22T13:10:12.607Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/30/f0fb7907a77e733bb801c7bdcde903500b31215141cdb261f04421e6fbec/openai-1.99.1.tar.gz", hash = "sha256:2c9d8e498c298f51bb94bcac724257a3a6cac6139ccdfc1186c6708f7a93120f", size = 497075, upload-time = "2025-08-05T19:42:36.131Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ee/35/412a0e9c3f0d37c94ed764b8ac7adae2d834dbd20e69f6aca582118e0f55/openai-1.97.1-py3-none-any.whl", hash = "sha256:4e96bbdf672ec3d44968c9ea39d2c375891db1acc1794668d8149d5fa6000606", size = 764380, upload-time = "2025-07-22T13:10:10.689Z" },
+ { url = "https://files.pythonhosted.org/packages/54/15/9c85154ffd283abfc43309ff3aaa63c3fd02f7767ee684e73670f6c5ade2/openai-1.99.1-py3-none-any.whl", hash = "sha256:8eeccc69e0ece1357b51ca0d9fb21324afee09b20c3e5b547d02445ca18a4e03", size = 767827, upload-time = "2025-08-05T19:42:34.192Z" },
]
[[package]]
@@ -1730,17 +1730,17 @@ wheels = [
[[package]]
name = "py-spy"
-version = "0.4.0"
+version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7c/cd/9dacc04604dc4398ce5bed77ed59918ad0940f15165954d4aaa651cc640c/py_spy-0.4.0.tar.gz", hash = "sha256:806602ce7972782cc9c1e383f339bfc27bfb822d42485e6a3e0530ae5040e1f0", size = 253236, upload-time = "2024-11-01T19:08:51.487Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/e2/ff811a367028b87e86714945bb9ecb5c1cc69114a8039a67b3a862cef921/py_spy-0.4.1.tar.gz", hash = "sha256:e53aa53daa2e47c2eef97dd2455b47bb3a7e7f962796a86cc3e7dbde8e6f4db4", size = 244726, upload-time = "2025-07-31T19:33:25.172Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/7e/02ca3ee68507db47afce769504060d71b4dc1455f0f9faa8d32fc7762221/py_spy-0.4.0-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f2cf3f7130e7d780471faa5957441d3b4e0ec39a79b2c00f4c33d494f7728428", size = 3617847, upload-time = "2024-11-01T19:08:37.44Z" },
- { url = "https://files.pythonhosted.org/packages/65/7c/d9e26cc4c8e91f96a3a65de04d2e2e4131fbcaf6830d10917d4fab9d6788/py_spy-0.4.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:47cdda4c34d9b6cb01f3aaeceb2e88faf57da880207fe72ff6ff97e9bb6cc8a9", size = 1761955, upload-time = "2024-11-01T19:08:39.632Z" },
- { url = "https://files.pythonhosted.org/packages/d2/e4/8fbfd219b7f282b80e6b2e74c9197850d2c51db8555705567bb65507b060/py_spy-0.4.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eee3d0bde85ca5cf4f01f012d461180ca76c24835a96f7b5c4ded64eb6a008ab", size = 2059471, upload-time = "2024-11-01T19:08:41.818Z" },
- { url = "https://files.pythonhosted.org/packages/a7/1d/79a94a5ace810c13b730ce96765ca465c171b4952034f1be7402d8accbc1/py_spy-0.4.0-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5f06ffce4c9c98b7fc9f5e67e5e7db591173f1351837633f3f23d9378b1d18a", size = 2067486, upload-time = "2024-11-01T19:08:43.673Z" },
- { url = "https://files.pythonhosted.org/packages/6d/90/fbbb038f826a83ed15ebc4ae606815d6cad6c5c6399c86c7ab96f6c60817/py_spy-0.4.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87573e64dbfdfc89ba2e0f5e2f525aa84e0299c7eb6454b47ea335fde583a7a0", size = 2141433, upload-time = "2024-11-01T19:08:45.988Z" },
- { url = "https://files.pythonhosted.org/packages/c9/c1/5e012669ebb687e546dc99fcfc4861ebfcf3a337b7a41af945df23140bb5/py_spy-0.4.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8bf2f3702cef367a489faa45177b41a6c31b2a3e5bd78c978d44e29340152f5a", size = 2732951, upload-time = "2024-11-01T19:08:48.109Z" },
- { url = "https://files.pythonhosted.org/packages/74/8b/dd8490660019a6b0be28d9ffd2bf1db967604b19f3f2719c0e283a16ac7f/py_spy-0.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:77d8f637ade38367d944874776f45b703b7ac5938b1f7be8891f3a5876ddbb96", size = 1810770, upload-time = "2024-11-01T19:08:50.229Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc", size = 3682508, upload-time = "2025-07-31T19:33:13.753Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/bf/e4d280e9e0bec71d39fc646654097027d4bbe8e04af18fb68e49afcff404/py_spy-0.4.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:1fb8bf71ab8df95a95cc387deed6552934c50feef2cf6456bc06692a5508fd0c", size = 1796395, upload-time = "2025-07-31T19:33:15.325Z" },
+ { url = "https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084", size = 2034938, upload-time = "2025-07-31T19:33:17.194Z" },
+ { url = "https://files.pythonhosted.org/packages/53/a5/36862e3eea59f729dfb70ee6f9e14b051d8ddce1aa7e70e0b81d9fe18536/py_spy-0.4.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:532d3525538254d1859b49de1fbe9744df6b8865657c9f0e444bf36ce3f19226", size = 2658968, upload-time = "2025-07-31T19:33:18.916Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/9ea0b586b065a623f591e5e7961282ec944b5fbbdca33186c7c0296645b3/py_spy-0.4.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4972c21890b6814017e39ac233c22572c4a61fd874524ebc5ccab0f2237aee0a", size = 2147541, upload-time = "2025-07-31T19:33:20.565Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29", size = 2763338, upload-time = "2025-07-31T19:33:22.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl", hash = "sha256:d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc", size = 1818784, upload-time = "2025-07-31T19:33:23.802Z" },
]
[[package]]
@@ -2050,71 +2050,81 @@ wheels = [
[[package]]
name = "regex"
-version = "2024.11.6"
+version = "2025.7.34"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714, upload-time = "2025-07-31T00:21:16.262Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674, upload-time = "2024-11-06T20:08:57.575Z" },
- { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684, upload-time = "2024-11-06T20:08:59.787Z" },
- { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589, upload-time = "2024-11-06T20:09:01.896Z" },
- { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511, upload-time = "2024-11-06T20:09:04.062Z" },
- { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149, upload-time = "2024-11-06T20:09:06.237Z" },
- { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707, upload-time = "2024-11-06T20:09:07.715Z" },
- { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702, upload-time = "2024-11-06T20:09:10.101Z" },
- { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976, upload-time = "2024-11-06T20:09:11.566Z" },
- { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397, upload-time = "2024-11-06T20:09:13.119Z" },
- { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726, upload-time = "2024-11-06T20:09:14.85Z" },
- { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098, upload-time = "2024-11-06T20:09:16.504Z" },
- { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325, upload-time = "2024-11-06T20:09:18.698Z" },
- { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277, upload-time = "2024-11-06T20:09:21.725Z" },
- { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197, upload-time = "2024-11-06T20:09:24.092Z" },
- { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714, upload-time = "2024-11-06T20:09:26.36Z" },
- { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042, upload-time = "2024-11-06T20:09:28.762Z" },
- { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" },
- { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" },
- { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" },
- { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" },
- { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" },
- { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" },
- { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" },
- { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" },
- { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" },
- { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" },
- { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" },
- { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" },
- { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" },
- { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" },
- { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" },
- { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" },
- { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" },
- { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" },
- { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" },
- { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" },
- { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" },
- { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" },
- { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" },
- { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" },
- { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" },
- { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" },
- { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" },
- { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
- { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
- { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
- { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
- { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
- { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
- { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
- { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
- { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
- { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
- { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
- { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
- { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
- { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
- { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
- { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d2/0a44a9d92370e5e105f16669acf801b215107efea9dea4317fe96e9aad67/regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6", size = 484591, upload-time = "2025-07-31T00:18:46.675Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b1/00c4f83aa902f1048495de9f2f33638ce970ce1cf9447b477d272a0e22bb/regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83", size = 289293, upload-time = "2025-07-31T00:18:53.069Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b0/5bc5c8ddc418e8be5530b43ae1f7c9303f43aeff5f40185c4287cf6732f2/regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f", size = 285932, upload-time = "2025-07-31T00:18:54.673Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c7/a1a28d050b23665a5e1eeb4d7f13b83ea86f0bc018da7b8f89f86ff7f094/regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834", size = 780361, upload-time = "2025-07-31T00:18:56.13Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0d/82e7afe7b2c9fe3d488a6ab6145d1d97e55f822dfb9b4569aba2497e3d09/regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f", size = 849176, upload-time = "2025-07-31T00:18:57.483Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/16/3036e16903d8194f1490af457a7e33b06d9e9edd9576b1fe6c7ac660e9ed/regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177", size = 897222, upload-time = "2025-07-31T00:18:58.721Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c2/010e089ae00d31418e7d2c6601760eea1957cde12be719730c7133b8c165/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e", size = 789831, upload-time = "2025-07-31T00:19:00.436Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/86/b312b7bf5c46d21dbd9a3fdc4a80fde56ea93c9c0b89cf401879635e094d/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064", size = 780665, upload-time = "2025-07-31T00:19:01.828Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e5/674b82bfff112c820b09e3c86a423d4a568143ede7f8440fdcbce259e895/regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f", size = 773511, upload-time = "2025-07-31T00:19:03.654Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/18/39e7c578eb6cf1454db2b64e4733d7e4f179714867a75d84492ec44fa9b2/regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d", size = 843990, upload-time = "2025-07-31T00:19:05.61Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/d9/522a6715aefe2f463dc60c68924abeeb8ab6893f01adf5720359d94ede8c/regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03", size = 834676, upload-time = "2025-07-31T00:19:07.023Z" },
+ { url = "https://files.pythonhosted.org/packages/59/53/c4d5284cb40543566542e24f1badc9f72af68d01db21e89e36e02292eee0/regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5", size = 778420, upload-time = "2025-07-31T00:19:08.511Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/4a/b779a7707d4a44a7e6ee9d0d98e40b2a4de74d622966080e9c95e25e2d24/regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3", size = 263999, upload-time = "2025-07-31T00:19:10.072Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/6e/33c7583f5427aa039c28bff7f4103c2de5b6aa5b9edc330c61ec576b1960/regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a", size = 276023, upload-time = "2025-07-31T00:19:11.34Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/fc/00b32e0ac14213d76d806d952826402b49fd06d42bfabacdf5d5d016bc47/regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986", size = 268357, upload-time = "2025-07-31T00:19:12.729Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594, upload-time = "2025-07-31T00:19:13.927Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294, upload-time = "2025-07-31T00:19:15.395Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933, upload-time = "2025-07-31T00:19:16.704Z" },
+ { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335, upload-time = "2025-07-31T00:19:18.561Z" },
+ { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605, upload-time = "2025-07-31T00:19:20.204Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780, upload-time = "2025-07-31T00:19:21.876Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868, upload-time = "2025-07-31T00:19:23.222Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784, upload-time = "2025-07-31T00:19:24.59Z" },
+ { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837, upload-time = "2025-07-31T00:19:25.911Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240, upload-time = "2025-07-31T00:19:27.688Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139, upload-time = "2025-07-31T00:19:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019, upload-time = "2025-07-31T00:19:31.129Z" },
+ { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047, upload-time = "2025-07-31T00:19:32.497Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362, upload-time = "2025-07-31T00:19:34.094Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492, upload-time = "2025-07-31T00:19:35.57Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000, upload-time = "2025-07-31T00:19:37.175Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072, upload-time = "2025-07-31T00:19:38.612Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341, upload-time = "2025-07-31T00:19:40.119Z" },
+ { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556, upload-time = "2025-07-31T00:19:41.556Z" },
+ { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762, upload-time = "2025-07-31T00:19:43Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892, upload-time = "2025-07-31T00:19:44.645Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551, upload-time = "2025-07-31T00:19:46.127Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457, upload-time = "2025-07-31T00:19:47.562Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902, upload-time = "2025-07-31T00:19:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038, upload-time = "2025-07-31T00:19:50.794Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417, upload-time = "2025-07-31T00:19:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387, upload-time = "2025-07-31T00:19:53.593Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482, upload-time = "2025-07-31T00:19:55.183Z" },
+ { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334, upload-time = "2025-07-31T00:19:56.58Z" },
+ { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942, upload-time = "2025-07-31T00:19:57.943Z" },
+ { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991, upload-time = "2025-07-31T00:19:59.837Z" },
+ { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415, upload-time = "2025-07-31T00:20:01.668Z" },
+ { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487, upload-time = "2025-07-31T00:20:03.142Z" },
+ { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717, upload-time = "2025-07-31T00:20:04.727Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943, upload-time = "2025-07-31T00:20:07.1Z" },
+ { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664, upload-time = "2025-07-31T00:20:08.818Z" },
+ { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457, upload-time = "2025-07-31T00:20:10.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008, upload-time = "2025-07-31T00:20:11.823Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101, upload-time = "2025-07-31T00:20:13.729Z" },
+ { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401, upload-time = "2025-07-31T00:20:15.233Z" },
+ { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368, upload-time = "2025-07-31T00:20:16.711Z" },
+ { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482, upload-time = "2025-07-31T00:20:18.189Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/23/6376f3a23cf2f3c00514b1cdd8c990afb4dfbac3cb4a68b633c6b7e2e307/regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a", size = 485385, upload-time = "2025-07-31T00:20:19.692Z" },
+ { url = "https://files.pythonhosted.org/packages/73/5b/6d4d3a0b4d312adbfd6d5694c8dddcf1396708976dd87e4d00af439d962b/regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435", size = 289788, upload-time = "2025-07-31T00:20:21.941Z" },
+ { url = "https://files.pythonhosted.org/packages/92/71/5862ac9913746e5054d01cb9fb8125b3d0802c0706ef547cae1e7f4428fa/regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac", size = 286136, upload-time = "2025-07-31T00:20:26.146Z" },
+ { url = "https://files.pythonhosted.org/packages/27/df/5b505dc447eb71278eba10d5ec940769ca89c1af70f0468bfbcb98035dc2/regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72", size = 797753, upload-time = "2025-07-31T00:20:27.919Z" },
+ { url = "https://files.pythonhosted.org/packages/86/38/3e3dc953d13998fa047e9a2414b556201dbd7147034fbac129392363253b/regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e", size = 863263, upload-time = "2025-07-31T00:20:29.803Z" },
+ { url = "https://files.pythonhosted.org/packages/68/e5/3ff66b29dde12f5b874dda2d9dec7245c2051f2528d8c2a797901497f140/regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751", size = 910103, upload-time = "2025-07-31T00:20:31.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/fe/14176f2182125977fba3711adea73f472a11f3f9288c1317c59cd16ad5e6/regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4", size = 801709, upload-time = "2025-07-31T00:20:33.323Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/0d/80d4e66ed24f1ba876a9e8e31b709f9fd22d5c266bf5f3ab3c1afe683d7d/regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98", size = 786726, upload-time = "2025-07-31T00:20:35.252Z" },
+ { url = "https://files.pythonhosted.org/packages/12/75/c3ebb30e04a56c046f5c85179dc173818551037daae2c0c940c7b19152cb/regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7", size = 857306, upload-time = "2025-07-31T00:20:37.12Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b2/a4dc5d8b14f90924f27f0ac4c4c4f5e195b723be98adecc884f6716614b6/regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47", size = 848494, upload-time = "2025-07-31T00:20:38.818Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/21/9ac6e07a4c5e8646a90b56b61f7e9dac11ae0747c857f91d3d2bc7c241d9/regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e", size = 787850, upload-time = "2025-07-31T00:20:40.478Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6c/d51204e28e7bc54f9a03bb799b04730d7e54ff2718862b8d4e09e7110a6a/regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb", size = 269730, upload-time = "2025-07-31T00:20:42.253Z" },
+ { url = "https://files.pythonhosted.org/packages/74/52/a7e92d02fa1fdef59d113098cb9f02c5d03289a0e9f9e5d4d6acccd10677/regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae", size = 278640, upload-time = "2025-07-31T00:20:44.42Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/78/a815529b559b1771080faa90c3ab401730661f99d495ab0071649f139ebd/regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64", size = 271757, upload-time = "2025-07-31T00:20:46.355Z" },
]
[[package]]
@@ -2267,40 +2277,40 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.12.5"
+version = "0.12.7"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" },
- { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" },
- { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" },
- { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" },
- { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" },
- { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" },
- { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" },
- { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" },
- { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" },
- { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" },
- { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" },
- { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" },
- { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" },
- { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" },
- { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" },
- { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" },
- { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" },
+ { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" },
+ { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" },
+ { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" },
+ { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" },
+ { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" },
+ { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" },
]
[[package]]
name = "sentry-sdk"
-version = "2.33.2"
+version = "2.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b0/82/dfe4a91fd38e048fbb55ca6c072710408e8802015aa27cde18e8684bb1e9/sentry_sdk-2.33.2.tar.gz", hash = "sha256:e85002234b7b8efac9b74c2d91dbd4f8f3970dc28da8798e39530e65cb740f94", size = 335804, upload-time = "2025-07-22T10:41:18.578Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969, upload-time = "2025-07-30T11:13:37.93Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c2/dc/4d825d5eb6e924dfcc6a91c8185578a7b0a5c41fd2416a6f49c8226d6ef9/sentry_sdk-2.33.2-py2.py3-none-any.whl", hash = "sha256:8d57a3b4861b243aa9d558fda75509ad487db14f488cbdb6c78c614979d77632", size = 356692, upload-time = "2025-07-22T10:41:16.531Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743, upload-time = "2025-07-30T11:13:36.145Z" },
]
[package.optional-dependencies]
@@ -2334,47 +2344,47 @@ wheels = [
[[package]]
name = "sqlalchemy"
-version = "2.0.41"
+version = "2.0.42"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" },
- { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" },
- { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" },
- { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" },
- { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" },
- { url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" },
- { url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" },
- { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" },
- { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" },
- { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" },
- { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" },
- { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" },
- { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" },
- { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" },
- { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" },
- { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
- { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
- { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
- { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
- { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
- { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
- { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
- { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
- { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
- { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
- { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
- { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
- { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
- { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
- { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
- { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
- { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/12/33ff43214c2c6cc87499b402fe419869d2980a08101c991daae31345e901/sqlalchemy-2.0.42-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:172b244753e034d91a826f80a9a70f4cbac690641207f2217f8404c261473efe", size = 2130469, upload-time = "2025-07-29T13:25:15.215Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c4/4d2f2c21ddde9a2c7f7b258b202d6af0bac9fc5abfca5de367461c86d766/sqlalchemy-2.0.42-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be28f88abd74af8519a4542185ee80ca914933ca65cdfa99504d82af0e4210df", size = 2120393, upload-time = "2025-07-29T13:25:16.367Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0d/5ff2f2dfbac10e4a9ade1942f8985ffc4bd8f157926b1f8aed553dfe3b88/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b344859d282fde388047f1710860bb23f4098f705491e06b8ab52a48aafea9", size = 3206173, upload-time = "2025-07-29T13:29:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/59/71493fe74bd76a773ae8fa0c50bfc2ccac1cbf7cfa4f9843ad92897e6dcf/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97978d223b11f1d161390a96f28c49a13ce48fdd2fed7683167c39bdb1b8aa09", size = 3206910, upload-time = "2025-07-29T13:24:50.58Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/51/01b1d85bbb492a36b25df54a070a0f887052e9b190dff71263a09f48576b/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e35b9b000c59fcac2867ab3a79fc368a6caca8706741beab3b799d47005b3407", size = 3145479, upload-time = "2025-07-29T13:29:02.3Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/78/10834f010e2a3df689f6d1888ea6ea0074ff10184e6a550b8ed7f9189a89/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bc7347ad7a7b1c78b94177f2d57263113bb950e62c59b96ed839b131ea4234e1", size = 3169605, upload-time = "2025-07-29T13:24:52.135Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/75/e6fdd66d237582c8488dd1dfa90899f6502822fbd866363ab70e8ac4a2ce/sqlalchemy-2.0.42-cp310-cp310-win32.whl", hash = "sha256:739e58879b20a179156b63aa21f05ccacfd3e28e08e9c2b630ff55cd7177c4f1", size = 2098759, upload-time = "2025-07-29T13:23:55.809Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/a8/366db192641c2c2d1ea8977e7c77b65a0d16a7858907bb76ea68b9dd37af/sqlalchemy-2.0.42-cp310-cp310-win_amd64.whl", hash = "sha256:1aef304ada61b81f1955196f584b9e72b798ed525a7c0b46e09e98397393297b", size = 2122423, upload-time = "2025-07-29T13:23:56.968Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/3c/7bfd65f3c2046e2fb4475b21fa0b9d7995f8c08bfa0948df7a4d2d0de869/sqlalchemy-2.0.42-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c34100c0b7ea31fbc113c124bcf93a53094f8951c7bf39c45f39d327bad6d1e7", size = 2133779, upload-time = "2025-07-29T13:25:18.446Z" },
+ { url = "https://files.pythonhosted.org/packages/66/17/19be542fe9dd64a766090e90e789e86bdaa608affda6b3c1e118a25a2509/sqlalchemy-2.0.42-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad59dbe4d1252448c19d171dfba14c74e7950b46dc49d015722a4a06bfdab2b0", size = 2123843, upload-time = "2025-07-29T13:25:19.749Z" },
+ { url = "https://files.pythonhosted.org/packages/14/fc/83e45fc25f0acf1c26962ebff45b4c77e5570abb7c1a425a54b00bcfa9c7/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9187498c2149919753a7fd51766ea9c8eecdec7da47c1b955fa8090bc642eaa", size = 3294824, upload-time = "2025-07-29T13:29:03.879Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/81/421efc09837104cd1a267d68b470e5b7b6792c2963b8096ca1e060ba0975/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f092cf83ebcafba23a247f5e03f99f5436e3ef026d01c8213b5eca48ad6efa9", size = 3294662, upload-time = "2025-07-29T13:24:53.715Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/ba/55406e09d32ed5e5f9e8aaec5ef70c4f20b4ae25b9fa9784f4afaa28e7c3/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc6afee7e66fdba4f5a68610b487c1f754fccdc53894a9567785932dbb6a265e", size = 3229413, upload-time = "2025-07-29T13:29:05.638Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/c4/df596777fce27bde2d1a4a2f5a7ddea997c0c6d4b5246aafba966b421cc0/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:260ca1d2e5910f1f1ad3fe0113f8fab28657cee2542cb48c2f342ed90046e8ec", size = 3255563, upload-time = "2025-07-29T13:24:55.17Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ed/b9c4a939b314400f43f972c9eb0091da59d8466ef9c51d0fd5b449edc495/sqlalchemy-2.0.42-cp311-cp311-win32.whl", hash = "sha256:2eb539fd83185a85e5fcd6b19214e1c734ab0351d81505b0f987705ba0a1e231", size = 2098513, upload-time = "2025-07-29T13:23:58.946Z" },
+ { url = "https://files.pythonhosted.org/packages/91/72/55b0c34e39feb81991aa3c974d85074c356239ac1170dfb81a474b4c23b3/sqlalchemy-2.0.42-cp311-cp311-win_amd64.whl", hash = "sha256:9193fa484bf00dcc1804aecbb4f528f1123c04bad6a08d7710c909750fa76aeb", size = 2123380, upload-time = "2025-07-29T13:24:00.155Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/ac31a9821fc70a7376321fb2c70fdd7eadbc06dadf66ee216a22a41d6058/sqlalchemy-2.0.42-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09637a0872689d3eb71c41e249c6f422e3e18bbd05b4cd258193cfc7a9a50da2", size = 2132203, upload-time = "2025-07-29T13:29:19.291Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ba/fd943172e017f955d7a8b3a94695265b7114efe4854feaa01f057e8f5293/sqlalchemy-2.0.42-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3cb3ec67cc08bea54e06b569398ae21623534a7b1b23c258883a7c696ae10df", size = 2120373, upload-time = "2025-07-29T13:29:21.049Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/a2/b5f7d233d063ffadf7e9fff3898b42657ba154a5bec95a96f44cba7f818b/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87e6a5ef6f9d8daeb2ce5918bf5fddecc11cae6a7d7a671fcc4616c47635e01", size = 3317685, upload-time = "2025-07-29T13:26:40.837Z" },
+ { url = "https://files.pythonhosted.org/packages/86/00/fcd8daab13a9119d41f3e485a101c29f5d2085bda459154ba354c616bf4e/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b718011a9d66c0d2f78e1997755cd965f3414563b31867475e9bc6efdc2281d", size = 3326967, upload-time = "2025-07-29T13:22:31.009Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/85/e622a273d648d39d6771157961956991a6d760e323e273d15e9704c30ccc/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16d9b544873fe6486dddbb859501a07d89f77c61d29060bb87d0faf7519b6a4d", size = 3255331, upload-time = "2025-07-29T13:26:42.579Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a0/2c2338b592c7b0a61feffd005378c084b4c01fabaf1ed5f655ab7bd446f0/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21bfdf57abf72fa89b97dd74d3187caa3172a78c125f2144764a73970810c4ee", size = 3291791, upload-time = "2025-07-29T13:22:32.454Z" },
+ { url = "https://files.pythonhosted.org/packages/41/19/b8a2907972a78285fdce4c880ecaab3c5067eb726882ca6347f7a4bf64f6/sqlalchemy-2.0.42-cp312-cp312-win32.whl", hash = "sha256:78b46555b730a24901ceb4cb901c6b45c9407f8875209ed3c5d6bcd0390a6ed1", size = 2096180, upload-time = "2025-07-29T13:16:08.952Z" },
+ { url = "https://files.pythonhosted.org/packages/48/1f/67a78f3dfd08a2ed1c7be820fe7775944f5126080b5027cc859084f8e223/sqlalchemy-2.0.42-cp312-cp312-win_amd64.whl", hash = "sha256:4c94447a016f36c4da80072e6c6964713b0af3c8019e9c4daadf21f61b81ab53", size = 2123533, upload-time = "2025-07-29T13:16:11.705Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" },
+ { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" },
+ { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" },
]
[[package]]
@@ -2422,38 +2432,38 @@ wheels = [
[[package]]
name = "tiktoken"
-version = "0.9.0"
+version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "regex" },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/6a/e0f66d5cf612979fec2c18f9069962c73ed51949403967ba0de9ea6859e5/tiktoken-0.10.0.tar.gz", hash = "sha256:7cd88c11699b18081822e6ae1beee55e8f20ea361d73c507d33f5a89a1898f1c", size = 36451, upload-time = "2025-08-05T17:58:07.619Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/64/f3/50ec5709fad61641e4411eb1b9ac55b99801d71f1993c29853f256c726c9/tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382", size = 1065770, upload-time = "2025-02-14T06:02:01.251Z" },
- { url = "https://files.pythonhosted.org/packages/d6/f8/5a9560a422cf1755b6e0a9a436e14090eeb878d8ec0f80e0cd3d45b78bf4/tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108", size = 1009314, upload-time = "2025-02-14T06:02:02.869Z" },
- { url = "https://files.pythonhosted.org/packages/bc/20/3ed4cfff8f809cb902900ae686069e029db74567ee10d017cb254df1d598/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd", size = 1143140, upload-time = "2025-02-14T06:02:04.165Z" },
- { url = "https://files.pythonhosted.org/packages/f1/95/cc2c6d79df8f113bdc6c99cdec985a878768120d87d839a34da4bd3ff90a/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de", size = 1197860, upload-time = "2025-02-14T06:02:06.268Z" },
- { url = "https://files.pythonhosted.org/packages/c7/6c/9c1a4cc51573e8867c9381db1814223c09ebb4716779c7f845d48688b9c8/tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990", size = 1259661, upload-time = "2025-02-14T06:02:08.889Z" },
- { url = "https://files.pythonhosted.org/packages/cd/4c/22eb8e9856a2b1808d0a002d171e534eac03f96dbe1161978d7389a59498/tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4", size = 894026, upload-time = "2025-02-14T06:02:12.841Z" },
- { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987, upload-time = "2025-02-14T06:02:14.174Z" },
- { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155, upload-time = "2025-02-14T06:02:15.384Z" },
- { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898, upload-time = "2025-02-14T06:02:16.666Z" },
- { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535, upload-time = "2025-02-14T06:02:18.595Z" },
- { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548, upload-time = "2025-02-14T06:02:20.729Z" },
- { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895, upload-time = "2025-02-14T06:02:22.67Z" },
- { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" },
- { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" },
- { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" },
- { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" },
- { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" },
- { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" },
- { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919, upload-time = "2025-02-14T06:02:37.494Z" },
- { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877, upload-time = "2025-02-14T06:02:39.516Z" },
- { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095, upload-time = "2025-02-14T06:02:41.791Z" },
- { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649, upload-time = "2025-02-14T06:02:43Z" },
- { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465, upload-time = "2025-02-14T06:02:45.046Z" },
- { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/69/68ac82e68cd03df75b4fbe280331ad5f1bab879464d679c5b37fdafcb4e6/tiktoken-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1db7a65b196d757d18ef53193957d44549b88f373d4b87db532f04d18193b847", size = 1047220, upload-time = "2025-08-05T17:57:27.463Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/1a/6396b6987d6dd1508fddc645fd41ae1b505c4afd2c5ed310f28fb7fbc2b6/tiktoken-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f55701461267d025597ebb2290d612fe9c5c5fbb625ebf7495c9f0f8e4c30f01", size = 989183, upload-time = "2025-08-05T17:57:29.134Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/75/1ddd7ee07332966a5686c5dab91bb862d41edc7b144b405104c6e5993f41/tiktoken-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83693279af9e8deac0363cbf21dc3d666807f22dcc1091f51e69e6fe6433f71", size = 1120360, upload-time = "2025-08-05T17:57:32.066Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/14/6083daba660141b580b7da6ee2b3ed7084c73822ad52739071164ea26786/tiktoken-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f973bdeb68d645f73dcce60c7795cb5c6f8b7f3dcf92c40c39ad4aee398c075", size = 1175483, upload-time = "2025-08-05T17:57:33.464Z" },
+ { url = "https://files.pythonhosted.org/packages/df/19/9a16880dbf592007eec7a7010f42225253eb669c1787d2f6b1bd5a84e25e/tiktoken-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5cab2a8e8974d6c9744264e94886a58b9087a39737c53fd65dbe2fe8522e719", size = 1237876, upload-time = "2025-08-05T17:57:35.065Z" },
+ { url = "https://files.pythonhosted.org/packages/92/2a/d80d05a53a21f4da7b38c975f220639e2829bd598b17028ccaca64db679a/tiktoken-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a49d2aedf911b68f23c9c84abce8eaf569cb02613f307cee4fb1bebd8c55ae9", size = 876340, upload-time = "2025-08-05T17:57:36.447Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/ba/d392a2ffa94dbbcf1cd187b70df18f61a96f76d1de9d5d2bc8414b402d05/tiktoken-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:29f55c6b7c9a6a7ea953691c3962ee8fe4ee2f0ceb2a3fded3925acfc45e4b0a", size = 1047150, upload-time = "2025-08-05T17:57:37.459Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e0/86b36df9ab5f014ef976f75b000b9daf06c6edff229186e05e56b94f4cdf/tiktoken-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f641d0735059b48252c4409d6546b6a62edeb4c4e48306499db11bbe403b872f", size = 988709, upload-time = "2025-08-05T17:57:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/43/24813c6e7db16314229b5cd13bc23719a58401242dd42be2bac3b36516d4/tiktoken-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76e03676b36c2a5e5304b428cff48eb86a033af55212d41a6ac6faec25b2ad", size = 1120621, upload-time = "2025-08-05T17:57:40.209Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/94/fd4156ccc20d4c7986352353385d768307e4c6d8e2502c3d6ee061847474/tiktoken-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ecbd4dc4db3b82ca3421f50d1207d750de1a2856e6ca0544294fe58d2853", size = 1175233, upload-time = "2025-08-05T17:57:41.354Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/db/32f4b4f1c13f84118924258b7a92c2a20a1d72f974abcdd73c3cd6308983/tiktoken-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d2e032cd44700a9ed511c50b9f302bb9e9e77e2ebf8e2d9b8ec12ce64ebf00c6", size = 1237489, upload-time = "2025-08-05T17:57:42.556Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a4/dc6558ce48a6c339a30a9e532bb5767e0ff511f8f948bca1340f57aa2fea/tiktoken-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb520960fa1788caf033489e3b63bb675c0f321db40569b2d3ca24d7fab5ca72", size = 876253, upload-time = "2025-08-05T17:57:43.91Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/8a/2dad6ea056f88f12602bdbdba19ed1d29a574bb8dbe034af0842eb2ee29f/tiktoken-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c27021a605b2778af07f84a33fe7c74b5c960a8cfac5d2a7a1075ed592cbe4", size = 1046690, upload-time = "2025-08-05T17:57:45.213Z" },
+ { url = "https://files.pythonhosted.org/packages/84/88/b0ec9383e7126f7cf033971dac81c4dea0954607db8c2fbfe109061bf730/tiktoken-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:45d654f049b4f7ed617ad0c66462c15cc41e5db120f37f122d5b57ffa2aec062", size = 988269, upload-time = "2025-08-05T17:57:46.645Z" },
+ { url = "https://files.pythonhosted.org/packages/28/83/ed1900dd68890c891f55ded1f760f7878c5e16c8c68e576145105d66aa63/tiktoken-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34f73d93641b9cc73e19b18dbf9ce1baf086f1c4e18cfa36b952956843bca23c", size = 1118515, upload-time = "2025-08-05T17:57:48.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a8/84ad6c23cfd51d2c39ba1bfbc70b6208c6be3e5cd15132a9d11cc93bfdd5/tiktoken-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fbfc5dda624c0f85c56bbf8f2ffb42964c798fc6fc1c321a452bf31d4ba21f5", size = 1174193, upload-time = "2025-08-05T17:57:49.486Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/b5/7be4c78a10566ae068d88788a6fdf3a8b1162e7f78997999c46c919cdb69/tiktoken-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:853671af10636b581d632fe7b64b755301c73bc1f2ce837a8cdd9a44ab51f846", size = 1235834, upload-time = "2025-08-05T17:57:50.503Z" },
+ { url = "https://files.pythonhosted.org/packages/55/da/a0e90c85f36bd21ef4dbe881f9e259403c6fd6287841ff3691c1d2a5baab/tiktoken-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:56c4e7544be63c78265c82857b9f2a4e2190ae29541fe3467708fc7aabcb1522", size = 875661, upload-time = "2025-08-05T17:57:52.091Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/44/03c259d8df262181f0efab0159373435284f1074adf966418663879023e1/tiktoken-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10e779827ddb0490a415d09d95f8e7a982fd8e14f88c4c2348358f722af3c415", size = 1046536, upload-time = "2025-08-05T17:57:53.162Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/16/98ee535d30f7abf528ac25e78d67171cc2dfc1366d762cf79eb0dcc9054d/tiktoken-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:783892cdbec0d33f0f51373d8969e059ab1244af531a52338f702131a032a116", size = 987947, upload-time = "2025-08-05T17:57:54.266Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/bd/c3847b55e4fd6c6ab89ab75d258c725c67890734a4e58e28315ae2208ee1/tiktoken-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43f38d62b6a62a7c82be3770fcdc24cdf5bff3bdf718909d038b804ac774a025", size = 1118091, upload-time = "2025-08-05T17:57:55.383Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/07/9a2c6e1d1ef138f79a6c943bf1c7c7e7cc159e2b7fbbe6ce5d176f963232/tiktoken-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72d0b30e08f1e856fa76ee423a3d3f0b1bc984cb6e86a21dbd7c5eee8f67f8f5", size = 1174129, upload-time = "2025-08-05T17:57:56.739Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/4f/ae9e36e43c0abbeb631b848697b22662cca5dbbe90a1b692b5c0ceda0fac/tiktoken-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa840eba09a0c1db0b436baf6cfe1ab7906f33fb663cf96f772a818bad5856a6", size = 1236097, upload-time = "2025-08-05T17:57:58.007Z" },
+ { url = "https://files.pythonhosted.org/packages/10/74/c6f69e46f98e42d6727853e4017d6287b57075905ff715fae0f35fa53173/tiktoken-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:642b8d9f266004629ea2457aa29a9eed4f1a031fd804f8c489edbf32df7026c3", size = 876020, upload-time = "2025-08-05T17:57:59.52Z" },
]
[[package]]
@@ -2629,16 +2639,16 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "20.32.0"
+version = "20.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" },
]
[[package]]