Ben/search rrf (#179)

* chore: fill out missing metadata inputs in python sdk

* feat: add get_peer_config to python sdk, thoroughly document ts sdk and remove bad client usage

* feat: zod
chore: update tests
chore: bump version, changelog

* chore: python sdk version bump and changelog

* [WIP] feat: combine search methods and rework endpoint to include limit param

* chore: test new stainless config with library

* nits: coderabbit

* Merge branch 'ben/sdk-improvements' into ben/search-rrf

* chore: pre-commit hooks cleanup

* feat: thoroughly document observation config

* Update sdks/python/src/honcho/peer.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: v1.3.0

* feat: update version to 2.2.0 and enhance search functionality with arbitrary filters

- Remove unused config variables
- Added arbitrary filters to all search endpoints.
- Pluralize `filters` everywhere in SDKs for consistency
- Updated documentation and changelog to reflect these changes.

* expose core client in TS and Python SDKs (#150)

* expose core client from sdks

* align text

* fix: resolve get_effective_observe me race condition, default peer config (#176)

* fix: resolve get_effective_observe me race condition, default peer config

* fix: preserve custom config even after leaving

* chore: test cases, enqueue types

* Update sdks/typescript/package.json

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: formatting

* chore: revert undesired changes to v1 spec, clean up docs, coderabbit

* feat: better search docs, fix worker.ts

* fix: correctly make ts params optional in cases, update docs

* chore: coderabbit

* chore: remove spurious package-lock

* fix: asyncify examples, use limit properly in search

* fix(tests): handle 4 return values in test_get_session_peer_configuration

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
This commit is contained in:
doria 2025-08-06 17:43:03 -04:00 committed by GitHub
parent ddc16cfc21
commit 7b174dd34b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 4198 additions and 2540 deletions

View File

@ -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

View File

@ -5,6 +5,17 @@ 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
### Changed
- Search has 10 results by default, max 100 results
## [2.1.2] - 2025-07-30
### Fixed

View File

@ -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)

View File

@ -1,6 +1,6 @@
# 🫡 Honcho
![Static Badge](https://img.shields.io/badge/Version-2.1.2-blue)
![Static Badge](https://img.shields.io/badge/Version-2.2.0-blue)
[![Discord](https://img.shields.io/discord/1016845111637839922?style=flat&logo=discord&logoColor=23ffffff&label=Plastic%20Labs&labelColor=235865F2)](https://discord.gg/plasticlabs)
[![arXiv](https://img.shields.io/badge/arXiv-2310.06983-b31b1b.svg)](https://arxiv.org/abs/2310.06983)
![GitHub License](https://img.shields.io/github/license/plastic-labs/honcho)

View File

@ -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]

View File

@ -8,7 +8,28 @@ This guide helps you understand which versions of Honcho's API are compatible wi
## Version Compatibility
### Honcho API v2.1.2 (Current)
### Honcho API v2.2.0 (Current)
<CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js">
**Compatible Version:** v1.3.0
Install with:
```bash
npm install @honcho-ai/sdk@1.3.0
```
</Card>
<Card title="Python SDK" icon="python">
**Compatible Version:** v1.3.0
Install with:
```bash
pip install honcho-ai==1.3.0
```
</Card>
</CardGroup>
### Honcho API v2.1.2
<CardGroup cols={2}>
<Card title="TypeScript SDK" icon="js">
@ -35,7 +56,8 @@ This guide helps you understand which versions of Honcho's API are compatible wi
| Honcho API Version | TypeScript SDK | Python SDK |
|-------------------|---------------|------------|
| v2.1.2 (Current) | v1.2.1 | v1.2.2 |
| v2.2.0 (Current) | v1.3.0 | v1.3.0 |
| v2.1.2 | v1.2.1 | v1.2.2 |
| v2.1.1 | v1.2.1 | v1.2.2 |
| v2.1.0 | v1.2.1 | v1.2.2 |
| v2.0.5 | v1.1.0 | v1.1.0 |

View File

@ -27,7 +27,20 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
### Honcho API and SDK Changelogs
<Tabs>
<Tab title="Honcho API">
<Update label="v2.1.2 (Current)">
<Update label="v2.2.0 (Current)">
### Added
- Arbitrary filters now available on all search endpoints
- Search combines full-text and semantic using [reciprocal rank fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf)
### Changed
- API uses 'filters' instead of 'filter' everywhere for consistency and better behavior with Python native types (breaking change)
- Search has `limit` parameter: 10 results by default, max 100 results
</Update>
<Update label="v2.1.2">
### Fixed
- Summarizer module to ignore empty summaries and pass appropriate one to get_context

View File

@ -19,7 +19,7 @@
"navigation": {
"versions": [
{
"version": "v2.1.2",
"version": "v2.2.0",
"api": {
"openapi": [
"openapi.documented.yml"

View File

@ -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)

View File

@ -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": {

View File

@ -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

View File

@ -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

View File

@ -211,9 +211,11 @@ backchannel directly with Honcho, via MCP or a direct API call.
Developers should frame the Dialectic as talking to an expert on the Peer rather than addressing the Peer itself, meaning:
```python
alice.chat("What is alice's mood like") # ✅ Correct
alice.chat("What is the user's mood today?") # ✅ Ideal
alice.chat("What is your mood like") # ❌ Wrong
alice.chat("What is alice's mood today?") # ✅ Works -- but make sure to consider what peer "Alice" has been saying in their messages about name/identity.
alice.chat("What is your mood today?") # ❌ Likely to fail -- the dialectic agent may conflate itself and the user.
```
<Note>

View File

@ -0,0 +1,156 @@
---
title: 'Configuration'
description: 'Customizing how Honcho handles peers and sessions'
icon: 'wrench'
---
Entities in Honcho can sometimes be configured to change the behavior of the deriver, which is responsible for generating and storing facts, summaries, and user representations.
These configurations can be set at the peer, session, and session-peer level (AKA the state of a peer within a specific session).
### Peer Configuration
By default, all peers are "observed" by Honcho. This means that Honcho will derive facts from messages sent by the peer and generate a representation of them. In most cases, this is why you use Honcho! However, sometimes an application requires a peer that should not be observed: for example, an assistant or game NPC that your program will never need to ask questions about.
You may therefore disable observation of a peer by setting the `observe_me` flag in their configuration to `false`.
If the peer has a session-level configuration, it will override this configuration. If the flag is not set, or is set to `true`, the peer will be observed.
<CodeGroup>
```python Python
from honcho import Honcho
# Initialize client
honcho = Honcho()
# Create peer with configuration
peer = honcho.peer("my-peer", config={"observe_me": False})
# Change peer's configuration
peer.set_peer_config({"observe_me": True})
# Note: creating the same peer again will also replace the configuration
peer = honcho.peer("my-peer", config={"observe_me": False})
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
(async () => {
// Initialize client
const honcho = new Honcho({});
// Create peer with configuration
const peer = await honcho.peer("my-peer", { config: { observe_me: false } });
// Change peer's configuration
await peer.setPeerConfig({ observe_me: true });
// Note: creating the same peer again will also replace the configuration
await honcho.peer("my-peer", { config: { observe_me: false } });
})();
```
</CodeGroup>
### Session Configuration
By default, all sessions have the deriver enabled, much like peers. You may create a session that escapes the deriver's watchful eye by setting the `deriver_disabled` flag to `true`. You can update the flag by calling `get_or_create` on the session with a new configuration.
<CodeGroup>
```python Python
from honcho import Honcho
# Initialize client
honcho = Honcho()
# Create session with configuration
session = honcho.session("my-session", config={"deriver_disabled": True})
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
(async () => {
// Initialize client
const honcho = new Honcho({});
// Create session with configuration
const session = await honcho.session("my-session", { config: { deriver_disabled: true } });
})();
```
</CodeGroup>
### Session-Peer Configuration
Configuration at the session-peer level is the most common use case for configuration flags. You will often want to arrange a session such that certain peers observe others in order to form "local representations" of them. There are two flags that can be set at the session-peer level:
- `observe_me`: Whether this peer should *be observed* by others in the session. By default, this is `true`. This overrides the peer-level `observe_me` flag.
- `observe_others`: Whether this peer should produce local representations of others in the session. By default, this is `false`. Other peers will only be observed if their `observe_me` flag is `true`.
You can combine these flags across multiple peers to arrange any possible permutation of directional observation. Note that in the default case, no local representations are produced. To produce local representations, you must set the `observe_others` flag to `true` for at least one peer in the session and at least one other peer must have their `observe_me` flag set to `true`.
Many applications will work best without local representations, preferring to chat with Honcho's top-down representation of each peer. Only enable local representations via the `observe_others` flag if you are doing advanced reasoning on user perspectives.
<img src="/images/local-vs-global-reps.png" alt="Peer Representations" />
You can dynamically change the configuration of a session-peer by calling `set_peer_config` on the session with the peer and the configuration you want to set.
<CodeGroup>
```python Python
from honcho import Honcho
# Initialize client
honcho = Honcho()
# Create session
session = honcho.session("my-session")
# Create peers
alice = honcho.peer("alice")
bob = honcho.peer("bob")
# Add peers to session with default configuration
session.add_peers([alice, bob])
# Add another peer to the session with a custom configuration
charlie = honcho.peer("charlie")
session.add_peers([charlie, {"observe_me": False, "observe_others": True}])
# Set session-peer configuration
session.set_peer_config(alice, {"observe_others": True})
session.set_peer_config(bob, {"observe_me": False})
# Get session-peer configuration
charlie_config = session.get_peer_config(charlie)
print(charlie_config)
```
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
(async () => {
// Initialize client
const honcho = new Honcho({});
// Create session
const session = await honcho.session("my-session");
// Create peers
const alice = await honcho.peer("alice");
const bob = await honcho.peer("bob");
// Add peers to session
await session.addPeers([alice, bob]);
// Add another peer to the session with a custom configuration
const charlie = await honcho.peer("charlie");
await session.addPeers([charlie, { observe_me: false, observe_others: true }]);
// Set session-peer configuration
await session.setPeerConfig(alice, { observe_others: true });
await session.setPeerConfig(bob, { observe_me: false });
// Get session-peer configuration
const charlieConfig = await session.getPeerConfig(charlie);
console.log(charlieConfig);
})();
```
</CodeGroup>

View File

@ -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.

View File

@ -67,4 +67,4 @@ cognitive functions.
continually generating & updating internal world models to anticipate sensory input, rather than
passively receiving it--closely linked to Bayesian brain hypotheses, which hold that the brain
interprets the world probabilistically, weighing prior knowledge against new evidence to minimize
uncertainty.
uncertainty.

View File

@ -14,7 +14,7 @@ Almost all agents require, in addition to personalization and memory, a way to q
### Creating Summaries
Honcho already has an asynchronous task queue for the purpose of deriving facts from messages. This is the ideal place to create summaries where they won't add latency to a message. Currently, Honcho has two configurable summary types:
Honcho already has an asynchronous task queue for the purpose of deriving facts from messages. This is the ideal place to create summaries where they won't add latency to a message. Currently, Honcho has two configurable summary types:
* Short summaries: by default, enqueued every 20 messages and given a token limit of 1000
* Long summaries: by default, enqueued every 60 messages and given a token limit of 4000
@ -46,4 +46,4 @@ The above scenarios indicate where summarization is not possible -- therefore, t
Sometimes, gaps in context aren't an issue. In these cases, it's best to pass a reasonable token limit depending on your needs. Other cases demand exhaustive context -- don't pass a token limit and just let Honcho retrieve the ideal combination of summary and recent messages. Finally, if you don't care about the conversation at large and just want the last few messages, set `summary` to false and `tokens` to some multiple of your desired message count. Note that context messages are not paginated, so there's a hard limit on the number of messages that can be retrieved (currently 100,000 tokens).
As a final note, remember that summaries are generated asynchronously and therefore may not be available immediately. If you batch-save a large number of messages, assume that summaries will not be available until those messages are processed, which can take seconds to minutes depending on the number of messages and the configured LLM provider. Exhaustive `get_context` calls performed during this time will likely just return the messages in the session.
As a final note, remember that summaries are generated asynchronously and therefore may not be available immediately. If you batch-save a large number of messages, assume that summaries will not be available until those messages are processed, which can take seconds to minutes depending on the number of messages and the configured LLM provider. Exhaustive `get_context` calls performed during this time will likely just return the messages in the session.

View File

@ -143,15 +143,17 @@ print(response)
```
```typescript TypeScript
// Ask what Bob is like
const response = await alice.chat("Tell me about Bob's interests and habits");
console.log(response);
(async () => {
// Ask what Bob is like
const response = await alice.chat("Tell me about Bob's interests and habits");
console.log(response);
// Returns rich context like:
// "Bob is health-conscious and has been working on getting back in shape.
// He regularly goes to the gym, particularly in the evenings, and finds
// exercise helps him relax. He's encouraging about fitness and willing
// to share advice about workout routines."
// Returns rich context like:
// "Bob is health-conscious and has been working on getting back in shape.
// He regularly goes to the gym, particularly in the evenings, and finds
// exercise helps him relax. He's encouraging about fitness and willing
// to share advice about workout routines."
})();
```
</CodeGroup>

View File

@ -574,8 +574,8 @@ session.add_messages([
])
# Filter messages by metadata
finance_messages = session.get_messages(filter={"metadata": {"topic": "finance"}})
action_items = session.get_messages(filter={"metadata": {"action_item": True}})
finance_messages = session.get_messages(filters={"metadata": {"topic": "finance"}})
action_items = session.get_messages(filters={"metadata": {"action_item": True}})
```
```typescript TypeScript
@ -597,10 +597,10 @@ await session.addMessages([
// Filter messages by metadata
const financeMessages = await session.getMessages({
filter: { metadata: { topic: "finance" } }
filters: { metadata: { topic: "finance" } }
});
const actionItems = await session.getMessages({
filter: { metadata: { action_item: true } }
filters: { metadata: { action_item: true } }
});
```
</CodeGroup>

View File

@ -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();

View File

@ -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,
)

View File

@ -32,8 +32,6 @@ Files are processed in memory and not stored on disk. Only the extracted text co
## Basic Usage
### Upload a Single File
<CodeGroup>
```python Python
from honcho import Honcho
@ -59,45 +57,20 @@ print(f"Created {len(messages)} messages from the PDF")
import { Honcho } from "@honcho-ai/sdk";
import fs from "fs";
// Initialize client
const honcho = new Honcho({});
(async () => {
// Initialize client
const honcho = new Honcho({});
// Create session and peer
const session = honcho.session("research-session");
const user = honcho.peer("researcher");
// Create session and peer
const session = await honcho.session("research-session");
const user = await honcho.peer("researcher");
// Upload a PDF to a session
const fileStream = fs.createReadStream("research_paper.pdf");
const messages = await session.uploadFile({
file: fileStream,
peerId: user.id,
});
// Upload a PDF to a session
const fileStream = fs.createReadStream("research_paper.pdf");
const messages = await session.uploadFile(fileStream, user.id);
console.log(`Created ${messages.length} messages from the PDF`);
```
</CodeGroup>
### Upload to Peer's Global Representation
<CodeGroup>
```python Python
# Upload files directly to a peer's global representation
with open("personal_notes.pdf", "rb") as file:
messages = user.upload_file(
file=file,
)
print(f"Added {len(messages)} messages to {user.id}'s global representation")
```
```typescript TypeScript
// Upload files directly to a peer's global representation
const fileStream = fs.createReadStream("personal_notes.pdf");
const messages = await user.uploadFile({
file: fileStream,
});
console.log(`Added ${messages.length} messages to ${user.id}'s global representation`);
console.log(`Created ${messages.length} messages from the PDF`);
})();
```
</CodeGroup>
@ -108,7 +81,7 @@ The upload methods accept the following parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `file` | File | Yes | File to upload |
| `peer_id` | String | Session only | ID of the peer creating the messages |
| `peer_id` | String | Yes | ID of the peer creating the messages |
## File Processing Details
@ -160,17 +133,19 @@ messages = context.to_openai(assistant=assistant)
```
```typescript TypeScript
// Query what was learned from the uploaded documents
const response = await user.chat("What are the key findings from the research papers I uploaded?");
console.log(response);
(async () => {
// Query what was learned from the uploaded documents
const response = await user.chat("What are the key findings from the research papers I uploaded?");
console.log(response);
// Ask about specific documents
const response2 = await user.chat("What does the quarterly report say about revenue growth?");
console.log(response2);
// Ask about specific documents
const response2 = await user.chat("What does the quarterly report say about revenue growth?");
console.log(response2);
// Get context from the uploaded documents for LLM integration
const context = await session.getContext({ tokens: 3000 });
const messages = context.toOpenAI(assistant);
// Get context from the uploaded documents for LLM integration
const context = await session.getContext({ tokens: 3000 });
const messages = context.toOpenAI(assistant);
})();
```
</CodeGroup>
@ -262,52 +237,51 @@ print("Document Analysis:", analysis)
import { Honcho } from "@honcho-ai/sdk";
import fs from "fs";
// Initialize
const honcho = new Honcho({});
const session = honcho.session("document-analysis");
const user = honcho.peer("analyst");
const assistant = honcho.peer("analysis-bot");
(async () => {
// Initialize
const honcho = new Honcho({});
const session = await honcho.session("document-analysis");
const user = await honcho.peer("analyst");
const assistant = await honcho.peer("analysis-bot");
async function uploadDocument(filePath: string, description: string) {
const fileStream = fs.createReadStream(filePath);
const messages = await session.uploadFile({
file: fileStream,
peerId: user.id,
});
return messages;
}
async function uploadDocument(filePath: string, description: string) {
const fileStream = fs.createReadStream(filePath);
const messages = await session.uploadFile(fileStream, user.id);
return messages;
}
async function analyzeDocuments() {
const context = await session.getContext({ tokens: 4000 });
const messages = context.toOpenAI(assistant);
// Add analysis request
messages.push({
role: "user",
content: "Please analyze all the documents I've uploaded and provide a comprehensive summary of the key findings, trends, and recommendations."
});
async function analyzeDocuments() {
const context = await session.getContext({ tokens: 4000 });
const messages = context.toOpenAI(assistant);
// Add analysis request
messages.push({
role: "user",
content: "Please analyze all the documents I've uploaded and provide a comprehensive summary of the key findings, trends, and recommendations."
});
// Call OpenAI (or your preferred LLM)
// const response = await openai.chat.completions.create({ model: "gpt-4", messages });
// return response.choices[0].message.content;
// Call OpenAI (or your preferred LLM)
// const response = await openai.chat.completions.create({ model: "gpt-4", messages });
// return response.choices[0].message.content;
return "Analysis would be generated here";
}
return "Analysis would be generated here";
}
// Upload multiple documents
const documents = [
["quarterly_report.pdf", "Q3 2024 Quarterly Financial Report"],
["market_research.pdf", "Market Analysis and Competitive Landscape"],
["product_roadmap.pdf", "Product Development Roadmap 2024-2025"]
];
// Upload multiple documents
const documents = [
["quarterly_report.pdf", "Q3 2024 Quarterly Financial Report"],
["market_research.pdf", "Market Analysis and Competitive Landscape"],
["product_roadmap.pdf", "Product Development Roadmap 2024-2025"]
];
for (const [filePath, description] of documents) {
const messages = await uploadDocument(filePath, description);
console.log(`Uploaded ${filePath}: ${messages.length} messages created`);
}
for (const [filePath, description] of documents) {
const messages = await uploadDocument(filePath, description);
console.log(`Uploaded ${filePath}: ${messages.length} messages created`);
}
// Get AI analysis
const analysis = await analyzeDocuments();
console.log("Document Analysis:", analysis);
// Get AI analysis
const analysis = await analyzeDocuments();
console.log("Document Analysis:", analysis);
})();
```
</CodeGroup>

View File

@ -27,12 +27,14 @@ context = session.get_context()
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
// Initialize client and create session
const honcho = new Honcho({});
const session = honcho.session("conversation-1");
(async () => {
// Initialize client and create session
const honcho = new Honcho({});
const session = await honcho.session("conversation-1");
// Get basic context (not very useful before adding any messages!)
const context = await session.getContext();
// Get basic context (not very useful before adding any messages!)
const context = await session.getContext();
})();
```
</CodeGroup>
@ -54,11 +56,13 @@ context = session.get_context(tokens=3000)
```
```typescript TypeScript
// Limit context to 1500 tokens
const context = await session.getContext({ tokens: 1500 });
(async () => {
// Limit context to 1500 tokens
const context = await session.getContext({ tokens: 1500 });
// Limit context to 3000 tokens for larger conversations
const context = await session.getContext({ tokens: 3000 });
// Limit context to 3000 tokens for larger conversations
const context = await session.getContext({ tokens: 3000 });
})();
```
</CodeGroup>
@ -76,14 +80,16 @@ context = session.get_context(summary=False, tokens=2000)
```
```typescript TypeScript
// Get context with summary enabled -- will contain both summary and messages
const context = await session.getContext({ summary: true });
(async () => {
// Get context with summary enabled -- will contain both summary and messages
const context = await session.getContext({ summary: true });
// Combine summary=False with token limits to get more messages
const context = await session.getContext({
summary: false,
tokens: 2000
});
// Combine summary=False with token limits to get more messages
const context = await session.getContext({
summary: false,
tokens: 2000
});
})();
```
</CodeGroup>
@ -120,26 +126,28 @@ print(openai_messages)
```
```typescript TypeScript
// Create peers
const alice = honcho.peer("alice");
const assistant = honcho.peer("assistant");
(async () => {
// Create peers
const alice = await honcho.peer("alice");
const assistant = await honcho.peer("assistant");
// Add some conversation
await session.addMessages([
alice.message("What's the weather like today?"),
assistant.message("It's sunny and 75°F outside!")
]);
// Add some conversation
await session.addMessages([
alice.message("What's the weather like today?"),
assistant.message("It's sunny and 75°F outside!")
]);
// Get context and convert to OpenAI format
const context = await session.getContext();
const openaiMessages = context.toOpenAI(assistant);
// Get context and convert to OpenAI format
const context = await session.getContext();
const openaiMessages = context.toOpenAI(assistant);
// The messages are now ready for OpenAI API
console.log(openaiMessages);
// [
// {"role": "user", "content": "What's the weather like today?"},
// {"role": "assistant", "content": "It's sunny and 75°F outside!"}
// ]
// The messages are now ready for OpenAI API
console.log(openaiMessages);
// [
// {"role": "user", "content": "What's the weather like today?"},
// {"role": "assistant", "content": "It's sunny and 75°F outside!"}
// ]
})();
```
</CodeGroup>
@ -158,12 +166,14 @@ print(anthropic_messages)
```
```typescript TypeScript
// Get context and convert to Anthropic format
const context = await session.getContext();
const anthropicMessages = context.toAnthropic(assistant);
(async () => {
// Get context and convert to Anthropic format
const context = await session.getContext();
const anthropicMessages = context.toAnthropic(assistant);
// Ready for Anthropic API
console.log(anthropicMessages);
// Ready for Anthropic API
console.log(anthropicMessages);
})();
```
</CodeGroup>
@ -217,39 +227,41 @@ session.add_messages([
import OpenAI from 'openai';
import { Honcho } from "@honcho-ai/sdk";
// Initialize clients
const honcho = new Honcho({});
const openai = new OpenAI();
(async () => {
// Initialize clients
const honcho = new Honcho({});
const openai = new OpenAI();
// Set up conversation
const session = honcho.session("support-chat");
const user = honcho.peer("user-123");
const assistant = honcho.peer("support-bot");
// Set up conversation
const session = honcho.session("support-chat");
const user = honcho.peer("user-123");
const assistant = honcho.peer("support-bot");
// Add conversation history
await session.addMessages([
user.message("I'm having trouble with my account login"),
assistant.message("I can help you with that. What error message are you seeing?"),
user.message("It says 'Invalid credentials' but I'm sure my password is correct")
]);
// Add conversation history
await session.addMessages([
user.message("I'm having trouble with my account login"),
assistant.message("I can help you with that. What error message are you seeing?"),
user.message("It says 'Invalid credentials' but I'm sure my password is correct")
]);
// Get context for LLM
const messages = await session.getContext({ tokens: 2000 }).toOpenAI(assistant);
// Get context for LLM
const messages = await session.getContext({ tokens: 2000 }).toOpenAI(assistant);
// Add new user message and get AI response
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
...messages,
{ role: "user", content: "Can you reset my password?" }
]
});
// Add new user message and get AI response
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
...messages,
{ role: "user", content: "Can you reset my password?" }
]
});
// Add AI response back to session
await session.addMessages([
user.message("Can you reset my password?"),
assistant.message(response.choices[0].message.content)
]);
// Add AI response back to session
await session.addMessages([
user.message("Can you reset my password?"),
assistant.message(response.choices[0].message.content)
]);
})();
```
</CodeGroup>
@ -294,44 +306,46 @@ chat_loop()
```
```typescript TypeScript
async function chatLoop() {
const session = honcho.session("chat-session");
const user = honcho.peer("user");
const assistant = honcho.peer("ai-assistant");
(async () => {
async function chatLoop() {
const session = await honcho.session("chat-session");
const user = await honcho.peer("user");
const assistant = await honcho.peer("ai-assistant");
// This would be replaced with actual user input handling in a real app
const userInputs = [
"Hello, how are you?",
"What's the weather like?",
"Tell me a joke"
];
// This would be replaced with actual user input handling in a real app
const userInputs = [
"Hello, how are you?",
"What's the weather like?",
"Tell me a joke"
];
for (const userInput of userInputs) {
console.log(`You: ${userInput}`);
for (const userInput of userInputs) {
console.log(`You: ${userInput}`);
// Add user message to session
await session.addMessages([user.message(userInput)]);
// Add user message to session
await session.addMessages([user.message(userInput)]);
// Get conversation context
const context = await session.getContext({ tokens: 2000 });
const messages = context.toOpenAI(assistant);
// Get conversation context
const context = await session.getContext({ tokens: 2000 });
const messages = context.toOpenAI(assistant);
// Get AI response
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages
});
// Get AI response
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages
});
const aiResponse = response.choices[0].message.content;
console.log(`Assistant: ${aiResponse}`);
const aiResponse = response.choices[0].message.content;
console.log(`Assistant: ${aiResponse}`);
// Add AI response to session
await session.addMessages([assistant.message(aiResponse)]);
}
}
// Add AI response to session
await session.addMessages([assistant.message(aiResponse)]);
}
}
// Start the chat loop
await chatLoop();
// Start the chat loop
await chatLoop();
})();
```
</CodeGroup>
@ -355,18 +369,20 @@ print(f"Context contains {len(messages)} formatted messages")
```
```typescript TypeScript
// For long conversations, use summary mode
const longSession = honcho.session("long-conversation");
(async () => {
// For long conversations, use summary mode
const longSession = await honcho.session("long-conversation");
// Get summarized context to fit within token limits
const context = await longSession.getContext({
summary: true,
tokens: 1500
});
const messages = context.toOpenAI(assistant);
// Get summarized context to fit within token limits
const context = await longSession.getContext({
summary: true,
tokens: 1500
});
const messages = context.toOpenAI(assistant);
// This will include a summary of older messages and recent full messages
console.log(`Context contains ${messages.length} formatted messages`);
// This will include a summary of older messages and recent full messages
console.log(`Context contains ${messages.length} formatted messages`);
})();
```
</CodeGroup>
@ -390,18 +406,20 @@ moderator_context = session.get_context().to_openai(assistant=moderator)
```
```typescript TypeScript
// Create different assistant peers
const chatbot = honcho.peer("chatbot");
const analyzer = honcho.peer("data-analyzer");
const moderator = honcho.peer("moderator");
(async () => {
// Create different assistant peers
const chatbot = await honcho.peer("chatbot");
const analyzer = await honcho.peer("data-analyzer");
const moderator = await honcho.peer("moderator");
// Get context formatted for each assistant type
const context = await session.getContext();
const chatbotContext = context.toOpenAI(chatbot);
const analyzerContext = context.toOpenAI(analyzer);
const moderatorContext = context.toOpenAI(moderator);
// Get context formatted for each assistant type
const context = await session.getContext();
const chatbotContext = context.toOpenAI(chatbot);
const analyzerContext = context.toOpenAI(analyzer);
const moderatorContext = context.toOpenAI(moderator);
// Each context will format the conversation from that assistant's perspective
// Each context will format the conversation from that assistant's perspective
})();
```
</CodeGroup>
@ -422,12 +440,14 @@ context = session.get_context(summary=True, tokens=2000)
```
```typescript TypeScript
// Good: Set reasonable token limits based on your model
const context = await session.getContext({ tokens: 3000 }); // For GPT-4
const context = await session.getContext({ tokens: 1500 }); // For smaller models
(async () => {
// Good: Set reasonable token limits based on your model
const context = await session.getContext({ tokens: 3000 }); // For GPT-4
const context = await session.getContext({ tokens: 1500 }); // For smaller models
// Good: Use summaries for very long conversations
const context = await session.getContext({ summary: true, tokens: 2000 });
// Good: Use summaries for very long conversations
const context = await session.getContext({ summary: true, tokens: 2000 });
})();
```
</CodeGroup>
@ -446,12 +466,14 @@ anthropic_messages = context.to_anthropic(assistant=assistant)
```
```typescript TypeScript
// Cache context for multiple LLM calls within the same request
const context = await session.getContext({ tokens: 2000 });
const openaiMessages = context.toOpenAI(assistant);
const anthropicMessages = context.toAnthropic(assistant);
(async () => {
// Cache context for multiple LLM calls within the same request
const context = await session.getContext({ tokens: 2000 });
const openaiMessages = context.toOpenAI(assistant);
const anthropicMessages = context.toAnthropic(assistant);
// Use the same context object for multiple format conversions
// Use the same context object for multiple format conversions
})();
```
</CodeGroup>
@ -477,20 +499,22 @@ except Exception as e:
```
```typescript TypeScript
try {
const context = await session.getContext({ tokens: 2000 });
const messages = context.toOpenAI(assistant);
(async () => {
try {
const context = await session.getContext({ tokens: 2000 });
const messages = context.toOpenAI(assistant);
// Use messages with LLM API
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages
});
// Use messages with LLM API
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: messages
});
} catch (error) {
console.error(`Error getting context: ${error}`);
// Handle error appropriately
}
} catch (error) {
console.error(`Error getting context: ${error}`);
// Handle error appropriately
}
})();
```
</CodeGroup>

View File

@ -6,14 +6,6 @@ icon: 'magnifying-glass'
Honcho's search functionality allows you to find relevant messages and conversations across different scopes - from entire workspaces down to specific peers or sessions.
## How Search Works
Search in Honcho is implemented with a two-tier approach:
1. **Primary**: PostgreSQL English language full-text search index for intelligent matching
2. **Fallback**: Simple string matching for broader coverage
All search results are returned in a paginated format, making it easy to handle large result sets efficiently.
## Search Scopes
### Workspace Search
@ -38,16 +30,18 @@ for result in results:
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
// Initialize client
const honcho = new Honcho({});
(async () => {
// Initialize client
const honcho = new Honcho({});
// Search across entire workspace
const results = await honcho.search("budget planning");
// Search across entire workspace
const results = await honcho.search("budget planning");
// Iterate through all results
for await (const result of results) {
console.log(`Found: ${result}`);
}
// Iterate through all results
for (const result of results) {
console.log(`Found: ${result}`);
}
})();
```
</CodeGroup>
@ -69,16 +63,18 @@ for result in results:
```
```typescript TypeScript
// Create or get a session
const session = honcho.session("team-meeting-jan");
(async () => {
// Create or get a session
const session = await honcho.session("team-meeting-jan");
// Search within this session only
const results = await session.search("action items");
// Search within this session only
const results = await session.search("action items");
// Process results
for await (const result of results) {
console.log(`Session result: ${result}`);
}
// Process results
for (const result of results) {
console.log(`Session result: ${result}`);
}
})();
```
</CodeGroup>
@ -100,160 +96,90 @@ for result in results:
```
```typescript TypeScript
// Create or get a peer
const alice = honcho.peer("alice");
import { Message } from "@honcho-ai/sdk";
// Search across all of Alice's messages and interactions
const results = await alice.search("programming");
(async () => {
// Create or get a peer
const alice = await honcho.peer("alice");
// View results
for await (const result of results) {
console.log(`Alice's content: ${result}`);
}
// Search across all of Alice's messages and interactions
const results: Message[] = await alice.search("programming");
// View results
for (const result of results) {
console.log(`Alice's content: ${result.content}`);
}
})();
```
</CodeGroup>
## Working with Search Results
## Filters and Limits
### Basic Result Processing
### Get a specific number of results
You can specify the number of results you want to return by passing the `limit` parameter to the search method. The default is 10 results, with a maximum of 100.
<CodeGroup>
```python Python
# Search returns a paginated iterator
results = honcho.search("customer feedback")
# Simple iteration processes all results automatically
for result in results:
# Each result contains the matched content and context
print(f"Match: {result}")
# Check if there are any results
results = honcho.search("nonexistent topic")
result_list = list(results)
if not result_list:
print("No results found")
results = honcho.search("budget planning", limit=20)
```
```typescript TypeScript
// Search returns a paginated Page object
const results = await honcho.search("customer feedback");
// Iterate through all results
for await (const result of results) {
// Each result contains the matched content and context
console.log(`Match: ${result}`);
}
// Check if there are any results
const emptyResults = await honcho.search("nonexistent topic");
const resultData = await emptyResults.data();
if (resultData.length === 0) {
console.log("No results found");
}
(async () => {
const results = await honcho.search("budget planning", { limit: 20 });
})();
```
</CodeGroup>
### Manual Pagination
### Get messages from a Peer in a specific Session
Combine Peer-level search with a `session_id` filter to get messages from a Peer in a specific Session.
<CodeGroup>
```python Python
# For manual pagination control, you can work with pages directly
results = honcho.search("project updates")
# The iterator handles pagination automatically, but you can also
# work with individual batches if needed
count = 0
for result in results:
count += 1
print(f"Result {count}: {result}")
# Stop after first 10 results
if count >= 10:
break
my_peer = honcho.peer("my-peer")
my_session = honcho.session("team-meeting-jan")
results = my_peer.search("budget planning", filters={"session_id": my_session.id})
```
```typescript TypeScript
// Manual pagination with TypeScript
let currentPage = await honcho.search("project updates");
while (currentPage) {
const data = await currentPage.data();
console.log(`Processing ${data.length} results`);
for (const result of data) {
console.log(`Result: ${result}`);
}
// Get next page
currentPage = await currentPage.nextPage();
}
(async () => {
const my_peer = await honcho.peer("my-peer");
const my_session = await honcho.session("team-meeting-jan");
const results = await my_peer.search("budget planning", { filters: { session_id: my_session.id } });
})();
```
</CodeGroup>
### Search with Context Building
### Filter results by time range
<CodeGroup>
```python Python
# Use search results to build context for LLM interactions
def build_context_from_search(query: str, session_id: str):
session = honcho.session(session_id)
# Search for relevant past discussions
search_results = list(session.search(query))
if search_results:
# Use search results to inform context
context_summary = f"Found {len(search_results)} relevant past discussions about '{query}'"
# Get normal session context
session_context = session.get_context(tokens=1500)
return {
"search_summary": context_summary,
"session_context": session_context,
"search_results": search_results[:3] # Top 3 results
}
return {"message": "No relevant past discussions found"}
# Build context for a new question
context_data = build_context_from_search("user authentication", "support-session-1")
print(f"Context: {context_data.get('search_summary', 'No context')}")
results = honcho.search("budget planning", filters={"created_at": {"gte": "2024-01-01", "lte": "2024-01-31"}})
```
```typescript TypeScript
// Use search results to build context for LLM interactions
async function buildContextFromSearch(query: string, sessionId: string) {
const session = honcho.session(sessionId);
// Search for relevant past discussions
const searchResults = await session.search(query);
const searchData = await searchResults.data();
if (searchData.length > 0) {
// Use search results to inform context
const contextSummary = `Found ${searchData.length} relevant past discussions about '${query}'`;
// Get normal session context
const sessionContext = await session.getContext({ tokens: 1500 });
return {
searchSummary: contextSummary,
sessionContext: sessionContext,
searchResults: searchData.slice(0, 3) // Top 3 results
};
}
return { message: "No relevant past discussions found" };
}
// Build context for a new question
const contextData = await buildContextFromSearch("user authentication", "support-session-1");
console.log(`Context: ${contextData.searchSummary || contextData.message}`);
(async () => {
const results = await honcho.search("budget planning", { filters: { created_at: { gte: "2024-01-01", lte: "2024-01-31" } } });
})();
```
</CodeGroup>
## Best Practices
### Filter results by metadata
<CodeGroup>
```python Python
results = honcho.search("budget planning", filters={"metadata": {"key": "value"}})
```
```typescript TypeScript
(async () => {
const results = await honcho.search("budget planning", { filters: { metadata: { key: "value" } } });
})();
```
</CodeGroup>
### Best Practices
### Handle Empty Results Gracefully
@ -272,18 +198,21 @@ else:
```
```typescript TypeScript
// Always check for empty results
const results = await honcho.search("very specific query");
const resultData = await results.data();
import { Message } from "@honcho-ai/sdk";
if (resultData.length > 0) {
console.log(`Found ${resultData.length} results`);
for (const result of resultData) {
console.log(`- ${result}`);
}
} else {
console.log("No results found - try a broader search");
}
(async () => {
// Always check for empty results
const results: Message[] = await honcho.search("very specific query");
if (results.length > 0) {
console.log(`Found ${results.length} results`);
for (const result of results) {
console.log(`- ${result.content}`);
}
} else {
console.log("No results found - try a broader search");
}
})();
```
</CodeGroup>

View File

@ -48,23 +48,25 @@ session.add_messages([
```typescript TypeScript
import { Honcho } from '@honcho-ai/sdk';
// Initialize client (using the default workspace)
const honcho = new Honcho();
(async () => {
// Initialize client (using the default workspace)
const honcho = new Honcho();
// Create or get peers
const user = honcho.peer('demo-user');
const assistant = honcho.peer('assistant');
// Create or get peers
const user = await honcho.peer('demo-user');
const assistant = await honcho.peer('assistant');
// Create a new session
const session = honcho.session('demo-session');
// Create a new session
const session = await honcho.session('demo-session');
// Add peers to the session
await session.addPeers([user, assistant]);
// Add peers to the session
await session.addPeers([user, assistant]);
// Store some messages for context (optional)
await session.addMessages([
user.message("Hello, I'm testing the streaming functionality")
]);
// Store some messages for context (optional)
await session.addMessages([
user.message("Hello, I'm testing the streaming functionality")
]);
})();
```
</CodeGroup>
@ -83,15 +85,17 @@ for chunk in response_stream.iter_text():
```
```typescript TypeScript
// Basic streaming example
const responseStream = await user.chat("What can you tell me about this user?", {
stream: true
});
(async () => {
// Basic streaming example
const responseStream = await user.chat("What can you tell me about this user?", {
stream: true
});
// Process the stream
for await (const chunk of responseStream.iter_text()) {
process.stdout.write(chunk); // Write to console without newlines
}
// Process the stream
for await (const chunk of responseStream.iter_text()) {
process.stdout.write(chunk); // Write to console without newlines
}
})();
```
</CodeGroup>
@ -171,63 +175,65 @@ if __name__ == "__main__":
```typescript TypeScript
import { Honcho } from '@honcho-ai/sdk';
async function restaurantRecommendationChat() {
// Initialize client
const honcho = new Honcho();
(async () => {
async function restaurantRecommendationChat() {
// Initialize client
const honcho = new Honcho();
// Create peers
const user = honcho.peer('food-lover');
const assistant = honcho.peer('restaurant-assistant');
// Create peers
const user = await honcho.peer('food-lover');
const assistant = await honcho.peer('restaurant-assistant');
// Create session
const session = honcho.session('food-preferences-session');
// Create session
const session = await honcho.session('food-preferences-session');
// Add peers to session
await session.addPeers([user, assistant]);
// Add peers to session
await session.addPeers([user, assistant]);
// Store multiple user messages about food preferences
const userMessages = [
"I absolutely love spicy Thai food, especially curries with coconut milk.",
"Italian cuisine is another favorite - fresh pasta and wood-fired pizza are my weakness!",
"I try to eat vegetarian most of the time, but occasionally enjoy seafood.",
"I can't handle overly sweet desserts, but love something with dark chocolate."
];
// Store multiple user messages about food preferences
const userMessages = [
"I absolutely love spicy Thai food, especially curries with coconut milk.",
"Italian cuisine is another favorite - fresh pasta and wood-fired pizza are my weakness!",
"I try to eat vegetarian most of the time, but occasionally enjoy seafood.",
"I can't handle overly sweet desserts, but love something with dark chocolate."
];
// Add the user's messages to the session
const sessionMessages = userMessages.map(message => user.message(message));
await session.addMessages(sessionMessages);
// Add the user's messages to the session
const sessionMessages = userMessages.map(message => user.message(message));
await session.addMessages(sessionMessages);
// Print the user messages
for (const message of userMessages) {
console.log(`User: ${message}`);
}
// Print the user messages
for (const message of userMessages) {
console.log(`User: ${message}`);
}
// Ask for restaurant recommendations based on preferences
console.log("\nRequesting restaurant recommendations...");
process.stdout.write("Assistant: ");
let fullResponse = "";
// Ask for restaurant recommendations based on preferences
console.log("\nRequesting restaurant recommendations...");
process.stdout.write("Assistant: ");
let fullResponse = "";
// Stream the response using the user's peer to get recommendations
const responseStream = await user.chat(
"Based on this user's food preferences, recommend 3 restaurants they might enjoy in the Lower East Side.",
{
stream: true,
sessionId: session.id
// Stream the response using the user's peer to get recommendations
const responseStream = await user.chat(
"Based on this user's food preferences, recommend 3 restaurants they might enjoy in the Lower East Side.",
{
stream: true,
sessionId: session.id
}
);
for await (const chunk of responseStream.iter_text()) {
process.stdout.write(chunk);
fullResponse += chunk;
}
// Store the assistant's complete response
await session.addMessages([
assistant.message(fullResponse)
]);
}
);
for await (const chunk of responseStream.iter_text()) {
process.stdout.write(chunk);
fullResponse += chunk;
}
// Store the assistant's complete response
await session.addMessages([
assistant.message(fullResponse)
]);
}
restaurantRecommendationChat().catch(console.error);
await restaurantRecommendationChat();
})();
```
</CodeGroup>

View File

@ -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,
)

View File

@ -22,15 +22,15 @@ from honcho import Honcho
honcho = Honcho()
# Simple peer filter
peers = honcho.get_peers(filter={"peer_id": "alice"})
peers = honcho.get_peers(filters={"peer_id": "alice"})
# Simple session filter with metadata
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {"type": "support"}
})
# Simple message filter
messages = honcho.get_messages(filter={
messages = honcho.get_messages(filters={
"session_id": "support-chat-1",
"peer_id": "alice"
})
@ -39,28 +39,30 @@ messages = honcho.get_messages(filter={
```typescript TypeScript
import { Honcho } from "@honcho-ai/sdk";
// Initialize client
const honcho = new Honcho({});
(async () => {
// Initialize client
const honcho = new Honcho({});
// Simple peer filter
const peers = await honcho.getPeers({
filter: { peerId: "alice" }
});
// Simple peer filter
const peers = await honcho.getPeers({
filters: { peerId: "alice" }
});
// Simple session filter with metadata
const sessions = await honcho.getSessions({
filter: {
metadata: { type: "support" }
}
});
// Simple session filter with metadata
const sessions = await honcho.getSessions({
filters: {
metadata: { type: "support" }
}
});
// Simple message filter
const messages = await honcho.getMessages({
filter: {
sessionId: "support-chat-1",
peerId: "alice"
}
});
// Simple message filter
const messages = await honcho.getMessages({
filters: {
sessionId: "support-chat-1",
peerId: "alice"
}
});
})();
```
</CodeGroup>
@ -74,7 +76,7 @@ Use AND to require all conditions to be true:
<CodeGroup>
```python Python
messages = honcho.get_messages(filter={
messages = honcho.get_messages(filters={
"AND": [
{"session_id": "chat-1"},
{"created_at": {"gte": "2024-01-01"}}
@ -83,14 +85,16 @@ messages = honcho.get_messages(filter={
```
```typescript TypeScript
const messages = await honcho.getMessages({
filter: {
AND: [
{ sessionId: "chat-1" },
{ createdAt: { gte: "2024-01-01" } }
]
}
});
(async () => {
const messages = await honcho.getMessages({
filters: {
AND: [
{ sessionId: "chat-1" },
{ createdAt: { gte: "2024-01-01" } }
]
}
});
})();
```
</CodeGroup>
@ -101,7 +105,7 @@ Use OR to match any of the specified conditions:
<CodeGroup>
```python Python
# Find messages from either alice or bob
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"OR": [
{"peer_id": "alice"},
{"peer_id": "bob"}
@ -109,7 +113,7 @@ messages = session.get_messages(filter={
})
# Complex OR with metadata conditions
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"OR": [
{"metadata": {"priority": "high"}},
{"metadata": {"urgent": True}},
@ -119,26 +123,28 @@ sessions = honcho.get_sessions(filter={
```
```typescript TypeScript
// Find messages from either alice or bob
const messages = await session.getMessages({
filter: {
OR: [
{ peerId: "alice" },
{ peerId: "bob" }
]
}
});
(async () => {
// Find messages from either alice or bob
const messages = await session.getMessages({
filters: {
OR: [
{ peerId: "alice" },
{ peerId: "bob" }
]
}
});
// Complex OR with metadata conditions
const sessions = await honcho.getSessions({
filter: {
OR: [
{ metadata: { priority: "high" } },
{ metadata: { urgent: true } },
{ metadata: { escalated: true } }
]
}
});
// Complex OR with metadata conditions
const sessions = await honcho.getSessions({
filters: {
OR: [
{ metadata: { priority: "high" } },
{ metadata: { urgent: true } },
{ metadata: { escalated: true } }
]
}
});
})();
```
</CodeGroup>
@ -149,14 +155,14 @@ Use NOT to exclude specific conditions:
<CodeGroup>
```python Python
# Find all peers except alice
peers = honcho.get_peers(filter={
peers = honcho.get_peers(filters={
"NOT": [
{"peer_id": "alice"}
]
})
# Find sessions that are NOT completed
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"NOT": [
{"metadata": {"status": "completed"}}
]
@ -164,23 +170,25 @@ sessions = honcho.get_sessions(filter={
```
```typescript TypeScript
// Find all peers except alice
const peers = await honcho.getPeers({
filter: {
NOT: [
{ peerId: "alice" }
]
}
});
(async () => {
// Find all peers except alice
const peers = await honcho.getPeers({
filters: {
NOT: [
{ peerId: "alice" }
]
}
});
// Find sessions that are NOT completed
const sessions = await honcho.getSessions({
filter: {
NOT: [
{ metadata: { status: "completed" } }
]
}
});
// Find sessions that are NOT completed
const sessions = await honcho.getSessions({
filters: {
NOT: [
{ metadata: { status: "completed" } }
]
}
});
})();
```
</CodeGroup>
@ -191,7 +199,7 @@ Create sophisticated queries by combining different logical operators:
<CodeGroup>
```python Python
# Find messages from alice OR bob, but NOT where message has archived set to true in metadata
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"AND": [
{
"OR": [
@ -209,24 +217,26 @@ messages = session.get_messages(filter={
```
```typescript TypeScript
// Find messages from alice OR bob, but NOT where message has archived set to true in metadata
const messages = await session.getMessages({
filter: {
AND: [
{
OR: [
{ peerId: "alice" },
{ peerId: "bob" }
]
},
{
NOT: [
{ metadata: { archived: true } }
(async () => {
// Find messages from alice OR bob, but NOT where message has archived set to true in metadata
const messages = await session.getMessages({
filters: {
AND: [
{
OR: [
{ peerId: "alice" },
{ peerId: "bob" }
]
},
{
NOT: [
{ metadata: { archived: true } }
]
}
]
}
]
}
});
});
})();
```
</CodeGroup>
@ -239,12 +249,12 @@ Use comparison operators for range queries and advanced matching:
<CodeGroup>
```python Python
# Find sessions created after a specific date
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"created_at": {"gte": "2024-01-01"}
})
# Find messages within a date range
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"created_at": {
"gte": "2024-01-01",
"lte": "2024-12-31"
@ -252,7 +262,7 @@ messages = session.get_messages(filter={
})
# Metadata numeric comparisons
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {
"score": {"gt": 8.5},
"duration": {"lte": 3600}
@ -261,32 +271,34 @@ sessions = honcho.get_sessions(filter={
```
```typescript TypeScript
// Find sessions created after a specific date
const sessions = await honcho.getSessions({
filter: {
createdAt: { gte: "2024-01-01" }
}
});
(async () => {
// Find sessions created after a specific date
const sessions = await honcho.getSessions({
filters: {
createdAt: { gte: "2024-01-01" }
}
});
// Find messages within a date range
const messages = await session.getMessages({
filter: {
createdAt: {
gte: "2024-01-01",
lte: "2024-12-31"
}
}
});
// Find messages within a date range
const messages = await session.getMessages({
filters: {
createdAt: {
gte: "2024-01-01",
lte: "2024-12-31"
}
}
});
// Metadata numeric comparisons
const sessions = await honcho.getSessions({
filter: {
metadata: {
score: { gt: 8.5 },
duration: { lte: 3600 }
}
}
});
// Metadata numeric comparisons
const sessions = await honcho.getSessions({
filters: {
metadata: {
score: { gt: 8.5 },
duration: { lte: 3600 }
}
}
});
})();
```
</CodeGroup>
@ -295,19 +307,19 @@ const sessions = await honcho.getSessions({
<CodeGroup>
```python Python
# Find messages from specific peers in a session
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"peer_id": {"in": ["alice", "bob", "charlie"]}
})
# Find sessions with specific tags
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {
"tag": {"in": ["important", "urgent", "follow-up"]}
}
})
# Not equal comparisons
peers = honcho.get_peers(filter={
peers = honcho.get_peers(filters={
"metadata": {
"status": {"ne": "inactive"}
}
@ -315,30 +327,32 @@ peers = honcho.get_peers(filter={
```
```typescript TypeScript
// Find messages from specific peers in a session
const messages = await session.getMessages({
filter: {
peerId: { in: ["alice", "bob", "charlie"] }
}
});
(async () => {
// Find messages from specific peers in a session
const messages = await session.getMessages({
filters: {
peerId: { in: ["alice", "bob", "charlie"] }
}
});
// Find sessions with specific tags
const sessions = await honcho.getSessions({
filter: {
metadata: {
tag: { in: ["important", "urgent", "follow-up"] }
}
}
});
// Find sessions with specific tags
const sessions = await honcho.getSessions({
filters: {
metadata: {
tag: { in: ["important", "urgent", "follow-up"] }
}
}
});
// Not equal comparisons
const peers = await honcho.getPeers({
filter: {
metadata: {
status: { ne: "inactive" }
}
}
});
// Not equal comparisons
const peers = await honcho.getPeers({
filters: {
metadata: {
status: { ne: "inactive" }
}
}
});
})();
```
</CodeGroup>
@ -351,7 +365,7 @@ Metadata filtering is particularly powerful in Honcho, supporting nested conditi
<CodeGroup>
```python Python
# Simple metadata equality
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {
"type": "customer_support",
"priority": "high"
@ -359,7 +373,7 @@ sessions = honcho.get_sessions(filter={
})
# Nested metadata objects
peers = honcho.get_peers(filter={
peers = honcho.get_peers(filters={
"metadata": {
"profile": {
"role": "admin",
@ -370,27 +384,29 @@ peers = honcho.get_peers(filter={
```
```typescript TypeScript
// Simple metadata equality
const sessions = await honcho.getSessions({
filter: {
metadata: {
type: "customer_support",
priority: "high"
}
}
});
// Nested metadata objects
const peers = await honcho.getPeers({
filter: {
metadata: {
profile: {
role: "admin",
department: "engineering"
(async () => {
// Simple metadata equality
const sessions = await honcho.getSessions({
filters: {
metadata: {
type: "customer_support",
priority: "high"
}
}
}
}
});
});
// Nested metadata objects
const peers = await honcho.getPeers({
filters: {
metadata: {
profile: {
role: "admin",
department: "engineering"
}
}
}
});
})();
```
</CodeGroup>
@ -403,7 +419,7 @@ If you want to do advanced queries like these, make sure not to create metadata
<CodeGroup>
```python Python
# Metadata with comparison operators
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {
"score": {"gte": 4.0, "lte": 5.0},
"created_by": {"ne": "system"},
@ -412,7 +428,7 @@ sessions = honcho.get_sessions(filter={
})
# Complex metadata conditions
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"AND": [
{"metadata": {"sentiment": {"in": ["positive", "neutral"]}}},
{"metadata": {"confidence": {"gt": 0.8}}},
@ -422,27 +438,29 @@ messages = session.get_messages(filter={
```
```typescript TypeScript
// Metadata with comparison operators
const sessions = await honcho.getSessions({
filter: {
metadata: {
score: { gte: 4.0, lte: 5.0 },
createdBy: { ne: "system" },
tags: { contains: "important" }
}
}
});
(async () => {
// Metadata with comparison operators
const sessions = await honcho.getSessions({
filters: {
metadata: {
score: { gte: 4.0, lte: 5.0 },
createdBy: { ne: "system" },
tags: { contains: "important" }
}
}
});
// Complex metadata conditions
const messages = await session.getMessages({
filter: {
AND: [
{ metadata: { sentiment: { in: ["positive", "neutral"] } } },
{ metadata: { confidence: { gt: 0.8 } } },
{ content: { icontains: "thank" } }
]
}
});
// Complex metadata conditions
const messages = await session.getMessages({
filters: {
AND: [
{ metadata: { sentiment: { in: ["positive", "neutral"] } } },
{ metadata: { confidence: { gt: 0.8 } } },
{ content: { icontains: "thank" } }
]
}
});
})();
```
</CodeGroup>
@ -453,17 +471,17 @@ Use wildcards (*) to match any value for a field:
<CodeGroup>
```python Python
# Find all sessions with any peer_id (essentially all sessions)
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"peer_id": "*"
})
# Wildcard in lists - matches everything
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"peer_id": {"in": ["alice", "bob", "*"]}
})
# Metadata wildcards
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"metadata": {
"type": "*", # Any type
"status": "active" # But status must be active
@ -472,29 +490,31 @@ sessions = honcho.get_sessions(filter={
```
```typescript TypeScript
// Find all sessions with any peer_id (essentially all sessions)
const sessions = await honcho.getSessions({
filter: {
peerId: "*"
}
});
(async () => {
// Find all sessions with any peer_id (essentially all sessions)
const sessions = await honcho.getSessions({
filters: {
peerId: "*"
}
});
// Wildcard in lists - matches everything
const messages = await session.getMessages({
filter: {
peerId: { in: ["alice", "bob", "*"] }
}
});
// Wildcard in lists - matches everything
const messages = await session.getMessages({
filters: {
peerId: { in: ["alice", "bob", "*"] }
}
});
// Metadata wildcards
const sessions = await honcho.getSessions({
filter: {
metadata: {
type: "*", // Any type
status: "active" // But status must be active
}
}
});
// Metadata wildcards
const sessions = await honcho.getSessions({
filters: {
metadata: {
type: "*", // Any type
status: "active" // But status must be active
}
}
});
})();
```
</CodeGroup>
@ -505,12 +525,12 @@ const sessions = await honcho.getSessions({
<CodeGroup>
```python Python
# Find workspaces by name pattern
workspaces = honcho.get_workspaces(filter={
workspaces = honcho.get_workspaces(filters={
"name": {"contains": "prod"}
})
# Filter by metadata
workspaces = honcho.get_workspaces(filter={
workspaces = honcho.get_workspaces(filters={
"metadata": {
"environment": "production",
"team": {"in": ["backend", "frontend", "devops"]}
@ -519,22 +539,24 @@ workspaces = honcho.get_workspaces(filter={
```
```typescript TypeScript
// Find workspaces by name pattern
const workspaces = await honcho.getWorkspaces({
filter: {
name: { contains: "prod" }
}
});
(async () => {
// Find workspaces by name pattern
const workspaces = await honcho.getWorkspaces({
filters: {
name: { contains: "prod" }
}
});
// Filter by metadata
const workspaces = await honcho.getWorkspaces({
filter: {
metadata: {
environment: "production",
team: { in: ["backend", "frontend", "devops"] }
}
}
});
// Filter by metadata
const workspaces = await honcho.getWorkspaces({
filters: {
metadata: {
environment: "production",
team: { in: ["backend", "frontend", "devops"] }
}
}
});
})();
```
</CodeGroup>
@ -546,7 +568,7 @@ const workspaces = await honcho.getWorkspaces({
from datetime import datetime, timedelta
week_ago = (datetime.now() - timedelta(days=7)).isoformat()
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"AND": [
{"content": {"icontains": "error"}},
{"created_at": {"gte": week_ago}},
@ -555,7 +577,7 @@ messages = session.get_messages(filter={
})
# Find messages in specific sessions with sentiment analysis
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"AND": [
{"session_id": {"in": ["support-1", "support-2", "support-3"]}},
{"metadata": {"sentiment": "negative"}},
@ -565,28 +587,30 @@ messages = session.get_messages(filter={
```
```typescript TypeScript
// Find error messages from the last week
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const messages = await session.getMessages({
filter: {
AND: [
{ content: { icontains: "error" } },
{ createdAt: { gte: weekAgo } },
{ metadata: { level: { in: ["error", "critical"] } } }
]
}
});
(async () => {
// Find error messages from the last week
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const messages = await session.getMessages({
filters: {
AND: [
{ content: { icontains: "error" } },
{ createdAt: { gte: weekAgo } },
{ metadata: { level: { in: ["error", "critical"] } } }
]
}
});
// Find messages in specific sessions with sentiment analysis
const messages = await session.getMessages({
filter: {
AND: [
{ sessionId: { in: ["support-1", "support-2", "support-3"] } },
{ metadata: { sentiment: "negative" } },
{ metadata: { confidence: { gte: 0.7 } } }
]
}
});
// Find messages in specific sessions with sentiment analysis
const messages = await session.getMessages({
filters: {
AND: [
{ sessionId: { in: ["support-1", "support-2", "support-3"] } },
{ metadata: { sentiment: "negative" } },
{ metadata: { confidence: { gte: 0.7 } } }
]
}
});
})();
```
</CodeGroup>
@ -600,7 +624,7 @@ from honcho.exceptions import FilterError
try:
# Invalid filter - unsupported operator
messages = session.get_messages(filter={
messages = session.get_messages(filters={
"created_at": {"invalid_operator": "2024-01-01"}
})
except FilterError as e:
@ -609,7 +633,7 @@ except FilterError as e:
try:
# Invalid column name
sessions = honcho.get_sessions(filter={
sessions = honcho.get_sessions(filters={
"nonexistent_field": "value"
})
except FilterError as e:
@ -617,30 +641,32 @@ except FilterError as e:
```
```typescript TypeScript
try {
// Invalid filter - unsupported operator
const messages = await session.getMessages({
filter: {
createdAt: { invalidOperator: "2024-01-01" }
(async () => {
try {
// Invalid filter - unsupported operator
const messages = await session.getMessages({
filters: {
createdAt: { invalidOperator: "2024-01-01" }
}
});
} catch (error) {
if (error.message.includes("filters")) {
console.error(`Filter error: ${error.message}`);
// Handle the error appropriately
}
}
});
} catch (error) {
if (error.message.includes("filter")) {
console.error(`Filter error: ${error.message}`);
// Handle the error appropriately
}
}
try {
// Invalid column name
const sessions = await honcho.getSessions({
filter: {
nonexistentField: "value"
try {
// Invalid column name
const sessions = await honcho.getSessions({
filters: {
nonexistentField: "value"
}
});
} catch (error) {
console.error(`Invalid field: ${error.message}`);
}
});
} catch (error) {
console.error(`Invalid field: ${error.message}`);
}
})();
```
</CodeGroup>

View File

@ -25,7 +25,7 @@ paths:
Get a Workspace by ID.
If workspace_id is provided as a query parameter, it uses that (must match JWT workspace_id).
Otherwise, it uses the workspace_id from the JWT token.
Otherwise, it uses the workspace_id from the JWT.
operationId: get_or_create_workspace_v2_workspaces_post
requestBody:
content:
@ -480,7 +480,7 @@ paths:
Get a Peer by ID
If peer_id is provided as a query parameter, it uses that (must match JWT workspace_id).
Otherwise, it uses the peer_id from the JWT token.
Otherwise, it uses the peer_id from the JWT.
operationId: get_or_create_peer_v2_workspaces__workspace_id__peers_post
security:
- HTTPBearer: []
@ -972,7 +972,7 @@ paths:
Get a specific session in a workspace.
If session_id is provided as a query parameter, it verifies the session is in the workspace.
Otherwise, it uses the session_id from the JWT token for verification.
Otherwise, it uses the session_id from the JWT for verification.
operationId: get_or_create_session_v2_workspaces__workspace_id__sessions_post
security:
- HTTPBearer: []
@ -2114,7 +2114,7 @@ paths:
items:
$ref: '#/components/schemas/Message'
title: >-
Response Create Messages With File V2 Workspaces Workspace Id Sessions Session Id
Response Create Messages With File V2 Workspaces Workspace Id Sessions Session Id
Messages Upload Post
'422':
description: Validation Error
@ -2677,7 +2677,7 @@ components:
title: MessageCreate
MessageGet:
properties:
filter:
filters:
anyOf:
- additionalProperties: true
type: object
@ -2879,7 +2879,7 @@ components:
title: PeerCreate
PeerGet:
properties:
filter:
filters:
anyOf:
- additionalProperties: true
type: object
@ -3032,7 +3032,7 @@ components:
title: SessionDeriverStatus
SessionGet:
properties:
filter:
filters:
anyOf:
- additionalProperties: true
type: object
@ -3142,7 +3142,7 @@ components:
title: WorkspaceCreate
WorkspaceGet:
properties:
filter:
filters:
anyOf:
- additionalProperties: true
type: object
@ -3169,4 +3169,4 @@ components:
securitySchemes:
HTTPBearer:
type: http
scheme: bearer
scheme: bearer

2
mcp/.gitignore vendored
View File

@ -24,4 +24,4 @@ dist/
Thumbs.db
# Logs
*.log
*.log

View File

@ -2,11 +2,12 @@
## Quickstart: Use the Hosted MCP Server
Go to https://app.honcho.dev and get an API key. Then go to Claude Desktop and navigate to custom MCP servers.
Go to <https://app.honcho.dev> and get an API key. Then go to Claude Desktop and navigate to custom MCP servers.
If you don't have node/bun installed you will need to do that. You can also use npm if you already have that installed. If not, Claude Desktop or Claude Code can help!
Add Honcho to your Claude desktop config. You must provide a username for Honcho to refer to you as -- preferably what you want Claude to actually call you.
```json
{
"mcpServers": {
@ -62,18 +63,22 @@ You may customize your assistant name and/or workspace ID. Both are optional.
## Available Tools
### start_conversation
Start a new conversation session with Honcho. This initializes a session for tracking conversation history and context.
**Returns:** A session ID that you must store and use for all subsequent interactions in this conversation.
### add_turn
Add a conversation turn (user and assistant messages) to the current session. This stores the conversation in Honcho for context tracking.
**Parameters:**
- `session_id`: The ID of the session to add the turn to
- `messages`: Array of message objects with `role` ("user" or "assistant") and `content`
**Example usage:**
```json
{
"session_id": "session-uuid",
@ -83,7 +88,7 @@ Add a conversation turn (user and assistant messages) to the current session. Th
"content": "Hello, how are you?"
},
{
"role": "assistant",
"role": "assistant",
"content": "I'm doing well, thank you!"
}
]
@ -91,153 +96,195 @@ Add a conversation turn (user and assistant messages) to the current session. Th
```
### get_personalization_insights
Get personalization insights from Honcho based on conversation history. This queries the user's conversation context to provide personalized responses.
**Parameters:**
- `session_id`: The ID of the session for context
- `query`: The question about the user's preferences, habits, etc.
**Example queries:**
- "What does this message reveal about the user's communication preferences?"
- "How formal or casual should I be with the user based on our history?"
- "What emotional state might the user be in right now?"
### search_workspace
Search for messages across the entire workspace.
**Parameters:**
- `query`: The search query to use
### get_workspace_metadata
Get metadata for the current workspace.
**Parameters:** None
### set_workspace_metadata
Set metadata for the current workspace.
**Parameters:**
- `metadata`: A dictionary of metadata to associate with the workspace
### create_peer
Create or get a peer with the specified ID and optional configuration.
**Parameters:**
- `peer_id`: Unique identifier for the peer
- `config`: Optional configuration dictionary for the peer
### get_peer_metadata
Get metadata for a specific peer.
**Parameters:**
- `peer_id`: The ID of the peer to get metadata for
### set_peer_metadata
Set metadata for a specific peer.
**Parameters:**
- `peer_id`: The ID of the peer to set metadata for
- `metadata`: A dictionary of metadata to associate with the peer
### search_peer_messages
Search for messages sent by a peer.
**Parameters:**
- `peer_id`: The ID of the peer to search messages for
- `query`: The search query to use
### chat
Query a peer's representation with natural language questions.
**Parameters:**
- `peer_id`: The ID of the peer to query
- `query`: The natural language question to ask
- `target_peer_id`: Optional target peer ID for local representation queries
- `session_id`: Optional session ID to scope the query to a specific session
### list_peers
Get all peers in the current workspace.
**Parameters:** None
### create_session
Create or get a session with the specified ID and optional configuration.
**Parameters:**
- `session_id`: Unique identifier for the session
- `config`: Optional configuration dictionary for the session
### get_session_metadata
Get metadata for a specific session.
**Parameters:**
- `session_id`: The ID of the session to get metadata for
### set_session_metadata
Set metadata for a specific session.
**Parameters:**
- `session_id`: The ID of the session to set metadata for
- `metadata`: A dictionary of metadata to associate with the session
### add_peers_to_session
Add peers to a session.
**Parameters:**
- `session_id`: The ID of the session to add peers to
- `peer_ids`: List of peer IDs to add to the session
### remove_peers_from_session
Remove peers from a session.
**Parameters:**
- `session_id`: The ID of the session to remove peers from
- `peer_ids`: List of peer IDs to remove from the session
### get_session_peers
Get all peer IDs in a session.
**Parameters:**
- `session_id`: The ID of the session to get peers from
### add_messages_to_session
Add messages to a session.
**Parameters:**
- `session_id`: The ID of the session to add messages to
- `messages`: List of message dictionaries with `peer_id`, `content`, and optional `metadata`
### get_session_messages
Get messages from a session with optional filtering.
**Parameters:**
- `session_id`: The ID of the session to get messages from
- `filters`: Optional dictionary of filter criteria
### get_session_context
Get optimized context for a session within a token limit.
**Parameters:**
- `session_id`: The ID of the session to get context for
- `summary`: Whether to include summary information (default: true)
- `tokens`: Maximum number of tokens to include in the context
### search_session_messages
Search for messages in a specific session.
**Parameters:**
- `session_id`: The ID of the session to search messages in
- `query`: The search query to use
### get_working_representation
Get the current working representation of a peer in a session.
**Parameters:**
- `session_id`: The ID of the session
- `peer_id`: The ID of the peer to get the working representation of
- `target_peer_id`: Optional target peer ID to get the representation of what peer_id knows about target_peer_id
### list_sessions
Get all sessions in the current workspace.
**Parameters:** None
@ -249,11 +296,13 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
### Deploy MCP Worker
1. **Install dependencies:**
```bash
bun i
```
2. **Login to Cloudflare (if not already done):**
```bash
bun wrangler login
```
@ -263,11 +312,13 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
- Update the worker names in the `[env.production]` and `[env.staging]` sections
4. **Test locally:**
```bash
bun dev
```
5. **Deploy to production:**
```bash
bun run deploy
```
@ -277,13 +328,14 @@ A Cloudflare Worker that implements the Model Context Protocol (MCP) to provide
You can customize the behavior using HTTP headers:
**Available Configuration:**
- `apiKey`: Your Honcho API key
- `baseUrl`: Custom Honcho API base URL (default: https://api.honcho.dev)
- `baseUrl`: Custom Honcho API base URL (default: <https://api.honcho.dev>)
- `workspaceId`: Workspace ID (default: "default")
- `userName`: User identifier (default: "User")
- `assistantName`: Assistant identifier (default: "Assistant")
#### Using HTTP Headers:
#### Using HTTP Headers
Pass configuration to mcp-remote via custom headers:
@ -297,6 +349,7 @@ bunx mcp-remote https://YOUR_WORKER_NAME.YOUR_SUBDOMAIN.workers.dev \
```
**Supported Custom Headers:**
- `Authorization: Bearer YOUR_API_KEY` - Your Honcho API key
- `X-Honcho-Base-URL` - Custom Honcho API base URL
- `X-Honcho-Workspace-ID` - Workspace identifier
@ -326,6 +379,7 @@ The server provides proper JSON-RPC 2.0 error responses:
- `-32603`: Internal error
Common issues:
- **Missing API key**: Ensure you provide a valid Honcho API key via header or URL parameter
- **Invalid tool parameters**: Check that required parameters are provided and properly formatted
- **Network errors**: Verify the worker is deployed and accessible
- **Network errors**: Verify the worker is deployed and accessible

View File

@ -117,7 +117,7 @@ For subsequent messages in the same conversation:
session_id: "session_abc123"
messages: [
{
"role": "user",
"role": "user",
"content": "Thanks for listening. Can you help me prioritize my tasks?"
},
{
@ -153,4 +153,4 @@ Ask theory-of-mind questions that reveal:
3. **Use `get_personalization_insights` strategically for better responses**
4. **Ask thoughtful theory-of-mind questions**
5. **Never expose technical details to the user**
6. **The system maintains context automatically between sessions**
6. **The system maintains context automatically between sessions**

View File

@ -11,4 +11,4 @@
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
}

View File

@ -1341,4 +1341,4 @@ export default {
return createErrorResponse(requestData.id ?? null, -32603, errorMessage);
}
},
};
};

View File

@ -7,7 +7,7 @@ compatibility_flags = ["nodejs_compat"]
name = "honcho-mcp"
[env.staging]
name = "honcho-mcp-staging"
name = "honcho-mcp-staging"
[observability.logs]
enabled = true
enabled = true

View File

@ -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
""")

View File

@ -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"},
@ -31,8 +31,8 @@ dependencies = [
]
[tool.uv]
dev-dependencies = [
"honcho-core>=1.2.0",
"honcho-ai>=1.2.1",
"honcho-core==1.3.0",
"honcho-ai==1.2.2",
"pytest>=8.2.2",
"sqlalchemy-utils>=0.41.2",
"pytest-asyncio>=0.23.7",

View File

@ -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:

View File

@ -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

View File

@ -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",
]

View File

@ -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(

View File

@ -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:
"""

View File

@ -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.

View File

@ -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(

View File

@ -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:
"""

View File

@ -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.

View File

@ -61,18 +61,18 @@ class SessionContext(BaseModel):
self,
*,
assistant: str | Peer,
) -> list[dict[str, object]]:
) -> list[dict[str, str]]:
"""
Convert the context to OpenAI-compatible message format.
Transforms the message history into the format expected by OpenAI's
Chat Completions API, with proper role assignments based on the
Transforms the message history and summary into the format expected by
OpenAI's Chat Completions API, with proper role assignments based on the
assistant's identity.
Args:
assistant: The assistant peer (Peer object or peer ID string) to use
for determining message roles. Messages from this peer will
be marked as "assistant", others as "user"
for determining message roles. Messages from this peer will be marked
as "assistant", others as "user"
Returns:
A list of dictionaries in OpenAI format, where each dictionary contains
@ -80,26 +80,30 @@ class SessionContext(BaseModel):
"""
assistant_id = assistant if isinstance(assistant, str) else assistant.id
return [
summary_message = {
"role": "system",
"content": f"<summary>{self.summary}</summary>",
}
messages = [
{
"role": "assistant" if message.peer_id == assistant_id else "user",
"name": message.peer_id,
"content": message.content,
}
for message in self.messages
]
return [summary_message, *messages] if self.summary else messages
def to_anthropic(
self,
*,
assistant: str | Peer,
) -> list[dict[str, object]]:
) -> list[dict[str, str]]:
"""
Convert the context to Anthropic-compatible message format.
Transforms the message history into the format expected by Anthropic's
Claude API. TODO: Anthropic requires messages to alternate between
user and assistant roles, so this method may need to handle role
consolidation or filtering in the future.
Claude API, with proper role assignments based on the assistant's identity.
Args:
assistant: The assistant peer (Peer object or peer ID string) to use
@ -116,13 +120,23 @@ class SessionContext(BaseModel):
"""
assistant_id = assistant if isinstance(assistant, str) else assistant.id
return [
summary_message = {
"role": "user",
"content": f"<summary>{self.summary}</summary>",
}
messages = [
{
"role": "assistant" if message.peer_id == assistant_id else "user",
"role": "assistant",
"content": message.content,
}
if message.peer_id == assistant_id
else {
"role": "user",
"content": f"{message.peer_id}: {message.content}",
}
for message in self.messages
]
return [summary_message, *messages] if self.summary else messages
def __len__(self) -> int:
"""
@ -131,7 +145,7 @@ class SessionContext(BaseModel):
Returns:
The number of messages in this context
"""
return len(self.messages)
return len(self.messages) + (1 if self.summary else 0)
def __repr__(self) -> str:
"""
@ -140,4 +154,4 @@ class SessionContext(BaseModel):
Returns:
A string representation suitable for debugging
"""
return f"SessionContext(messages={len(self.messages)})"
return f"SessionContext(messages={len(self.messages)}, summary={self.summary})"

View File

@ -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", git = "https://github.com/plastic-labs/honcho-python-core.git?rev=2fc998040ba1781762584664b6e308f135792852" },
{ name = "httpx", specifier = ">=0.28.0,<1" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
]
@ -122,8 +122,8 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }]
[[package]]
name = "honcho-core"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
version = "1.3.0"
source = { git = "https://github.com/plastic-labs/honcho-python-core.git?rev=2fc998040ba1781762584664b6e308f135792852#2fc998040ba1781762584664b6e308f135792852" }
dependencies = [
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "anyio", version = "4.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
@ -135,10 +135,6 @@ 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" }
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" },
]
[[package]]
name = "httpcore"

View File

@ -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

View File

@ -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) {

View File

@ -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();
});
});
});

View File

@ -58,8 +58,8 @@ describe('Honcho SDK Integration Tests', () => {
const mockPeerData = { id: 'assistant', metadata: { role: 'ai' } };
const mockSessionData = { id: 'chat-session', metadata: { topic: 'general' } };
const mockMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'user' },
{ id: 'msg2', content: 'Hi there!', peer_name: 'assistant' },
{ id: 'msg1', content: 'Hello', peer_id: 'user' },
{ id: 'msg2', content: 'Hi there!', peer_id: 'assistant' },
];
const mockContextData = { messages: mockMessages, summary: 'Friendly greeting' };
@ -71,8 +71,8 @@ describe('Honcho SDK Integration Tests', () => {
mockWorkspacesApi.workspaces.peers.chat.mockResolvedValue({ content: 'AI response' });
// Step 1: Create peers
const user = honcho.peer('user');
const assistant = honcho.peer('assistant');
const user = await honcho.peer('user');
const assistant = await honcho.peer('assistant');
expect(user).toBeInstanceOf(Peer);
expect(assistant).toBeInstanceOf(Peer);
@ -80,7 +80,7 @@ describe('Honcho SDK Integration Tests', () => {
expect(assistant.id).toBe('assistant');
// Step 2: Create session
const session = honcho.session('chat-session');
const session = await honcho.session('chat-session');
expect(session).toBeInstanceOf(Session);
expect(session.id).toBe('chat-session');
@ -90,8 +90,8 @@ describe('Honcho SDK Integration Tests', () => {
'integration-test-workspace',
'chat-session',
{
'user': { observe_me: true, observe_others: false },
'assistant': { observe_me: true, observe_others: false }
'user': {},
'assistant': {}
}
);
@ -123,12 +123,14 @@ describe('Honcho SDK Integration Tests', () => {
const anthropicFormat = context.toAnthropic('assistant');
expect(openAIFormat).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' },
{ role: 'system', content: '<summary>Friendly greeting</summary>' },
{ role: 'user', content: 'Hello', name: 'user' },
{ role: 'assistant', content: 'Hi there!', name: 'assistant' },
]);
expect(anthropicFormat).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: '<summary>Friendly greeting</summary>' },
{ role: 'user', content: 'user: Hello' },
{ role: 'assistant', content: 'Hi there!' },
]);
@ -193,32 +195,17 @@ describe('Honcho SDK Integration Tests', () => {
it('should handle search functionality across different scopes', async () => {
// Setup mock responses
const mockWorkspaceSearchResults = {
items: [
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
const mockWorkspaceSearchResults = [
{ id: 'msg1', content: 'workspace message', peer_id: 'peer1' },
];
const mockPeerSearchResults = {
items: [
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
const mockPeerSearchResults = [
{ id: 'msg2', content: 'peer message', peer_id: 'peer1' },
];
const mockSessionSearchResults = {
items: [
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
],
total: 1,
size: 1,
hasNextPage: false,
};
const mockSessionSearchResults = [
{ id: 'msg3', content: 'session message', peer_id: 'peer1' },
];
mockWorkspacesApi.workspaces.search.mockResolvedValue(mockWorkspaceSearchResults);
mockWorkspacesApi.workspaces.peers.search.mockResolvedValue(mockPeerSearchResults);
@ -226,30 +213,30 @@ describe('Honcho SDK Integration Tests', () => {
// Step 1: Search workspace
const workspaceResults = await honcho.search('test query');
expect(workspaceResults).toBeInstanceOf(Page);
expect(Array.isArray(workspaceResults)).toBe(true);
expect(mockWorkspacesApi.workspaces.search).toHaveBeenCalledWith(
'integration-test-workspace',
{ body: 'test query' }
{ query: 'test query', limit: undefined }
);
// Step 2: Search peer
const peer = honcho.peer('test-peer');
const peer = await honcho.peer('test-peer');
const peerResults = await peer.search('peer query');
expect(peerResults).toBeInstanceOf(Page);
expect(Array.isArray(peerResults)).toBe(true);
expect(mockWorkspacesApi.workspaces.peers.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-peer',
{ query: 'peer query' }
{ query: 'peer query', limit: undefined }
);
// Step 3: Search session
const session = honcho.session('test-session');
const session = await honcho.session('test-session');
const sessionResults = await session.search('session query');
expect(sessionResults).toBeInstanceOf(Page);
expect(Array.isArray(sessionResults)).toBe(true);
expect(mockWorkspacesApi.workspaces.sessions.search).toHaveBeenCalledWith(
'integration-test-workspace',
'test-session',
{ query: 'session query' }
{ query: 'session query', limit: undefined }
);
});
@ -258,14 +245,14 @@ describe('Honcho SDK Integration Tests', () => {
mockWorkspacesApi.workspaces.peers.chat.mockRejectedValue(new Error('Chat API failed'));
mockWorkspacesApi.workspaces.sessions.getContext.mockRejectedValue(new Error('Context API failed'));
const assistant = honcho.peer('assistant');
const session = honcho.session('error-session');
const assistant = await honcho.peer('assistant');
const session = await honcho.session('error-session');
// Test error handling in chat
await expect(assistant.chat('Hello')).rejects.toThrow('Chat API failed');
await expect(assistant.chat('Hello')).rejects.toThrow();
// Test error handling in context
await expect(session.getContext()).rejects.toThrow('Context API failed');
await expect(session.getContext()).rejects.toThrow();
});
it('should handle pagination correctly', async () => {
@ -329,9 +316,9 @@ describe('Honcho SDK Integration Tests', () => {
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(mockWorkingRep);
const session = honcho.session('working-rep-session');
const alice = honcho.peer('alice');
const bob = honcho.peer('bob');
const session = await honcho.session('working-rep-session');
const alice = await honcho.peer('alice');
const bob = await honcho.peer('bob');
// Test working representation without target
const globalRep = await session.workingRep('alice');
@ -359,8 +346,8 @@ describe('Honcho SDK Integration Tests', () => {
mockWorkspacesApi.workspaces.peers.list.mockResolvedValue({ items: [], total: 0, hasNextPage: false });
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({ messages: [] });
const peer = honcho.peer('empty-peer');
const session = honcho.session('empty-session');
const peer = await honcho.peer('empty-peer');
const session = await honcho.session('empty-session');
// Test null chat response
const chatResult = await peer.chat('Hello');
@ -379,20 +366,20 @@ describe('Honcho SDK Integration Tests', () => {
it('should maintain type safety throughout the workflow', async () => {
// This test verifies TypeScript types are maintained correctly
const peer: Peer = honcho.peer('typed-peer');
const session: Session = honcho.session('typed-session');
const peer: Peer = await honcho.peer('typed-peer');
const session: Session = await honcho.session('typed-session');
expect(typeof peer.id).toBe('string');
expect(typeof session.id).toBe('string');
const message = peer.message('typed message', { metadata: { type: 'test' } });
expect(typeof message.peerId).toBe('string');
expect(typeof message.peer_id).toBe('string');
expect(typeof message.content).toBe('string');
expect(typeof message.metadata).toBe('object');
// Mock successful operations
mockWorkspacesApi.workspaces.sessions.getContext.mockResolvedValue({
messages: [{ id: 'msg1', content: 'Hello', peer_name: 'typed-peer' }],
messages: [{ id: 'msg1', content: 'Hello', peer_id: 'typed-peer' }],
summary: 'Test summary',
});

View File

@ -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 () => {

View File

@ -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();
});
});
});

View File

@ -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();
});
});
});

View File

@ -1,26 +1,42 @@
import { SessionContext } from '../src/session_context';
import { Peer } from '../src/peer';
/**
* Helper function to create a proper Message object for testing
*/
function createTestMessage(id: string, content: string, peer_id: string, additionalProps: any = {}): any {
return {
id,
content,
peer_id,
created_at: new Date().toISOString(),
session_id: 'test-session',
token_count: 0,
workspace_id: 'test-workspace',
...additionalProps
};
}
describe('SessionContext', () => {
let sessionContext: SessionContext;
let mockMessages: any[];
beforeEach(() => {
mockMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'Hi there', peer_name: 'user' },
{ id: 'msg3', content: 'How are you?', peer_name: 'user' },
{ id: 'msg4', content: 'I am doing well, thank you!', peer_name: 'assistant' },
createTestMessage('msg1', 'Hello', 'assistant'),
createTestMessage('msg2', 'Hi there', 'user'),
createTestMessage('msg3', 'How are you?', 'user'),
createTestMessage('msg4', 'I am doing well, thank you!', 'assistant'),
];
sessionContext = new SessionContext('test-session', mockMessages, 'This is a summary');
sessionContext = new SessionContext('test-session', mockMessages, '');
});
describe('constructor', () => {
it('should initialize with all properties', () => {
expect(sessionContext.sessionId).toBe('test-session');
expect(sessionContext.messages).toEqual(mockMessages);
expect(sessionContext.summary).toBe('This is a summary');
expect(sessionContext.summary).toBe('');
});
it('should initialize with empty summary when not provided', () => {
@ -53,24 +69,24 @@ describe('SessionContext', () => {
const openAIMessages = sessionContext.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
it('should convert messages to OpenAI format with Peer object', () => {
const mockHoncho = {} as any;
const assistantPeer = new Peer('assistant', mockHoncho);
const mockClient = {} as any;
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
const openAIMessages = sessionContext.toOpenAI(assistantPeer);
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
@ -78,10 +94,10 @@ describe('SessionContext', () => {
const openAIMessages = sessionContext.toOpenAI('different-assistant');
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'I am doing well, thank you!' },
{ role: 'user', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'user', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
@ -92,37 +108,50 @@ describe('SessionContext', () => {
expect(openAIMessages).toEqual([]);
});
it('should handle messages with missing peer_name', () => {
it('should include summary message when summary exists', () => {
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
const openAIMessages = contextWithSummary.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'system', content: '<summary>This is a summary</summary>' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'Hi there', name: 'user' },
{ role: 'user', content: 'How are you?', name: 'user' },
{ role: 'assistant', content: 'I am doing well, thank you!', name: 'assistant' },
]);
});
it('should handle messages with missing peer_id', () => {
const messagesWithMissingPeer = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'No peer' }, // missing peer_name
{ id: 'msg3', content: 'Another message', peer_name: null },
createTestMessage('msg1', 'Hello', 'assistant'),
createTestMessage('msg2', 'No peer', ''), // missing peer_id
createTestMessage('msg3', 'Another message', ''), // null peer_id
];
const context = new SessionContext('test', messagesWithMissingPeer);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'No peer' },
{ role: 'user', content: 'Another message' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
{ role: 'user', content: 'No peer', name: '' },
{ role: 'user', content: 'Another message', name: '' },
]);
});
it('should handle complex message content', () => {
const complexMessages = [
{ id: 'msg1', content: 'Message with\nnewlines and special chars!@#$%', peer_name: 'assistant' },
{ id: 'msg2', content: '', peer_name: 'user' }, // empty content
{ id: 'msg3', content: ' whitespace ', peer_name: 'assistant' },
createTestMessage('msg1', 'Message with\nnewlines and special chars!@#$%', 'assistant'),
createTestMessage('msg2', '', 'user'), // empty content
createTestMessage('msg3', ' whitespace ', 'assistant'),
];
const context = new SessionContext('test', complexMessages);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%' },
{ role: 'user', content: '' },
{ role: 'assistant', content: ' whitespace ' },
{ role: 'assistant', content: 'Message with\nnewlines and special chars!@#$%', name: 'assistant' },
{ role: 'user', content: '', name: 'user' },
{ role: 'assistant', content: ' whitespace ', name: 'assistant' },
]);
});
});
@ -133,22 +162,22 @@ describe('SessionContext', () => {
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should convert messages to Anthropic format with Peer object', () => {
const mockHoncho = {} as any;
const assistantPeer = new Peer('assistant', mockHoncho);
const mockClient = {} as any;
const assistantPeer = new Peer('assistant', 'test-workspace', mockClient);
const anthropicMessages = sessionContext.toAnthropic(assistantPeer);
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
@ -157,10 +186,10 @@ describe('SessionContext', () => {
const anthropicMessages = sessionContext.toAnthropic('different-assistant');
expect(anthropicMessages).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'user', content: 'I am doing well, thank you!' },
{ role: 'user', content: 'assistant: Hello' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'user', content: 'assistant: I am doing well, thank you!' },
]);
});
@ -171,11 +200,24 @@ describe('SessionContext', () => {
expect(anthropicMessages).toEqual([]);
});
it('should handle messages with missing peer_name', () => {
it('should include summary message when summary exists', () => {
const contextWithSummary = new SessionContext('test-session', mockMessages, 'This is a summary');
const anthropicMessages = contextWithSummary.toAnthropic('assistant');
expect(anthropicMessages).toEqual([
{ role: 'user', content: '<summary>This is a summary</summary>' },
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'user: Hi there' },
{ role: 'user', content: 'user: How are you?' },
{ role: 'assistant', content: 'I am doing well, thank you!' },
]);
});
it('should handle messages with missing peer_id', () => {
const messagesWithMissingPeer = [
{ id: 'msg1', content: 'Hello', peer_name: 'assistant' },
{ id: 'msg2', content: 'No peer' }, // missing peer_name
{ id: 'msg3', content: 'Another message', peer_name: undefined },
createTestMessage('msg1', 'Hello', 'assistant'),
createTestMessage('msg2', 'No peer', ''), // missing peer_id
createTestMessage('msg3', 'Another message', ''), // undefined peer_id
];
const context = new SessionContext('test', messagesWithMissingPeer);
@ -183,8 +225,8 @@ describe('SessionContext', () => {
expect(anthropicMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'No peer' },
{ role: 'user', content: 'Another message' },
{ role: 'user', content: ': No peer' },
{ role: 'user', content: ': Another message' },
]);
});
});
@ -208,58 +250,56 @@ describe('SessionContext', () => {
describe('toString', () => {
it('should return correct string representation', () => {
const result = sessionContext.toString();
expect(result).toBe('SessionContext(messages=4)');
expect(result).toBe('SessionContext(messages=4, summary=)');
});
it('should handle empty messages', () => {
const emptyContext = new SessionContext('session-id', []);
const result = emptyContext.toString();
expect(result).toBe('SessionContext(messages=0)');
expect(result).toBe('SessionContext(messages=0, summary=)');
});
it('should handle large number of messages', () => {
const manyMessages = Array.from({ length: 1000 }, (_, i) => ({
id: `msg${i}`,
content: `Message ${i}`,
peer_name: i % 2 === 0 ? 'assistant' : 'user',
}));
const manyMessages = Array.from({ length: 1000 }, (_, i) =>
createTestMessage(`msg${i}`, `Message ${i}`, i % 2 === 0 ? 'assistant' : 'user')
);
const context = new SessionContext('session-id', manyMessages);
const result = context.toString();
expect(result).toBe('SessionContext(messages=1000)');
expect(result).toBe('SessionContext(messages=1000, summary=)');
});
});
describe('edge cases and error handling', () => {
it('should handle messages with null content', () => {
const messagesWithNullContent = [
{ id: 'msg1', content: null, peer_name: 'assistant' },
{ id: 'msg2', content: undefined, peer_name: 'user' },
createTestMessage('msg1', null as any, 'assistant'),
createTestMessage('msg2', undefined as any, 'user'),
];
const context = new SessionContext('test', messagesWithNullContent);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: null },
{ role: 'user', content: undefined },
{ role: 'assistant', content: null, name: 'assistant' },
{ role: 'user', content: undefined, name: 'user' },
]);
});
it('should handle messages with non-string content', () => {
const messagesWithNonStringContent = [
{ id: 'msg1', content: 123, peer_name: 'assistant' },
{ id: 'msg2', content: { text: 'object content' }, peer_name: 'user' },
{ id: 'msg3', content: true, peer_name: 'assistant' },
createTestMessage('msg1', 123 as any, 'assistant'),
createTestMessage('msg2', { text: 'object content' } as any, 'user'),
createTestMessage('msg3', true as any, 'assistant'),
];
const context = new SessionContext('test', messagesWithNonStringContent);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 123 },
{ role: 'user', content: { text: 'object content' } },
{ role: 'assistant', content: true },
{ role: 'assistant', content: 123, name: 'assistant' },
{ role: 'user', content: { text: 'object content' }, name: 'user' },
{ role: 'assistant', content: true, name: 'assistant' },
]);
});
@ -270,57 +310,54 @@ describe('SessionContext', () => {
expect(context.sessionId).toBe(longSessionId);
expect(context.summary).toBe(longSummary);
expect(context.length).toBe(4);
expect(context.length).toBe(5); // 4 messages + 1 summary
});
it('should handle messages with additional properties', () => {
const messagesWithExtraProps = [
{
id: 'msg1',
content: 'Hello',
peer_name: 'assistant',
createTestMessage('msg1', 'Hello', 'assistant', {
timestamp: '2023-01-01T00:00:00Z',
metadata: { important: true },
extra_field: 'extra_value'
},
}),
];
const context = new SessionContext('test', messagesWithExtraProps);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'assistant', content: 'Hello', name: 'assistant' },
]);
});
it('should handle case-sensitive peer names', () => {
const caseMessages = [
{ id: 'msg1', content: 'Hello', peer_name: 'Assistant' },
{ id: 'msg2', content: 'Hi', peer_name: 'ASSISTANT' },
{ id: 'msg3', content: 'Hey', peer_name: 'assistant' },
createTestMessage('msg1', 'Hello', 'Assistant'),
createTestMessage('msg2', 'Hi', 'ASSISTANT'),
createTestMessage('msg3', 'Hey', 'assistant'),
];
const context = new SessionContext('test', caseMessages);
const openAIMessages = context.toOpenAI('assistant');
expect(openAIMessages).toEqual([
{ role: 'user', content: 'Hello' }, // 'Assistant' != 'assistant'
{ role: 'user', content: 'Hi' }, // 'ASSISTANT' != 'assistant'
{ role: 'assistant', content: 'Hey' }, // exact match
{ role: 'user', content: 'Hello', name: 'Assistant' }, // 'Assistant' != 'assistant'
{ role: 'user', content: 'Hi', name: 'ASSISTANT' }, // 'ASSISTANT' != 'assistant'
{ role: 'assistant', content: 'Hey', name: 'assistant' }, // exact match
]);
});
it('should handle messages without id field', () => {
const messagesWithoutId = [
{ content: 'Message without ID', peer_name: 'assistant' },
{ peer_name: 'user', content: 'Another message' },
createTestMessage('', 'Message without ID', 'assistant'),
createTestMessage('', 'Another message', 'user'),
];
const context = new SessionContext('test', messagesWithoutId);
expect(context.length).toBe(2);
expect(context.toOpenAI('assistant')).toEqual([
{ role: 'assistant', content: 'Message without ID' },
{ role: 'user', content: 'Another message' },
{ role: 'assistant', content: 'Message without ID', name: 'assistant' },
{ role: 'user', content: 'Another message', name: 'user' },
]);
});
});

View File

@ -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=="],
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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})`);
}

View File

@ -1,6 +1,6 @@
{
"name": "@honcho-ai/sdk",
"version": "1.2.1",
"version": "1.3.0",
"description": "Official DX Optimized TypeScript SDK for Honcho",
"author": "Plastic Labs <hello@plasticlabs.ai>",
"license": "Apache-2.0",
@ -20,8 +20,9 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@honcho-ai/core": "1.3.0",
"@types/node": "^24.0.1",
"@honcho-ai/core": "1.2.0"
"zod": "4.0.0"
},
"devDependencies": {
"@biomejs/biome": "^2.1.2",

View File

@ -1,94 +1,272 @@
import HonchoCore from '@honcho-ai/core'
import type { DefaultQuery } from '@honcho-ai/core/src/core'
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
import type {
DeriverStatus,
WorkspaceDeriverStatusParams,
} from '@honcho-ai/core/src/resources/workspaces/workspaces'
import { Page } from './pagination'
import { Peer } from './peer'
import { Session } from './session'
import {
type DeriverStatusOptions,
DeriverStatusOptionsSchema,
FilterSchema,
type Filters,
type HonchoConfig,
HonchoConfigSchema,
LimitSchema,
type PeerConfig,
PeerConfigSchema,
PeerIdSchema,
type PeerMetadata,
PeerMetadataSchema,
SearchQuerySchema,
type SessionConfig,
SessionConfigSchema,
SessionIdSchema,
type SessionMetadata,
SessionMetadataSchema,
type WorkspaceMetadata,
WorkspaceMetadataSchema,
} from './validation'
/**
* Main client for the Honcho TypeScript SDK.
* Provides access to peers, sessions, and workspace operations.
*
* Provides access to peers, sessions, and workspace operations with configuration
* from environment variables or explicit parameters. This is the primary entry
* point for interacting with the Honcho conversational memory platform.
*
* For advanced usage, the underlying @honcho-ai/core client can be accessed via the
* `core` property to use functionality not exposed through this SDK.
*
* @example
* ```typescript
* const honcho = new Honcho({
* apiKey: 'your-api-key',
* workspaceId: 'your-workspace-id'
* })
*
* const peer = await honcho.peer('user123')
* const session = await honcho.session('session456')
* ```
*/
export class Honcho {
private _client: InstanceType<typeof HonchoCore>
/**
* Workspace ID for scoping operations.
*/
readonly workspaceId: string
/**
* Reference to the core Honcho client instance.
*/
private _client: HonchoCore
/**
* Access the underlying @honcho-ai/core client. The @honcho-ai/core client is the raw Stainless-generated client,
* allowing users to access functionality that is not exposed through this SDK.
*
* @returns The underlying HonchoCore client instance
*
* @example
* ```typescript
* import { Honcho } from '@honcho-ai/sdk';
*
* const client = new Honcho();
*
* const workspace = await client.core.workspaces.getOrCreate({ id: "custom-workspace-id" });
* ```
*/
get core(): InstanceType<typeof HonchoCore> {
return this._client
}
/**
* Initialize the Honcho client.
*
* @param options - Configuration options for the client
* @param options.apiKey - API key for authentication. If not provided, will attempt to
* read from HONCHO_API_KEY environment variable
* @param options.environment - Environment to use (local, production, or demo)
* @param options.baseURL - Base URL for the Honcho API. If not provided, will attempt to
* read from HONCHO_URL environment variable or default to the
* production API URL
* @param options.workspaceId - Workspace ID to use for operations. If not provided, will
* attempt to read from HONCHO_WORKSPACE_ID environment variable
* or default to "default"
* @param options.timeout - Optional custom timeout for the HTTP client
* @param options.maxRetries - Optional custom maximum number of retries for the HTTP client
* @param options.defaultHeaders - Optional custom default headers for the HTTP client
* @param options.defaultQuery - Optional custom default query parameters for the HTTP client
*/
constructor(options: {
apiKey?: string
environment?: 'local' | 'production' | 'demo'
baseURL?: string
workspaceId?: string
timeout?: number
maxRetries?: number
defaultHeaders?: Record<string, string>
defaultQuery?: Record<string, unknown>
}) {
constructor(options: HonchoConfig) {
const validatedOptions = HonchoConfigSchema.parse(options)
this.workspaceId =
options.workspaceId || process.env.HONCHO_WORKSPACE_ID || 'default'
validatedOptions.workspaceId ||
process.env.HONCHO_WORKSPACE_ID ||
'default'
this._client = new HonchoCore({
apiKey: options.apiKey || process.env.HONCHO_API_KEY,
environment: options.environment,
baseURL: options.baseURL || process.env.HONCHO_URL,
timeout: options.timeout,
maxRetries: options.maxRetries,
defaultHeaders: options.defaultHeaders,
defaultQuery: options.defaultQuery as any,
}) as any
apiKey: validatedOptions.apiKey || process.env.HONCHO_API_KEY,
environment: validatedOptions.environment,
baseURL: validatedOptions.baseURL || process.env.HONCHO_URL,
timeout: validatedOptions.timeout,
maxRetries: validatedOptions.maxRetries,
defaultHeaders: validatedOptions.defaultHeaders,
defaultQuery: validatedOptions.defaultQuery as DefaultQuery,
})
// Note: Constructor cannot be async, so we can't await here
// The workspace will be created on first use if it doesn't exist
// due to the upsert behavior of the API
this._client.workspaces.getOrCreate({ id: this.workspaceId })
}
/**
* Get or create a peer with the given ID.
*
* Creates a Peer object that can be used to interact with the specified peer.
* If metadata or config is provided, makes an API call to get/create the peer
* immediately with those values.
*
* Provided metadata and configuration will overwrite existing data for this peer
* if it already exists.
*
* @param id - Unique identifier for the peer within the workspace. Should be a
* stable identifier that can be used consistently across sessions.
* @param metadata - Optional metadata dictionary to associate with this peer.
* If set, will get/create peer immediately with metadata.
* @param config - Optional configuration to set for this peer.
* If set, will get/create peer immediately with flags.
* @returns Promise resolving to a Peer object that can be used to send messages,
* join sessions, and query the peer's knowledge representations
* @throws Error if the peer ID is empty or invalid
*/
peer(id: string, options?: { config?: Record<string, unknown> }): Peer {
if (!id || typeof id !== 'string') {
throw new Error('Peer ID must be a non-empty string')
async peer(
id: string,
options?: {
metadata?: PeerMetadata
config?: PeerConfig
}
return new Peer(id, this, options?.config)
): Promise<Peer> {
const validatedId = PeerIdSchema.parse(id)
const validatedMetadata = options?.metadata
? PeerMetadataSchema.parse(options.metadata)
: undefined
const validatedConfig = options?.config
? PeerConfigSchema.parse(options.config)
: undefined
const peer = new Peer(validatedId, this.workspaceId, this._client)
if (validatedConfig || validatedMetadata) {
await this._client.workspaces.peers.getOrCreate(this.workspaceId, {
id: peer.id,
configuration: validatedConfig,
metadata: validatedMetadata,
})
}
return peer
}
/**
* Get all peers in the current workspace.
*
* Makes an API call to retrieve all peers that have been created or used
* within the current workspace. Returns a paginated result.
*
* @param filters - Optional filter criteria for peers. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @returns Promise resolving to a Page of Peer objects representing all peers in the workspace
*/
async getPeers(
filter?: { [key: string]: unknown } | null
): Promise<Page<Peer>> {
async getPeers(filters?: Filters): Promise<Page<Peer>> {
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
const peersPage = await this._client.workspaces.peers.list(
this.workspaceId,
{ filter }
{ filters: validatedFilter }
)
return new Page(
peersPage,
(peer) => new Peer(peer.id, this.workspaceId, this._client)
)
return new Page(peersPage, (peer: any) => new Peer(peer.id, this))
}
/**
* Get or create a session with the given ID.
*
* Creates a Session object that can be used to manage conversations between
* multiple peers. If metadata or config is provided, makes an API call to
* get/create the session immediately with those values.
*
* Provided metadata and configuration will overwrite existing data for this session
* if it already exists.
*
* @param id - Unique identifier for the session within the workspace. Should be a
* stable identifier that can be used consistently to reference the
* same conversation
* @param metadata - Optional metadata dictionary to associate with this session.
* If set, will get/create session immediately with metadata.
* @param config - Optional configuration to set for this session.
* If set, will get/create session immediately with flags.
* @returns Promise resolving to a Session object that can be used to add peers,
* send messages, and manage conversation context
* @throws Error if the session ID is empty or invalid
*/
session(id: string, options?: { config?: Record<string, unknown> }): Session {
if (!id || typeof id !== 'string') {
throw new Error('Session ID must be a non-empty string')
async session(
id: string,
options?: {
metadata?: SessionMetadata
config?: SessionConfig
}
return new Session(id, this, options?.config)
): Promise<Session> {
const validatedId = SessionIdSchema.parse(id)
const validatedMetadata = options?.metadata
? SessionMetadataSchema.parse(options.metadata)
: undefined
const validatedConfig = options?.config
? SessionConfigSchema.parse(options.config)
: undefined
const session = new Session(validatedId, this.workspaceId, this._client)
if (validatedConfig || validatedMetadata) {
await this._client.workspaces.sessions.getOrCreate(this.workspaceId, {
id: session.id,
configuration: validatedConfig,
metadata: validatedMetadata,
})
}
return session
}
/**
* Get all sessions in the current workspace.
*
* Makes an API call to retrieve all sessions that have been created within
* the current workspace.
*
* @param filters - Optional filter criteria for sessions. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @returns Promise resolving to a Page of Session objects representing all sessions
* in the workspace. Returns an empty page if no sessions exist
*/
async getSessions(
filter?: { [key: string]: unknown } | null
): Promise<Page<Session>> {
async getSessions(filters?: Filters): Promise<Page<Session>> {
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
const sessionsPage = await this._client.workspaces.sessions.list(
this.workspaceId,
{ filter }
{ filters: validatedFilter }
)
return new Page(
sessionsPage,
(session: any) => new Session(session.id, this)
(session) => new Session(session.id, this.workspaceId, this._client)
)
}
/**
* Get metadata for the current workspace.
*
* Makes an API call to retrieve metadata associated with the current workspace.
* Workspace metadata can include settings, configuration, or any other
* key-value data associated with the workspace.
*
* @returns Promise resolving to a dictionary containing the workspace's metadata.
* Returns an empty dictionary if no metadata is set
*/
async getMetadata(): Promise<Record<string, unknown>> {
const workspace = await this._client.workspaces.getOrCreate({
@ -99,18 +277,35 @@ export class Honcho {
/**
* Set metadata for the current workspace.
*
* Makes an API call to update the metadata associated with the current workspace.
* This will overwrite any existing metadata with the provided values.
*
* @param metadata - A dictionary of metadata to associate with the workspace.
* Keys must be strings, values can be any JSON-serializable type
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await this._client.workspaces.update(this.workspaceId, { metadata })
async setMetadata(metadata: WorkspaceMetadata): Promise<void> {
const validatedMetadata = WorkspaceMetadataSchema.parse(metadata)
await this._client.workspaces.update(this.workspaceId, {
metadata: validatedMetadata,
})
}
/**
* Get all workspace IDs from the Honcho instance.
*
* Makes an API call to retrieve all workspace IDs that the authenticated
* user has access to.
*
* @param filters - Optional filter criteria for workspaces. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @returns Promise resolving to a list of workspace ID strings. Returns an empty
* list if no workspaces are accessible or none exist
*/
async getWorkspaces(
filter?: { [key: string]: unknown } | null
): Promise<string[]> {
const workspacesPage = await this._client.workspaces.list({ filter })
async getWorkspaces(filters?: Filters): Promise<string[]> {
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
const workspacesPage = await this._client.workspaces.list({
filters: validatedFilter,
})
const ids: string[] = []
for await (const workspace of workspacesPage) {
ids.push(workspace.id)
@ -123,45 +318,63 @@ export class Honcho {
*
* Makes an API call to search for messages in the current workspace.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
* @param query - The search query to use
* @param filters - Optional filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @param limit - Number of results to return (1-100, default: 10).
* @returns Promise resolving to an array of Message objects representing the search results.
* Returns an empty array if no messages are found.
* @throws Error if the search query is empty or invalid
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string')
async search(
query: string,
options?: {
filters?: Filters
limit?: number
}
const messagesPage = await this._client.workspaces.search(
this.workspaceId,
{ body: query }
)
return new Page(messagesPage)
): Promise<Message[]> {
const validatedQuery = SearchQuerySchema.parse(query)
const validatedFilters = options?.filters
? FilterSchema.parse(options.filters)
: undefined
const validatedLimit = options?.limit
? LimitSchema.parse(options.limit)
: undefined
return await this._client.workspaces.search(this.workspaceId, {
query: validatedQuery,
filters: validatedFilters,
limit: validatedLimit,
})
}
/**
* Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
*
* @param options Configuration options for the status request
* @param options.observerId Optional observer ID to scope the status to
* @param options.senderId Optional sender ID to scope the status to
* @param options.sessionId Optional session ID to scope the status to
* @returns Promise resolving to the deriver status information
* Makes an API call to retrieve the current status of the deriver processing queue.
* The deriver is responsible for processing messages and updating peer representations.
*
* @param options - Configuration options for the status request
* @param options.observerId - Optional observer ID to scope the status to
* @param options.senderId - Optional sender ID to scope the status to
* @param options.sessionId - Optional session ID to scope the status to
* @returns Promise resolving to the deriver status information including work unit counts
*/
async getDeriverStatus(options?: {
observerId?: string
senderId?: string
sessionId?: string
}): Promise<{
async getDeriverStatus(options?: DeriverStatusOptions): Promise<{
totalWorkUnits: number
completedWorkUnits: number
inProgressWorkUnits: number
pendingWorkUnits: number
sessions?: Record<string, any>
sessions?: Record<string, DeriverStatus.Sessions>
}> {
const queryParams: any = {}
if (options?.observerId) queryParams.observer_id = options.observerId
if (options?.senderId) queryParams.sender_id = options.senderId
if (options?.sessionId) queryParams.session_id = options.sessionId
const validatedOptions = options
? DeriverStatusOptionsSchema.parse(options)
: undefined
const queryParams: WorkspaceDeriverStatusParams = {}
if (validatedOptions?.observerId)
queryParams.observer_id = validatedOptions.observerId
if (validatedOptions?.senderId)
queryParams.sender_id = validatedOptions.senderId
if (validatedOptions?.sessionId)
queryParams.session_id = validatedOptions.sessionId
const status = await this._client.workspaces.deriverStatus(
this.workspaceId,
@ -184,31 +397,29 @@ export class Honcho {
*
* The polling estimates sleep time by assuming each work unit takes 1 second.
*
* @param options Configuration options for the status request
* @param options.observerId Optional observer ID to scope the status to
* @param options.senderId Optional sender ID to scope the status to
* @param options.sessionId Optional session ID to scope the status to
* @param options.timeoutMs Optional timeout in milliseconds (default: 300000 - 5 minutes)
* @param options - Configuration options for the status request
* @param options.observerId - Optional observer ID to scope the status to
* @param options.senderId - Optional sender ID to scope the status to
* @param options.sessionId - Optional session ID to scope the status to
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
* @returns Promise resolving to the final deriver status when processing is complete
* @throws Error if timeout is exceeded before processing completes
*/
async pollDeriverStatus(options?: {
observerId?: string
senderId?: string
sessionId?: string
timeoutMs?: number
}): Promise<{
async pollDeriverStatus(options?: DeriverStatusOptions): Promise<{
totalWorkUnits: number
completedWorkUnits: number
inProgressWorkUnits: number
pendingWorkUnits: number
sessions?: Record<string, any>
sessions?: Record<string, DeriverStatus.Sessions>
}> {
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
const validatedOptions = options
? DeriverStatusOptionsSchema.parse(options)
: undefined
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
const startTime = Date.now()
while (true) {
const status = await this.getDeriverStatus(options)
const status = await this.getDeriverStatus(validatedOptions)
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
return status
}
@ -237,4 +448,13 @@ export class Honcho {
}
}
}
/**
* Return a string representation of the Honcho client.
*
* @returns A string representation suitable for debugging
*/
toString(): string {
return `Honcho(workspaceId='${this.workspaceId}', baseURL='${this._client.baseURL}')`
}
}

View File

@ -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'

View File

@ -1,55 +1,95 @@
import type { Honcho } from './client'
import type HonchoCore from '@honcho-ai/core'
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
import { Page } from './pagination'
import { Session } from './session'
import {
ChatQuerySchema,
FilterSchema,
type Filters,
LimitSchema,
MessageContentSchema,
MessageMetadataSchema,
SearchQuerySchema,
type MessageCreate as ValidatedMessageCreate,
} from './validation'
/**
* Represents a peer in the Honcho system.
*
* Peers can send messages, participate in sessions, and maintain both global
* and local representations for contextual interactions. A peer represents
* an entity (user, assistant, etc.) that can communicate within the system.
*/
export class Peer {
/**
* Unique identifier for this peer.
*/
readonly id: string
private _honcho: Honcho
/**
* Workspace ID for scoping operations.
*/
readonly workspaceId: string
/**
* Reference to the parent Honcho client instance.
*/
private _client: HonchoCore
/**
* Initialize a new Peer.
* Initialize a new Peer. **Do not call this directly, use the client.peer() method instead.**
*
* @param id - Unique identifier for this peer within the workspace
* @param workspaceId - Workspace ID for scoping operations
* @param client - Reference to the parent Honcho client instance
*/
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
constructor(id: string, workspaceId: string, client: HonchoCore) {
this.id = id
this._honcho = honcho
if (config) {
this._honcho['_client'].workspaces.peers.getOrCreate(
this._honcho.workspaceId,
{ id: this.id, configuration: config }
)
}
this.workspaceId = workspaceId
this._client = client
}
/**
* Query the peer's representation with a natural language question.
*
* Makes an API call to the Honcho dialectic endpoint to query either the peer's
* global representation (all content associated with this peer) or their local
* representation of another peer (what this peer knows about the target peer).
*
* @param query - The natural language question to ask
* @param stream - Whether to stream the response
* @param target - Optional target peer for local representation query. If provided,
* queries what this peer knows about the target peer rather than
* querying the peer's global representation
* @param sessionId - Optional session ID to scope the query to a specific session.
* If provided, only information from that session is considered
* @returns Promise resolving to response string containing the answer to the query,
* or null if no relevant information is available
*/
async chat(
query: string,
opts?: {
options?: {
stream?: boolean
target?: string | Peer
sessionId?: string
}
): Promise<string | null> {
const response = await this._honcho['_client'].workspaces.peers.chat(
this._honcho.workspaceId,
const chatParams = ChatQuerySchema.parse({
query,
stream: options?.stream,
target: options?.target,
sessionId: options?.sessionId,
})
const response = await this._client.workspaces.peers.chat(
this.workspaceId,
this.id,
{
query,
stream: opts?.stream,
target: opts?.target
? typeof opts.target === 'string'
? opts.target
: opts.target.id
query: chatParams.query,
stream: chatParams.stream,
target: chatParams.target
? typeof chatParams.target === 'string'
? chatParams.target
: chatParams.target.id
: undefined,
session_id: opts?.sessionId,
session_id: chatParams.sessionId,
}
)
if (!response.content || response.content === 'None') {
@ -60,38 +100,68 @@ export class Peer {
/**
* Get all sessions this peer is a member of.
*
* Makes an API call to retrieve all sessions where this peer is an active participant.
* Sessions are created when peers are added to them or send messages to them.
*
* @param filters - Optional filter criteria for sessions. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @returns Promise resolving to a paginated list of Session objects this peer belongs to.
* Returns an empty list if the peer is not a member of any sessions
*/
async getSessions(
filter?: { [key: string]: unknown } | null
): Promise<Page<Session>> {
const sessionsPage = await this._honcho[
'_client'
].workspaces.peers.sessions.list(this._honcho.workspaceId, this.id, {
filter,
})
async getSessions(filters?: Filters | null): Promise<Page<Session>> {
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
const sessionsPage = await this._client.workspaces.peers.sessions.list(
this.workspaceId,
this.id,
{
filters: validatedFilter,
}
)
return new Page(
sessionsPage,
(session: any) => new Session(session.id, this._honcho)
(session) => new Session(session.id, this.workspaceId, this._client)
)
}
/**
* Create a message attributed to this peer.
* Create a message object attributed to this peer.
*
* This is a convenience method for creating message objects with this peer's ID.
* The created message object can then be added to sessions or used in other operations.
*
* @param content - The text content for the message
* @param metadata - Optional metadata to associate with the message
* @returns A new message object with this peer's ID and the provided content
*/
message(content: string, opts?: { metadata?: Record<string, unknown> }): any {
message(
content: string,
options?: { metadata?: Record<string, unknown> }
): ValidatedMessageCreate {
const validatedContent = MessageContentSchema.parse(content)
const validatedMetadata = options?.metadata
? MessageMetadataSchema.parse(options.metadata)
: undefined
return {
peerId: this.id,
content,
metadata: opts?.metadata,
peer_id: this.id,
content: validatedContent,
metadata: validatedMetadata,
}
}
/**
* Get the current metadata for this peer.
*
* Makes an API call to retrieve metadata associated with this peer. Metadata
* can include custom attributes, settings, or any other key-value data
* associated with the peer.
*
* @returns Promise resolving to a dictionary containing the peer's metadata.
* Returns an empty dictionary if no metadata is set
*/
async getMetadata(): Promise<Record<string, unknown>> {
const peer = await this._honcho['_client'].workspaces.peers.getOrCreate(
this._honcho.workspaceId,
const peer = await this._client.workspaces.peers.getOrCreate(
this.workspaceId,
{ id: this.id }
)
return peer.metadata || {}
@ -99,13 +169,50 @@ export class Peer {
/**
* Set the metadata for this peer.
*
* Makes an API call to update the metadata associated with this peer.
* This will overwrite any existing metadata with the provided values.
*
* @param metadata - A dictionary of metadata to associate with this peer.
* Keys must be strings, values can be any JSON-serializable type
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await this._honcho['_client'].workspaces.peers.update(
this._honcho.workspaceId,
this.id,
{ metadata }
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
metadata,
})
}
/**
* Get the current workspace-level configuration for this peer.
*
* Makes an API call to retrieve configuration associated with this peer.
* Configuration currently includes one optional flag, `observe_me`.
*
* @returns Promise resolving to a dictionary containing the peer's configuration
*/
async getPeerConfig(): Promise<Record<string, unknown>> {
const peer = await this._client.workspaces.peers.getOrCreate(
this.workspaceId,
{ id: this.id }
)
return peer.configuration || {}
}
/**
* Set the configuration for this peer. Currently the only supported config
* value is the `observe_me` flag, which controls whether derivation tasks
* should be created for this peer's global representation. Default is True.
*
* Makes an API call to update the configuration associated with this peer.
* This will overwrite any existing configuration with the provided values.
*
* @param config - A dictionary of configuration to associate with this peer.
* Keys must be strings, values can be any JSON-serializable type
*/
async setPeerConfig(config: Record<string, unknown>): Promise<void> {
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
configuration: config,
})
}
/**
@ -114,54 +221,39 @@ export class Peer {
* Makes an API call to search endpoint.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
* @param filters - Optional filters to scope the search. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @param limit - Optional limit on the number of results to return.
* @returns Promise resolving to an array of Message objects representing the search results.
* Returns an empty array if no messages are found.
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string')
}
const messagesPage = await this._honcho['_client'].workspaces.peers.search(
this._honcho.workspaceId,
async search(
query: string,
options?: { filters?: Filters; limit?: number }
): Promise<Message[]> {
const validatedQuery = SearchQuerySchema.parse(query)
const validatedFilters = options?.filters
? FilterSchema.parse(options.filters)
: undefined
const validatedLimit = options?.limit
? LimitSchema.parse(options.limit)
: undefined
return await this._client.workspaces.peers.search(
this.workspaceId,
this.id,
{ query: query }
{
query: validatedQuery,
filters: validatedFilters,
limit: validatedLimit,
}
)
return new Page(messagesPage)
}
/**
* Upload a file to create messages in this peer's global representation.
* Return a string representation of the Peer.
*
* Makes an API call to upload a file and convert it into messages. The file is
* processed to extract text content, split into appropriately sized chunks,
* and created as messages attributed to this peer.
*
* @param file File to upload. Should be an object with filename, content (as Buffer or Uint8Array), and content_type
* @returns A list of Message objects representing the created messages
*
* @note Supported file types include PDFs, text files, and JSON documents.
* Large files will be automatically split into multiple messages to fit
* within message size limits.
* @returns A string representation suitable for debugging
*/
async uploadFile(file: {
filename: string
content: Buffer | Uint8Array
content_type: string
}): Promise<any[]> {
// Convert file to the format expected by the API
const fileData = {
filename: file.filename,
content: file.content,
content_type: file.content_type,
}
// Call the upload endpoint
const response = await (
this._honcho['_client'] as any
).workspaces.peers.messages.upload(this._honcho.workspaceId, this.id, {
file: fileData,
})
return response
toString(): string {
return `Peer(id='${this.id}')`
}
}

View File

@ -1,79 +1,186 @@
import type { Honcho } from './client'
import type HonchoCore from '@honcho-ai/core'
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
import type { Uploadable } from '@honcho-ai/core/src/uploads'
import { Page } from './pagination'
import { Peer } from './peer'
import { SessionContext } from './session_context'
import {
ContextParamsSchema,
FileUploadSchema,
FilterSchema,
type Filters,
LimitSchema,
type MessageAddition,
MessageAdditionSchema,
type PeerAddition,
PeerAdditionSchema,
type PeerRemoval,
PeerRemovalSchema,
SearchQuerySchema,
SessionPeerConfigSchema,
WorkingRepParamsSchema,
} from './validation'
/**
* Configuration options for a peer within a specific session.
*
* Controls how peers interact and observe each other within the context
* of a particular session, allowing for fine-grained control over
* representation building and theory-of-mind behaviors.
*/
export class SessionPeerConfig {
observe_others: boolean
observe_me: boolean
/**
* Whether other peers in this session should try to form a session-level
* theory-of-mind representation of this peer. When false, prevents other
* peers from building local representations of this peer within this session.
*/
observe_me?: boolean | null
constructor(opts?: { observe_others?: boolean; observe_me?: boolean }) {
this.observe_others = opts?.observe_others ?? false
this.observe_me = opts?.observe_me ?? true
/**
* Whether this peer should form session-level theory-of-mind representations
* of other peers in the session. When false, this peer will not build local
* representations of other peers within this session.
*/
observe_others?: boolean
/**
* Initialize SessionPeerConfig with observation settings.
*
* @param observe_me - Whether other peers should observe this peer in the session
* @param observe_others - Whether this peer should observe others in the session
*/
constructor(observe_me?: boolean | null, observe_others?: boolean) {
const validatedConfig = SessionPeerConfigSchema.parse({
observe_me,
observe_others,
})
this.observe_me = validatedConfig.observe_me
this.observe_others = validatedConfig.observe_others
}
}
/**
* Represents a session in Honcho.
* Represents a session in the Honcho system.
*
* Sessions are scoped to a set of peers and contain messages/content. They create
* bidirectional relationships between peers and provide a context for multi-party
* conversations and interactions. Sessions serve as containers for conversations,
* allowing peers to communicate while maintaining both global and local
* representations of each other.
*
* Key features:
* - Multi-peer conversations with configurable observation settings
* - Message storage and retrieval with filtering capabilities
* - Context optimization for token-limited scenarios
* - File upload support with automatic message creation
* - Session-scoped peer representations and theory-of-mind modeling
* - Search functionality across session messages
*
* @example
* ```typescript
* const session = await honcho.session('conversation-123')
*
* // Add peers to the session
* await session.addPeers(['user1', 'assistant1'])
*
* // Send messages
* await session.addMessages([
* { peer_id: 'user1', content: 'Hello!' },
* { peer_id: 'assistant1', content: 'Hi there!' }
* ])
*
* // Get optimized context
* const context = await session.getContext(true, 4000)
* ```
*/
export class Session {
/**
* Unique identifier for this session.
*/
readonly id: string
private _honcho: Honcho
/**
* Workspace ID for scoping operations.
*/
readonly workspaceId: string
/**
* Reference to the parent Honcho client instance.
*/
private _client: HonchoCore
/**
* Initialize a new Session.
* Initialize a new Session. **Do not call this directly, use the client.session() method instead.**
*
* @param id - Unique identifier for this session within the workspace
* @param workspaceId - Workspace ID for scoping operations
* @param client - Reference to the parent Honcho client instance
*/
constructor(id: string, honcho: Honcho, config?: Record<string, unknown>) {
constructor(id: string, workspaceId: string, client: HonchoCore) {
this.id = id
this._honcho = honcho
if (config) {
this._honcho['_client'].workspaces.sessions.getOrCreate(
this._honcho.workspaceId,
{ id: this.id, configuration: config }
)
}
this.workspaceId = workspaceId
this._client = client
}
/**
* Add peers to this session.
*
* Makes an API call to add one or more peers to this session. Adding peers
* creates bidirectional relationships and allows them to participate in
* the session's conversations. Peers can be added with optional session-specific
* configuration to control observation behaviors.
*
* @param peers - Peers to add to the session. Can be:
* - string: Single peer ID
* - Peer: Single Peer object
* - Array<string | Peer>: List of peer IDs and/or Peer objects
* - [string | Peer, SessionPeerConfig]: Single peer with session config
* - Array<string | Peer | [string | Peer, SessionPeerConfig]>: Mixed list
* of peers and peer+config combinations
*
* @example
* ```typescript
* // Add single peer
* await session.addPeers('user123')
*
* // Add multiple peers
* await session.addPeers(['user1', 'user2', peer3])
*
* // Add peer with custom config
* await session.addPeers(['user1', new SessionPeerConfig(false, true)])
*
* // Add mixed peers with and without configs
* await session.addPeers([
* 'user1',
* ['user2', new SessionPeerConfig(true, false)],
* peer3
* ])
* ```
*/
async addPeers(
peers:
| string
| Peer
| Array<string | Peer>
| [string | Peer, SessionPeerConfig]
| Array<[string | Peer, SessionPeerConfig]>
| Array<string | Peer | [string | Peer, SessionPeerConfig]>
): Promise<void> {
async addPeers(peers: PeerAddition): Promise<void> {
const validatedPeers = PeerAdditionSchema.parse(peers)
const peerDict: Record<string, SessionPeerConfig> = {}
const peersArray = Array.isArray(peers) ? peers : [peers]
const peersArray = Array.isArray(validatedPeers)
? validatedPeers
: [validatedPeers]
for (const peer of peersArray) {
if (typeof peer === 'string') {
peerDict[peer] = { observe_others: false, observe_me: true }
} else if (typeof peer === 'object' && 'id' in peer) {
peerDict[peer.id] = { observe_others: false, observe_me: true }
// Handle string peer ID
peerDict[peer] = {}
} else if (Array.isArray(peer)) {
// Handle tuple [string | Peer, SessionPeerConfig]
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id
peerDict[peerId] = peer[1]
} else if (
typeof peer === 'object' &&
'id' in peer &&
'observe_others' in peer &&
'observe_me' in peer
) {
peerDict[(peer as any).id] = {
observe_others: (peer as any).observe_others,
observe_me: (peer as any).observe_me,
}
} else if (typeof peer === 'object' && 'id' in peer) {
// Handle Peer object
peerDict[peer.id] = {}
} else {
// This should never happen with proper typing, but handle gracefully
throw new Error(`Invalid peer type: ${typeof peer}`)
}
}
await (this._honcho['_client'] as any).workspaces.sessions.peers.add(
this._honcho.workspaceId,
await this._client.workspaces.sessions.peers.add(
this.workspaceId,
this.id,
peerDict
)
@ -81,40 +188,45 @@ export class Session {
/**
* Set the complete peer list for this session.
*
* Makes an API call to replace the current peer list with the provided peers.
* This will remove any peers not in the new list and add any that are missing.
* Unlike addPeers(), this method overwrites the entire peer membership.
*
* @param peers - Peers to set for the session. Can be:
* - string: Single peer ID
* - Peer: Single Peer object
* - Array<string | Peer>: List of peer IDs and/or Peer objects
* - [string | Peer, SessionPeerConfig]: Single peer with session config
* - Array<string | Peer | [string | Peer, SessionPeerConfig]>: Mixed list
* of peers and peer+config combinations
*/
async setPeers(
peers:
| string
| Peer
| Array<string | Peer>
| [string | Peer, SessionPeerConfig]
| Array<[string | Peer, SessionPeerConfig]>
| Array<string | Peer | [string | Peer, SessionPeerConfig]>
): Promise<void> {
async setPeers(peers: PeerAddition): Promise<void> {
const validatedPeers = PeerAdditionSchema.parse(peers)
const peerDict: Record<string, SessionPeerConfig> = {}
const peersArray = Array.isArray(peers) ? peers : [peers]
const peersArray = Array.isArray(validatedPeers)
? validatedPeers
: [validatedPeers]
for (const peer of peersArray) {
if (typeof peer === 'string') {
peerDict[peer] = { observe_others: false, observe_me: true }
} else if (typeof peer === 'object' && 'id' in peer) {
peerDict[peer.id] = { observe_others: false, observe_me: true }
// Handle string peer ID
peerDict[peer] = {}
} else if (Array.isArray(peer)) {
// Handle tuple [string | Peer, SessionPeerConfig]
const peerId = typeof peer[0] === 'string' ? peer[0] : peer[0].id
peerDict[peerId] = peer[1]
} else if (
typeof peer === 'object' &&
'id' in peer &&
'observe_others' in peer &&
'observe_me' in peer
) {
peerDict[(peer as any).id] = {
observe_others: (peer as any).observe_others,
observe_me: (peer as any).observe_me,
}
} else if (typeof peer === 'object' && 'id' in peer) {
// Handle Peer object
peerDict[peer.id] = {}
} else {
// This should never happen with proper typing, but handle gracefully
throw new Error(`Invalid peer type: ${typeof peer}`)
}
}
await (this._honcho['_client'] as any).workspaces.sessions.peers.set(
this._honcho.workspaceId,
await this._client.workspaces.sessions.peers.set(
this.workspaceId,
this.id,
peerDict
)
@ -122,40 +234,65 @@ export class Session {
/**
* Remove peers from this session.
*
* Makes an API call to remove one or more peers from this session.
* Removed peers will no longer be able to participate in the session
* unless added back. Their existing messages remain in the session.
*
* @param peers - Peers to remove from the session. Can be:
* - string: Single peer ID
* - Peer: Single Peer object
* - Array<string | Peer>: List of peer IDs and/or Peer objects
*/
async removePeers(
peers: string | Peer | Array<string | Peer>
): Promise<void> {
const peerIds = Array.isArray(peers)
? peers.map((p) => (typeof p === 'string' ? p : p.id))
: [typeof peers === 'string' ? peers : peers.id]
await (this._honcho['_client'] as any).workspaces.sessions.peers.remove(
this._honcho.workspaceId,
async removePeers(peers: PeerRemoval): Promise<void> {
const validatedPeers = PeerRemovalSchema.parse(peers)
const peerIds = Array.isArray(validatedPeers)
? validatedPeers.map((p) => (typeof p === 'string' ? p : p.id))
: [
typeof validatedPeers === 'string'
? validatedPeers
: validatedPeers.id,
]
await this._client.workspaces.sessions.peers.remove(
this.workspaceId,
this.id,
peerIds
)
}
/**
* Get all peers in this session. Automatically converts the paginated response
* into a list for us -- the max number of peers in a session is usually 10.
* Get all peers in this session.
*
* Makes an API call to retrieve the list of peers that are currently
* members of this session. Automatically converts the paginated response
* into a list for convenience -- the max number of peers in a session is usually 10.
*
* @returns Promise resolving to a list of Peer objects that are members of this session
*/
async getPeers(): Promise<Peer[]> {
const peersPage = await (
this._honcho['_client'] as any
).workspaces.sessions.peers.list(this._honcho.workspaceId, this.id)
return peersPage.items.map((peer: any) => new Peer(peer.id, this._honcho))
const peersPage = await this._client.workspaces.sessions.peers.list(
this.workspaceId,
this.id
)
return peersPage.items.map(
(peer) => new Peer(peer.id, this.workspaceId, this._client)
)
}
/**
* Get the configuration for a peer in this session.
*
* Makes an API call to retrieve the session-specific configuration for a peer.
* This includes observation settings that control how this peer interacts
* with other peers within this session context.
*
* @param peer - The peer to get configuration for. Can be peer ID string or Peer object
* @returns Promise resolving to SessionPeerConfig object with the peer's session settings
*/
async getPeerConfig(peer: string | Peer): Promise<SessionPeerConfig> {
const peerId = typeof peer === 'string' ? peer : peer.id
return await (
this._honcho['_client'] as any
).workspaces.sessions.peers.getConfig(
this._honcho.workspaceId,
return await this._client.workspaces.sessions.peers.getConfig(
this.workspaceId,
this.id,
peerId
)
@ -163,92 +300,164 @@ export class Session {
/**
* Set the configuration for a peer in this session.
*
* Makes an API call to update the session-specific configuration for a peer.
* This controls observation behaviors and theory-of-mind formation within
* this session context.
*
* @param peer - The peer to configure. Can be peer ID string or Peer object
* @param config - SessionPeerConfig object specifying the observation settings
*/
async setPeerConfig(
peer: string | Peer,
config: SessionPeerConfig
): Promise<void> {
const peerId = typeof peer === 'string' ? peer : peer.id
await (this._honcho['_client'] as any).workspaces.sessions.peers.setConfig(
this._honcho.workspaceId,
const validatedConfig = SessionPeerConfigSchema.parse(config)
await this._client.workspaces.sessions.peers.setConfig(
this.workspaceId,
this.id,
peerId,
{
observe_others: config.observe_others,
observe_me: config.observe_me,
observe_others: validatedConfig.observe_others,
observe_me: validatedConfig.observe_me,
}
)
}
/**
* Add one or more messages to this session.
*
* Makes an API call to store messages in this session. Any message added
* to a session will automatically add the creating peer to the session
* if they are not already a member. Messages are the primary way content
* flows through the Honcho system.
*
* @param messages - Messages to add to the session. Can be:
* - MessageCreate: Single message object with peer_id and content
* - MessageCreate[]: Array of message objects
*
* @example
* ```typescript
* // Add single message
* await session.addMessages({
* peer_id: 'user123',
* content: 'Hello world!'
* })
*
* // Add multiple messages
* await session.addMessages([
* { peer_id: 'user1', content: 'Hello!' },
* { peer_id: 'assistant', content: 'Hi there!' }
* ])
* ```
*/
async addMessages(messages: any | any[]): Promise<void> {
const msgs = Array.isArray(messages) ? messages : [messages]
await (this._honcho['_client'] as any).workspaces.sessions.messages.create(
this._honcho.workspaceId,
async addMessages(messages: MessageAddition): Promise<void> {
const validatedMessages = MessageAdditionSchema.parse(messages)
const messagesList = Array.isArray(validatedMessages)
? validatedMessages
: [validatedMessages]
await this._client.workspaces.sessions.messages.create(
this.workspaceId,
this.id,
{
messages: msgs.map((msg) => ({
peer_id: msg.peerId,
content: msg.content,
metadata: msg.metadata,
})),
messages: messagesList,
}
)
}
/**
* Get messages from this session with optional filtering.
*
* Makes an API call to retrieve messages from this session. Results can be
* filtered based on various criteria and are returned in a paginated format.
* Messages are ordered by creation time (most recent first by default).
*
* @param filters - Optional filter criteria for messages. See [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @returns Promise resolving to a Page of Message objects matching the specified criteria
*/
async getMessages(opts?: {
filter?: Record<string, unknown>
}): Promise<Page<any>> {
const messagesPage = await (
this._honcho['_client'] as any
).workspaces.sessions.messages.list(
this._honcho.workspaceId,
async getMessages(filters?: Filters): Promise<Page<Message>> {
const validatedFilter = filters ? FilterSchema.parse(filters) : undefined
const messagesPage = await this._client.workspaces.sessions.messages.list(
this.workspaceId,
this.id,
opts?.filter
validatedFilter
)
return new Page(messagesPage)
}
/**
* Get metadata for this session.
*
* Makes an API call to retrieve the current metadata associated with this session.
* Metadata can include custom attributes, settings, or any other key-value data
* that provides context about the session.
*
* @returns Promise resolving to a dictionary containing the session's metadata.
* Returns an empty dictionary if no metadata is set
*/
async getMetadata(): Promise<Record<string, unknown>> {
const session = await (
this._honcho['_client'] as any
).workspaces.sessions.getOrCreate(this._honcho.workspaceId, { id: this.id })
const session = await this._client.workspaces.sessions.getOrCreate(
this.workspaceId,
{ id: this.id }
)
return session.metadata || {}
}
/**
* Set metadata for this session.
*
* Makes an API call to update the metadata associated with this session.
* This will overwrite any existing metadata with the provided values.
* Metadata is useful for storing custom attributes, configuration, or
* contextual information about the session.
*
* @param metadata - A dictionary of metadata to associate with this session.
* Keys must be strings, values can be any JSON-serializable type
*/
async setMetadata(metadata: Record<string, unknown>): Promise<void> {
await (this._honcho['_client'] as any).workspaces.sessions.update(
this._honcho.workspaceId,
this.id,
{ metadata }
)
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
metadata,
})
}
/**
* Get optimized context for this session within a token limit.
*
* Makes an API call to retrieve a curated list of messages that provides
* optimal context for the conversation while staying within the specified
* token limit. Uses tiktoken for token counting, so results should be
* compatible with OpenAI models. The context optimization balances
* recency and relevance to provide the best conversational context.
*
* @param summary - Whether to include summary information in the context.
* When true, includes session summary if available. Defaults to true
* @param tokens - Maximum number of tokens to include in the context. If not provided,
* uses the server's default configuration
* @returns Promise resolving to a SessionContext object containing the optimized
* message history and summary (if available) that maximizes conversational
* context while respecting the token limit
*
* @note Token counting is performed using tiktoken. For models using different
* tokenizers, you may need to adjust the token limit accordingly.
*/
async getContext(opts?: {
async getContext(options?: {
summary?: boolean
tokens?: number
}): Promise<SessionContext> {
const context = await (
this._honcho['_client'] as any
).workspaces.sessions.getContext(this._honcho.workspaceId, this.id, {
tokens: opts?.tokens,
summary: opts?.summary,
const contextParams = ContextParamsSchema.parse({
summary: options?.summary,
tokens: options?.tokens,
})
return new SessionContext(this.id, context.messages, context.summary || '')
const context = await this._client.workspaces.sessions.getContext(
this.workspaceId,
this.id,
{
tokens: contextParams.tokens,
summary: contextParams.summary,
}
)
return new SessionContext(this.id, context.messages, context.summary)
}
/**
@ -257,19 +466,34 @@ export class Session {
* Makes an API call to search for messages in this session.
*
* @param query The search query to use
* @returns A Page of Message objects representing the search results.
* Returns an empty page if no messages are found.
* @param filters - Optional filters to scope the search: see [search filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
* @param limit Number of results to return (1-100, default: 10).
* @returns A list of Message objects representing the search results.
* Returns an empty list if no messages are found.
*/
async search(query: string): Promise<Page<any>> {
if (!query || typeof query !== 'string' || query.trim().length === 0) {
throw new Error('Search query must be a non-empty string')
async search(
query: string,
options?: {
filters?: Filters
limit?: number
}
const messagesPage = await (
this._honcho['_client'] as any
).workspaces.sessions.search(this._honcho.workspaceId, this.id, {
query: query,
})
return new Page(messagesPage)
): Promise<Message[]> {
const validatedQuery = SearchQuerySchema.parse(query)
const validatedFilters = options?.filters
? FilterSchema.parse(options.filters)
: undefined
const validatedLimit = options?.limit
? LimitSchema.parse(options.limit)
: undefined
return await this._client.workspaces.sessions.search(
this.workspaceId,
this.id,
{
query: validatedQuery,
filters: validatedFilters,
limit: validatedLimit,
}
)
}
/**
@ -277,65 +501,94 @@ export class Session {
*
* Makes an API call to upload a file and convert it into messages. The file is
* processed to extract text content, split into appropriately sized chunks,
* and created as messages attributed to this peer.
* and created as messages attributed to the specified peer. The peer will be
* automatically added to the session if not already a member.
*
* @param file File to upload. Should be an object with filename, content (as Buffer or Uint8Array), and content_type
* @param peerId The peer ID to attribute the messages to
* @returns A list of Message objects representing the created messages
* @param file - File to upload. Can be:
* - File objects (browser File API)
* - Buffer or Uint8Array with filename and content_type
* - { filename: string, content: Buffer | Uint8Array, content_type: string }
* @param peerId - The peer ID to attribute the created messages to
* @returns Promise resolving to a list of Message objects representing the created messages
*
* @note Supported file types include PDFs, text files, and JSON documents.
* Large files will be automatically split into multiple messages to fit
* within message size limits.
*
* @example
* ```typescript
* // Upload a file
* const messages = await session.uploadFile(fileInput.files[0], 'user123')
* console.log(`Created ${messages.length} messages from file`)
* ```
*/
async uploadFile(
file: {
filename: string
content: Buffer | Uint8Array
content_type: string
},
peerId: string
): Promise<any[]> {
// Convert file to the format expected by the API
const fileData = {
filename: file.filename,
content: file.content,
content_type: file.content_type,
}
// Call the upload endpoint
const response = await (
this._honcho['_client'] as any
).workspaces.sessions.messages.upload(this._honcho.workspaceId, this.id, {
file: fileData,
peer_id: peerId,
})
async uploadFile(file: Uploadable, peerId: string): Promise<Message[]> {
const uploadParams = FileUploadSchema.parse({ file, peerId })
const response = await this._client.workspaces.sessions.messages.upload(
this.workspaceId,
this.id,
{
file: uploadParams.file,
peer_id: uploadParams.peerId,
}
)
return response
}
/**
* Get the current working representation of the peer in this session.
* Get the current working representation of a peer in this session.
*
* @param peer The peer to get the working representation of.
* @param target The target peer to get the representation of. If provided, queries what `peer` knows about the `target`.
* @returns A dictionary containing information about the peer.
* Makes an API call to retrieve the session-scoped representation that has been
* built for a peer. This can be either the peer's global representation or
* their local representation of another peer (theory-of-mind).
*
* @param peer - The peer to get the working representation of. Can be peer ID string or Peer object
* @param target - Optional target peer. If provided, returns what `peer` knows about
* `target` within this session context rather than `peer`'s global representation
* @returns Promise resolving to a dictionary containing the peer's representation information,
* including facts, characteristics, and contextual knowledge
*
* @example
* ```typescript
* // Get peer's global representation in this session
* const globalRep = await session.workingRep('user123')
*
* // Get what user123 knows about assistant in this session
* const localRep = await session.workingRep('user123', 'assistant')
* ```
*/
async workingRep(
peer: string | Peer,
target?: string | Peer
): Promise<Record<string, unknown>> {
const peerId = typeof peer === 'string' ? peer : peer.id
const targetId = target
? typeof target === 'string'
? target
: target.id
const workingRepParams = WorkingRepParamsSchema.parse({ peer, target })
const peerId =
typeof workingRepParams.peer === 'string'
? workingRepParams.peer
: workingRepParams.peer.id
const targetId = workingRepParams.target
? typeof workingRepParams.target === 'string'
? workingRepParams.target
: workingRepParams.target.id
: undefined
return await (
this._honcho['_client'] as any
).workspaces.peers.workingRepresentation(this._honcho.workspaceId, peerId, {
session_id: this.id,
target: targetId,
})
return await this._client.workspaces.peers.workingRepresentation(
this.workspaceId,
peerId,
{
session_id: this.id,
target: targetId,
}
)
}
/**
* Return a string representation of the Session.
*
* @returns A string representation suitable for debugging
*/
toString(): string {
return `Session(id='${this.id}')`
}
}

View File

@ -1,3 +1,4 @@
import type { Message } from '@honcho-ai/core/src/resources/workspaces/sessions/messages'
import type { Peer } from './peer'
/**
@ -16,7 +17,7 @@ export class SessionContext {
/**
* List of Message objects representing the conversation context.
*/
readonly messages: any[]
readonly messages: Message[]
/**
* Summary of the session history prior to the message cutoff.
@ -30,7 +31,7 @@ export class SessionContext {
* @param messages List of Message objects to include in the context
* @param summary Summary of the session history prior to the message cutoff
*/
constructor(sessionId: string, messages: any[], summary: string = '') {
constructor(sessionId: string, messages: Message[], summary: string = '') {
this.sessionId = sessionId
this.messages = messages
this.summary = summary || ''
@ -39,8 +40,8 @@ export class SessionContext {
/**
* Convert the context to OpenAI-compatible message format.
*
* Transforms the message history into the format expected by OpenAI's
* Chat Completions API, with proper role assignments based on the
* Transforms the message history and summary into the format expected by
* OpenAI's Chat Completions API, with proper role assignments based on the
* assistant's identity.
*
* @param assistant The assistant peer (Peer object or peer ID string) to use
@ -49,47 +50,71 @@ export class SessionContext {
* @returns A list of dictionaries in OpenAI format, where each dictionary contains
* "role" and "content" keys suitable for the OpenAI API
*/
toOpenAI(assistant: string | Peer): Array<{ role: string; content: string }> {
toOpenAI(
assistant: string | Peer
): Array<{ role: string; content: string; name?: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
return this.messages.map((message) => ({
role: message.peer_name === assistantId ? 'assistant' : 'user',
const summaryMessage = {
role: 'system',
content: `<summary>${this.summary}</summary>`,
}
const messages = this.messages.map((message) => ({
role: message.peer_id === assistantId ? 'assistant' : 'user',
name: message.peer_id,
content: message.content,
}))
return this.summary ? [summaryMessage, ...messages] : messages
}
/**
* Convert the context to Anthropic-compatible message format.
*
* Transforms the message history into the format expected by Anthropic's
* Claude API.
* Claude API, with proper role assignments based on the assistant's identity.
*
* @param assistant The assistant peer (Peer object or peer ID string) to use
* for determining message roles. Messages from this peer will
* be marked as "assistant", others as "user"
* @returns A list of dictionaries in Anthropic format, where each dictionary contains
* "role" and "content" keys suitable for the Anthropic API
*
* Note:
* Future versions may implement role alternation requirements for
* Anthropic's API compatibility
*/
toAnthropic(
assistant: string | Peer
): Array<{ role: string; content: string }> {
const assistantId = typeof assistant === 'string' ? assistant : assistant.id
return this.messages.map((message) => ({
role: message.peer_name === assistantId ? 'assistant' : 'user',
content: message.content,
}))
const summaryMessage = {
role: 'user',
content: `<summary>${this.summary}</summary>`,
}
const messages = this.messages.map((message) =>
message.peer_id === assistantId
? {
role: 'assistant',
content: message.content,
}
: {
role: 'user',
content: `${message.peer_id}: ${message.content}`,
}
)
return this.summary ? [summaryMessage, ...messages] : messages
}
/**
* Return the number of messages in the context.
*/
get length(): number {
return this.messages.length
return this.messages.length + (this.summary.length > 0 ? 1 : 0)
}
/**
* Return a string representation of the SessionContext.
*/
toString(): string {
return `SessionContext(messages=${this.messages.length})`
return `SessionContext(messages=${this.messages.length}, summary=${this.summary})`
}
}

View File

@ -0,0 +1,261 @@
import { z } from 'zod'
/**
* Validation schemas for the Honcho TypeScript SDK.
*
* These schemas ensure type safety and runtime validation for all inputs
* to the SDK, providing clear error messages when validation fails.
*/
/**
* Schema for Honcho client configuration options.
*/
export const HonchoConfigSchema = z.object({
apiKey: z.string().optional(),
environment: z.enum(['local', 'production', 'demo']).optional(),
baseURL: z.string().url('Base URL must be a valid URL').optional(),
workspaceId: z
.string()
.min(1, 'Workspace ID must be a non-empty string')
.optional(),
timeout: z.number().positive('Timeout must be a positive number').optional(),
maxRetries: z
.number()
.int()
.min(0, 'Max retries must be a non-negative integer')
.optional(),
defaultHeaders: z.record(z.string(), z.string()).optional(),
defaultQuery: z.record(z.string(), z.unknown()).optional(),
})
/**
* Schema for peer metadata.
*/
export const PeerMetadataSchema = z.record(z.string(), z.unknown())
/**
* Schema for peer configuration.
*/
export const PeerConfigSchema = z.record(z.string(), z.unknown())
/**
* Schema for peer ID validation.
*/
export const PeerIdSchema = z
.string()
.min(1, 'Peer ID must be a non-empty string')
/**
* Schema for session metadata.
*/
export const SessionMetadataSchema = z.record(z.string(), z.unknown())
/**
* Schema for session configuration.
*/
export const SessionConfigSchema = z.record(z.string(), z.unknown())
/**
* Schema for session ID validation.
*/
export const SessionIdSchema = z
.string()
.min(1, 'Session ID must be a non-empty string')
/**
* Schema for session peer configuration.
*/
export const SessionPeerConfigSchema = z.object({
observe_me: z.boolean().nullable().optional(),
observe_others: z.boolean().optional(),
})
/**
* Schema for message content.
*/
export const MessageContentSchema = z
.string()
.refine(
(content: string) => content === '' || content.trim().length > 0,
'Message content cannot be only whitespace'
)
/**
* Schema for message metadata.
*/
export const MessageMetadataSchema = z
.record(z.string(), z.unknown())
.optional()
/**
* Schema for message creation.
*/
export const MessageCreateSchema = z.object({
peer_id: PeerIdSchema,
content: MessageContentSchema,
metadata: MessageMetadataSchema,
})
/**
* Schema for search query validation.
*/
export const SearchQuerySchema = z
.string()
.min(1, 'Search query must be a non-empty string')
.refine(
(query: string) => query.trim().length > 0,
'Search query cannot be only whitespace'
)
/**
* Schema for filter objects.
*/
export const FilterSchema = z.record(z.string(), z.unknown()).optional()
/**
* Schema for chat query parameters.
*/
export const ChatQuerySchema = z.object({
query: SearchQuerySchema,
stream: z.boolean().optional(),
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
sessionId: z.string().optional(),
})
/**
* Schema for context retrieval parameters.
*/
export const ContextParamsSchema = z.object({
summary: z.boolean().optional(),
tokens: z
.number()
.positive('Token limit must be a positive number')
.optional(),
})
/**
* Schema for deriver status options.
*/
export const DeriverStatusOptionsSchema = z.object({
observerId: z.string().optional(),
senderId: z.string().optional(),
sessionId: z.string().optional(),
timeoutMs: z
.number()
.positive('Timeout must be a positive number')
.optional(),
})
/**
* Schema for file upload parameters.
* Supports File objects (browser), Buffer, Uint8Array, and custom uploadable objects.
*/
export const FileUploadSchema = z.object({
file: z.union([
// Browser File object
z.instanceof(File),
// Node.js Buffer
z.instanceof(Buffer),
// Uint8Array
z.instanceof(Uint8Array),
// Custom uploadable object with filename, content, and content_type
z.object({
filename: z.string().min(1, 'Filename must be a non-empty string'),
content: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array)]),
content_type: z
.string()
.min(1, 'Content type must be a non-empty string'),
}),
// Fallback for any other uploadable type
z
.any()
.refine(
(val) => val !== null && val !== undefined,
'File must not be null or undefined'
),
]),
peerId: PeerIdSchema,
})
/**
* Schema for working representation parameters.
*/
export const WorkingRepParamsSchema = z.object({
peer: z.union([z.string(), z.object({ id: z.string() })]),
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
})
/**
* Schema for peer addition to session.
*/
export const PeerAdditionSchema = z.union([
z.string(),
z.object({ id: z.string() }),
z.array(
z.union([
z.string(),
z.object({ id: z.string() }),
z.tuple([
z.union([z.string(), z.object({ id: z.string() })]),
SessionPeerConfigSchema,
]),
])
),
z.tuple([
z.union([z.string(), z.object({ id: z.string() })]),
SessionPeerConfigSchema,
]),
])
/**
* Schema for peer removal from session.
*/
export const PeerRemovalSchema = z.union([
z.string(),
z.object({ id: z.string() }),
z.array(z.union([z.string(), z.object({ id: z.string() })])),
])
/**
* Schema for message addition to session.
*/
export const MessageAdditionSchema = z.union([
MessageCreateSchema,
z.array(MessageCreateSchema),
])
/**
* Schema for workspace metadata.
*/
export const WorkspaceMetadataSchema = z.record(z.string(), z.unknown())
/**
* Schema for limit.
*/
export const LimitSchema = z
.number()
.int()
.min(1, 'Limit must be a positive integer')
.max(100, 'Limit must be less than or equal to 100')
/**
* Type exports for use throughout the SDK.
*/
export type HonchoConfig = z.infer<typeof HonchoConfigSchema>
export type PeerMetadata = z.infer<typeof PeerMetadataSchema>
export type PeerConfig = z.infer<typeof PeerConfigSchema>
export type SessionMetadata = z.infer<typeof SessionMetadataSchema>
export type SessionConfig = z.infer<typeof SessionConfigSchema>
export type SessionPeerConfig = z.infer<typeof SessionPeerConfigSchema>
export type MessageCreate = z.infer<typeof MessageCreateSchema>
export type Filters = z.infer<typeof FilterSchema>
export type ChatQuery = z.infer<typeof ChatQuerySchema>
export type ContextParams = z.infer<typeof ContextParamsSchema>
export type DeriverStatusOptions = z.infer<typeof DeriverStatusOptionsSchema>
export type FileUpload = z.infer<typeof FileUploadSchema>
export type WorkingRepParams = z.infer<typeof WorkingRepParamsSchema>
export type PeerAddition = z.infer<typeof PeerAdditionSchema>
export type PeerRemoval = z.infer<typeof PeerRemovalSchema>
export type MessageAddition = z.infer<typeof MessageAdditionSchema>
export type WorkspaceMetadata = z.infer<typeof WorkspaceMetadataSchema>
export type Limit = z.infer<typeof LimitSchema>

View File

@ -15,11 +15,11 @@
]
},
"include": [
"src/**/*",
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"baseUrl": ".",
"baseUrl": "."
}

View File

@ -193,10 +193,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
@ -244,8 +240,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)] = (

View File

@ -7,7 +7,6 @@ from .message import (
get_message_seq_in_session,
get_messages,
get_messages_id_range,
search,
update_message,
)
from .peer import (
@ -56,7 +55,6 @@ __all__ = [
"get_message",
"get_message_seq_in_session",
"update_message",
"search",
# Peer
"get_or_create_peers",
"get_peer",

View File

@ -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

View File

@ -115,7 +115,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",

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -30,7 +30,7 @@ class WorkspaceCreate(WorkspaceBase):
class WorkspaceGet(WorkspaceBase):
filter: dict[str, Any] | None = None
filters: dict[str, Any] | None = None
class WorkspaceUpdate(WorkspaceBase):
@ -67,7 +67,7 @@ class PeerCreate(PeerBase):
class PeerGet(PeerBase):
filter: dict[str, Any] | None = None
filters: dict[str, Any] | None = None
class PeerUpdate(PeerBase):
@ -131,7 +131,7 @@ class MessageCreate(MessageBase):
class MessageGet(MessageBase):
filter: dict[str, Any] | None = None
filters: dict[str, Any] | None = None
class MessageUpdate(MessageBase):
@ -197,7 +197,7 @@ class SessionCreate(SessionBase):
class SessionGet(SessionBase):
filter: dict[str, Any] | None = None
filters: dict[str, Any] | None = None
class SessionUpdate(SessionBase):
@ -246,9 +246,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",
)

View File

@ -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:

View File

@ -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(

220
src/utils/search.py Normal file
View File

@ -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

View File

@ -62,10 +62,11 @@ class TestSessionCRUD:
assert len(configurations) == 2
# Verify the structure of returned data
for peer_name, peer_config, session_peer_config in configurations:
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]

View File

@ -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

View File

@ -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

View File

@ -271,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()
@ -304,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()
@ -377,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()
@ -432,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()
@ -444,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"}},
@ -461,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"}},

View File

@ -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,22 +479,22 @@ 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
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
def test_get_peers_with_complex_filter(

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

644
uv.lock
View File

@ -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]]
@ -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" },
@ -891,8 +891,8 @@ requires-dist = [
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.2.2" },
{ 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" },
@ -919,7 +919,7 @@ wheels = [
[[package]]
name = "honcho-core"
version = "1.2.0"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -929,9 +929,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 +1250,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 +1258,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 +1299,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 +1463,7 @@ wheels = [
[[package]]
name = "openai"
version = "1.97.1"
version = "1.99.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -1477,9 +1475,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 +1728,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 +2048,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 +2275,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 +2342,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 +2430,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 +2637,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]]