Merge branch 'main' into vince/dev-1259
This commit is contained in:
commit
f0d24af827
|
|
@ -101,6 +101,8 @@ jobs:
|
|||
SENTRY_ENABLED: false
|
||||
LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
LLM_OPENAI_COMPATIBLE_API_KEY: test-key
|
||||
LLM_OPENAI_COMPATIBLE_BASE_URL: http://localhost:8000
|
||||
DERIVER_PROVIDER: openai
|
||||
DERIVER_MODEL: test
|
||||
DIALECTIC_PROVIDER: openai
|
||||
|
|
|
|||
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -5,6 +5,41 @@ 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.5.0] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- Message level configurations
|
||||
- CRUD operations for observations
|
||||
- Comprehensive test cases for harness
|
||||
- Peer level get_context
|
||||
- Set Peer Card Method
|
||||
- Manual dreaming trigger endpoint
|
||||
|
||||
### Changed
|
||||
|
||||
- Configurations to support more flags for fine-grained control of the deriver, peer cards, summaries, etc.
|
||||
- Working Representations to support more fine-grained parameters
|
||||
|
||||
### Fixed
|
||||
|
||||
- File uploads to match `MessageCreate` structure
|
||||
- Cache invalidation strategy
|
||||
|
||||
## [2.4.3] - 2025-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Redis caching to improve DB IO
|
||||
- Backup LLM provider to avoid failures when a provider is down
|
||||
|
||||
### Changed
|
||||
|
||||
- QueueItems to use standardized columns
|
||||
- Improved Deduplication logic for Representation Tasks
|
||||
- More finegrained metrics for representation, summary, and peer card tasks
|
||||
- DB constraint to follow standard naming conventions
|
||||
|
||||
## [2.4.2] - 2025-11-03
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## What is Honcho?
|
||||
|
||||
Honcho is an infrastructure layer for building AI agents with social cognition and theory of mind capabilities. Its primary purposes include:
|
||||
Honcho is an infrastructure layer for building AI agents with memory and social cognition. Its primary purposes include:
|
||||
|
||||
- Imbuing agents with a sense of identity
|
||||
- Personalizing user experiences through understanding user psychology
|
||||
|
|
@ -14,7 +14,7 @@ Honcho is an infrastructure layer for building AI agents with social cognition a
|
|||
- Supporting development of LLM-powered applications that adapt to end users
|
||||
- Enabling multi-peer sessions where multiple participants (users or agents) can interact
|
||||
|
||||
Honcho leverages the inherent theory-of-mind capabilities of LLMs to build coherent models of user psychology over time, enabling more personalized and effective AI interactions.
|
||||
Honcho leverages the inherent reasoning capabilities of LLMs to build coherent models of user psychology over time, enabling more personalized and effective AI interactions.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ Honcho uses a peer-based model where both users and agents are represented as "p
|
|||
- **Peer** (formerly User): Any participant in the system (human or AI)
|
||||
- **Session**: A conversation context that can involve multiple peers
|
||||
- **Message**: Data units that can represent communication between peers OR arbitrary data ingested by a peer to enhance its global representation
|
||||
- **Collections & Documents**: Internal vector storage for theory-of-mind representations (not exposed via API)
|
||||
- **Collections & Documents**: Internal vector storage for peer representations (not exposed via API)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
|
|||
|
||||
#### Dialectic API (`/peers/{peer_id}/chat`)
|
||||
|
||||
- Provides theory-of-mind informed responses
|
||||
- Provides bespoke responses informed by the representation
|
||||
- Integrates long-term facts from vector storage
|
||||
- Supports streaming responses
|
||||
- Configurable LLM providers
|
||||
|
|
@ -59,18 +59,11 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
|
|||
|
||||
1. Messages created via API (batch or single)
|
||||
2. Enqueued for background processing:
|
||||
- `representation`: Update peer's theory of mind
|
||||
- `representation`: Update peer's context
|
||||
- `summary`: Create session summaries
|
||||
3. Session-based queue processing ensures order
|
||||
4. Results stored internally in vector DB
|
||||
|
||||
#### Theory of Mind System
|
||||
|
||||
- Multiple implementation methods (conversational, single_prompt, long_term)
|
||||
- Facts extracted from messages and stored in collections
|
||||
- Representations combine short-term inference with long-term facts
|
||||
- Configurable via peer and session feature flags
|
||||
|
||||
### Configuration
|
||||
|
||||
- Hierarchical config: config.toml + environment variables
|
||||
|
|
@ -179,7 +172,6 @@ src/
|
|||
### Key Architectural Decisions
|
||||
|
||||
1. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings
|
||||
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**: JWTs can be scoped to workspace, peer, or session level
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
---
|
||||
|
||||

|
||||

|
||||
[](https://pypi.org/project/honcho-ai/)
|
||||
[](https://npmjs.org/package/@honcho-ai/sdk)
|
||||
[](https://discord.gg/plasticlabs)
|
||||
|
|
@ -429,7 +429,7 @@ Then modify the values as needed. The TOML file is organized into sections:
|
|||
- `[cache]` - Redis cache configuration
|
||||
- `[llm]` - LLM provider API keys and general settings
|
||||
- `[dialectic]` - Dialectic API configuration (provider, model, search settings)
|
||||
- `[deriver]` - Background worker settings and theory of mind configuration
|
||||
- `[deriver]` - Background worker settings and representation configuration
|
||||
- `[peer_card]` - Peer card generation settings
|
||||
- `[summary]` - Session summarization settings
|
||||
- `[dream]` - Dream processing configuration
|
||||
|
|
@ -501,8 +501,8 @@ Honcho uses a peer-based model where both users and agents are represented as "p
|
|||
|
||||
#### Key Features
|
||||
|
||||
- **Theory-of-Mind System**: Multiple implementation methods that extract facts from interactions and build comprehensive models of peer psychology
|
||||
- **Dialectic API**: Provides theory-of-mind informed responses that integrate long-term facts with current context
|
||||
- **Rich Reasoning System**: Multiple implementation methods that extract facts from interactions and build comprehensive models of peer psychology
|
||||
- **Dialectic API**: Provides reasoned informed responses that integrate long-term facts with current context
|
||||
- **Background Processing**: Asynchronous processing pipeline for expensive operations like representation updates and session summarization
|
||||
- **Multi-Provider Support**: Configurable LLM providers for different use cases
|
||||
|
||||
|
|
@ -567,7 +567,7 @@ The `Message` represents an atomic data unit that can exist at two levels:
|
|||
- **Session-level Messages**: Communication between peers within a session context
|
||||
|
||||
All messages are labeled by their source peer and can be processed
|
||||
asynchronously to update theory-of-mind models. This flexible design allows for
|
||||
asynchronously to update their representations. This flexible design allows for
|
||||
both conversational interactions and broader data ingestion for personality
|
||||
modeling.
|
||||
|
||||
|
|
@ -578,8 +578,7 @@ familiar with RAG based applications will be familiar with these. `Collections`
|
|||
store vector embedded data that developers and agents can retrieve against using
|
||||
functions like cosine similarity.
|
||||
|
||||
Collections are also used internally by Honcho while creating theory-of-mind
|
||||
representations of peers.
|
||||
Collections are also used internally by Honcho while creating representations of peers.
|
||||
|
||||
#### Documents
|
||||
|
||||
|
|
@ -596,7 +595,7 @@ A high level summary of the pipeline is as follows:
|
|||
|
||||
1. Messages are created via the API
|
||||
2. Derivation Tasks are enqueued for background processing including:
|
||||
- `representation`: To update theory-of-mind representations of `Peers`
|
||||
- `representation`: To update representations of `Peers`
|
||||
- `summary`: To create summaries of `Sessions`
|
||||
3. Session-based queue processing ensures proper ordering
|
||||
4. Results are stored internally
|
||||
|
|
@ -638,7 +637,7 @@ A developer's application can treat Honcho as an oracle to the `Peer` and
|
|||
consult it when necessary. Some examples of how to leverage the Dialectic
|
||||
API include:
|
||||
|
||||
- Asking Honcho for a theory-of-mind insight about the `Peer`
|
||||
- Asking Honcho for a generic or specific insight about the `Peer`
|
||||
- Asking Honcho to hydrate a prompt with data about the `Peer`s behavior
|
||||
- Asking Honcho for a 2nd opinion or approach about how to respond to the Peer
|
||||
- Getting personalized responses that incorporate long-term facts and context
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
## Version Compatibility
|
||||
|
||||
### Honcho API v2.4.2 (Current)
|
||||
### Honcho API v2.5.0 (Current)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="TypeScript SDK" icon="js">
|
||||
**Compatible Version:** v1.5.0
|
||||
**Compatible Version:** v1.6.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install @honcho-ai/sdk@1.5.0
|
||||
npm install @honcho-ai/sdk@1.6.0
|
||||
```
|
||||
</Card>
|
||||
<Card title="Python SDK" icon="python">
|
||||
**Compatible Version:** v1.5.0
|
||||
**Compatible Version:** v1.6.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
pip install honcho-ai==1.5.0
|
||||
pip install honcho-ai==1.6.0
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -34,7 +34,9 @@ This guide helps you understand which versions of Honcho's API are compatible wi
|
|||
|
||||
| Honcho API Version | TypeScript SDK | Python SDK |
|
||||
|-------------------|---------------|------------|
|
||||
| v2.4.2 (Current) | v1.5.0 | v1.5.0 |
|
||||
| v2.5.0 (Current) | v1.6.0 | v1.6.0 |
|
||||
| v2.4.3 | v1.5.0 | v1.5.0 |
|
||||
| v2.4.2 | v1.5.0 | v1.5.0 |
|
||||
| v2.4.1 | v1.5.0 | v1.5.0 |
|
||||
| v2.4.0 | v1.5.0 | v1.5.0 |
|
||||
| v2.3.3 | v1.4.1 | v1.4.1 |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,42 @@ 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.4.2 (Current)">
|
||||
<Update label="v2.5.0 (Current)">
|
||||
### Added
|
||||
|
||||
- Message level configurations
|
||||
- CRUD operations for observations
|
||||
- Comprehensive test cases for harness
|
||||
- Peer level get_context
|
||||
- Set Peer Card Method
|
||||
- Manual dreaming trigger endpoint
|
||||
|
||||
### Changed
|
||||
|
||||
- Configurations to support more flags for fine-grained control of the deriver, peer cards, summaries, etc.
|
||||
- Working Representations to support more fine-grained parameters
|
||||
|
||||
### Fixed
|
||||
|
||||
- File uploads to match `MessageCreate` structure
|
||||
- Cache invalidation strategy
|
||||
</Update>
|
||||
|
||||
<Update label="v2.4.3">
|
||||
### Added
|
||||
|
||||
- Redis caching to improve DB IO
|
||||
- Backup LLM provider to avoid failures when a provider is down
|
||||
|
||||
### Changed
|
||||
|
||||
- QueueItems to use standardized columns
|
||||
- Improved Deduplication logic for Representation Tasks
|
||||
- More finegrained metrics for representation, summary, and peer card tasks
|
||||
- DB constraint to follow standard naming conventions
|
||||
</Update>
|
||||
|
||||
<Update label="v2.4.2">
|
||||
### Fixed
|
||||
|
||||
- Langfuse tracing to have readable waterfalls
|
||||
|
|
@ -365,6 +400,19 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="Python SDK">
|
||||
[Python SDK](https://pypi.org/project/honcho-ai/)
|
||||
<Update label="v1.6.0 (Current)">
|
||||
### Added
|
||||
|
||||
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
|
||||
- Session Clone methods
|
||||
- Peer level get_context method
|
||||
- `ObservationScope` object to perform CRUD operations on observations
|
||||
- Representation object for WorkingRepresentations
|
||||
|
||||
### Changed
|
||||
|
||||
- methods that take IDs, can all optionally take an object of the same type
|
||||
</Update>
|
||||
<Update label="v1.5.0">
|
||||
### Added
|
||||
|
||||
|
|
@ -439,6 +487,19 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
|
|||
|
||||
<Tab title="TypeScript SDK">
|
||||
[TypeScript SDK](https://www.npmjs.com/package/@honcho-ai/sdk)
|
||||
<Update label="v1.6.0 (Current)">
|
||||
### Added
|
||||
|
||||
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
|
||||
- Session Clone methods
|
||||
- Peer level get_context method
|
||||
- `ObservationScope` object to perform CRUD operations on observations
|
||||
- Representation object for WorkingRepresentations
|
||||
|
||||
### Changed
|
||||
|
||||
- methods that take IDs, can all optionally take an object of the same type
|
||||
</Update>
|
||||
<Update label="v1.5.0">
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,21 @@
|
|||
},
|
||||
"favicon": "/favicon.svg",
|
||||
"contextual": {
|
||||
"options": ["copy", "view", "chatgpt", "claude"]
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude"
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"versions": [
|
||||
{
|
||||
"version": "v2.4.2",
|
||||
"version": "v2.5.0",
|
||||
"api": {
|
||||
"openapi": ["openapi.documented.yml"]
|
||||
"openapi": [
|
||||
"openapi.json"
|
||||
]
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
|
|
@ -125,7 +132,9 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": ["v2/api-reference/introduction"]
|
||||
"pages": [
|
||||
"v2/api-reference/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "workspaces",
|
||||
|
|
@ -135,7 +144,8 @@
|
|||
"v2/api-reference/endpoint/workspaces/update-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/delete-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/search-workspace",
|
||||
"v2/api-reference/endpoint/workspaces/get-deriver-status"
|
||||
"v2/api-reference/endpoint/workspaces/get-deriver-status",
|
||||
"v2/api-reference/endpoint/workspaces/trigger-dream"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -147,8 +157,10 @@
|
|||
"v2/api-reference/endpoint/peers/get-sessions-for-peer",
|
||||
"v2/api-reference/endpoint/peers/chat",
|
||||
"v2/api-reference/endpoint/peers/get-working-representation",
|
||||
"v2/api-reference/endpoint/peers/search-peer",
|
||||
"v2/api-reference/endpoint/peers/get-peer-card"
|
||||
"v2/api-reference/endpoint/peers/get-peer-card",
|
||||
"v2/api-reference/endpoint/peers/set-peer-card",
|
||||
"v2/api-reference/endpoint/peers/get-peer-context",
|
||||
"v2/api-reference/endpoint/peers/search-peer"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -180,6 +192,15 @@
|
|||
"v2/api-reference/endpoint/messages/create-messages-with-file"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "observations",
|
||||
"pages": [
|
||||
"v2/api-reference/endpoint/observations/create-observations",
|
||||
"v2/api-reference/endpoint/observations/list-observations",
|
||||
"v2/api-reference/endpoint/observations/query-observations",
|
||||
"v2/api-reference/endpoint/observations/delete-observation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "webhooks",
|
||||
"pages": [
|
||||
|
|
@ -215,7 +236,9 @@
|
|||
{
|
||||
"version": "v1.1.0",
|
||||
"api": {
|
||||
"openapi": ["openapi.json"]
|
||||
"openapi": [
|
||||
"openapi.json"
|
||||
]
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
|
|
@ -245,15 +268,23 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": ["v1/guides/overview", "v1/guides/streaming-response"]
|
||||
"pages": [
|
||||
"v1/guides/overview",
|
||||
"v1/guides/streaming-response"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Application Interfaces",
|
||||
"pages": ["v1/guides/discord", "v1/guides/honcho-mcp"]
|
||||
"pages": [
|
||||
"v1/guides/discord",
|
||||
"v1/guides/honcho-mcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Personal Memory",
|
||||
"pages": ["v1/guides/dialectic-endpoint"]
|
||||
"pages": [
|
||||
"v1/guides/dialectic-endpoint"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -262,7 +293,9 @@
|
|||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"pages": ["v1/api-reference/introduction"]
|
||||
"pages": [
|
||||
"v1/api-reference/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "apps",
|
||||
|
|
@ -310,7 +343,9 @@
|
|||
},
|
||||
{
|
||||
"group": "keys",
|
||||
"pages": ["v1/api-reference/endpoint/keys/create-key"]
|
||||
"pages": [
|
||||
"v1/api-reference/endpoint/keys/create-key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "metamessages",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"main": ".pnp.js",
|
||||
"scripts": {
|
||||
"dev": "mint dev",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.documented.yml -o api-reference/endpoint",
|
||||
"openapi": "npx @mintlify/scraping openapi-file v2/openapi.json -o v2/api-reference/endpoint",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: delete /v2/workspaces/{workspace_id}/observations/{observation_id}
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations/list
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/observations/query
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v2/workspaces/{workspace_id}/peers/{peer_id}/context
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: put /v2/workspaces/{workspace_id}/peers/{peer_id}/card
|
||||
---
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v2/workspaces/{workspace_id}/trigger_dream
|
||||
---
|
||||
|
|
@ -216,7 +216,7 @@ Honcho supports Redis caching to improve performance by caching frequently acces
|
|||
CACHE_ENABLED=false # Set to true to enable caching
|
||||
|
||||
# Redis connection
|
||||
CACHE_URL=redis://localhost:6379/0?suppress=false
|
||||
CACHE_URL=redis://localhost:6379/0?suppress=true
|
||||
|
||||
# Cache namespace and TTL
|
||||
CACHE_NAMESPACE=honcho # Prefix for all cache keys
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Honcho is a memory system that reasons. Read more on the approach [here](https:/
|
|||
|
||||
## Why Use Honcho?
|
||||
|
||||
Honcho streamlines the agent building process by offering elegant, flexible primitives for managing context. It also reasons over that context to give developers access to far richer insights only accessible through reasoning.
|
||||
Honcho streamlines the agent building process by offering elegant, flexible primitives for managing context. It also reasons over that context to give developers access to far richer insights only accessible through reasoning.
|
||||
|
||||
Take the following scenario:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,13 +34,12 @@ the Welcome page with integration guidance and links to documentation.
|
|||
</Frame>
|
||||
|
||||
Each organization has dedicated infrastructure running to isolate your
|
||||
workloads. Until you activate a subscription under the
|
||||
[Billing](https://app.honcho.dev/billing) page, the infrastructure will remain
|
||||
inactive.
|
||||
workloads. Once you add a valid payment method under the
|
||||
[Billing](https://app.honcho.dev/billing) page, your instance will turn on.
|
||||
|
||||
## 2. Activate your Honcho instance
|
||||
|
||||
Navigate to the [Billing](https://app.honcho.dev/billing) page to activate your subscription. Your Honcho instance provisions automatically, and you can monitor the deployment on the [Instance Status](https://app.honcho.dev/status) page until all systems show a green check mark.
|
||||
Navigate to the [Billing](https://app.honcho.dev/billing) page to add a payment method. Your Honcho instance provisions automatically, and you can monitor the deployment on the [Instance Status](https://app.honcho.dev/status) page until all systems show a green check mark.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/app-screenshots/status-page.png" alt="Instance Status Page" width="1200" height="800" loading="lazy" decoding="async" fetchpriority="low" />
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ response = alice.chat("What does the user know about weather?")
|
|||
response = alice.chat("What does the user know about the assistant?", target=assistant)
|
||||
|
||||
# Query scoped to a specific session
|
||||
response = alice.chat("What happened in our conversation?", session_id=session.id)
|
||||
response = alice.chat("What happened in our conversation?", session=session.id)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
@ -258,7 +258,7 @@ print(f"Workspace: {alice.workspace_id}")
|
|||
# Chat with peer's representations (supports streaming)
|
||||
response = alice.chat("What did I have for breakfast?")
|
||||
response = alice.chat("What do I know about Bob?", target="bob")
|
||||
response = alice.chat("What happened in session-1?", session_id="session-1")
|
||||
response = alice.chat("What happened in session-1?", session="session-1")
|
||||
|
||||
# Add content to a session with a peer
|
||||
session = honcho.session("session-1")
|
||||
|
|
@ -435,6 +435,148 @@ const bobSearch = await bobObs.query("work history");
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Peer Context
|
||||
|
||||
The `get_context()` method on peers retrieves both the working representation and peer card in a single API call:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get peer's own context
|
||||
context = alice.get_context()
|
||||
print(context.representation) # Working representation
|
||||
print(context.peer_card) # Peer card as list of strings
|
||||
|
||||
# Get context about another peer (what alice knows about bob)
|
||||
bob_context = alice.get_context(target="bob")
|
||||
|
||||
# Get context with semantic search
|
||||
context = alice.get_context(
|
||||
target="bob",
|
||||
search_query="work preferences",
|
||||
search_top_k=10,
|
||||
search_max_distance=0.8,
|
||||
include_most_derived=True,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get peer's own context
|
||||
const context = await alice.getContext();
|
||||
console.log(context.representation); // Working representation
|
||||
console.log(context.peerCard); // Peer card as array of strings
|
||||
|
||||
// Get context about another peer (what alice knows about bob)
|
||||
const bobContext = await alice.getContext("bob");
|
||||
|
||||
// Get context with semantic search
|
||||
const searchedContext = await alice.getContext("bob", {
|
||||
searchQuery: "work preferences",
|
||||
searchTopK: 10,
|
||||
searchMaxDistance: 0.8,
|
||||
includeMostDerived: true,
|
||||
maxObservations: 50
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Observations
|
||||
|
||||
Peers can access their observations (facts derived from messages) through the `observations` property and `observations_of()` method:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Access self-observations (what honcho knows about alice)
|
||||
self_obs = alice.observations
|
||||
|
||||
# List self-observations
|
||||
obs_list = self_obs.list()
|
||||
|
||||
# Search self-observations semantically
|
||||
results = self_obs.query("food preferences")
|
||||
|
||||
# Delete an observation
|
||||
self_obs.delete("observation-id")
|
||||
|
||||
# Access observations of another peer (what alice knows about bob)
|
||||
bob_obs = alice.observations_of("bob")
|
||||
bob_obs_list = bob_obs.list()
|
||||
bob_search = bob_obs.query("work history")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Access self-observations (what honcho knows about alice)
|
||||
const selfObs = alice.observations;
|
||||
|
||||
// List self-observations
|
||||
const obsList = await selfObs.list();
|
||||
|
||||
// Search self-observations semantically
|
||||
const results = await selfObs.query("food preferences");
|
||||
|
||||
// Delete an observation
|
||||
await selfObs.delete("observation-id");
|
||||
|
||||
// Access observations of another peer (what alice knows about bob)
|
||||
const bobObs = alice.observationsOf("bob");
|
||||
const bobObsList = await bobObs.list();
|
||||
const bobSearch = await bobObs.query("work history");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
#### Creating Observations Manually
|
||||
|
||||
You can also create observations directly, which is useful for importing data or adding explicit facts:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Create observations for what alice knows about bob
|
||||
bob_obs = alice.observations_of("bob")
|
||||
|
||||
# Create a single observation
|
||||
created = bob_obs.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session-1"}
|
||||
])
|
||||
|
||||
# Create multiple observations in batch
|
||||
created = bob_obs.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session-1"},
|
||||
{"content": "User works late at night", "session_id": "session-1"},
|
||||
{"content": "User enjoys programming", "session_id": "session-1"},
|
||||
])
|
||||
|
||||
# Returns list of created Observation objects with IDs
|
||||
for obs in created:
|
||||
print(f"Created observation: {obs.id} - {obs.content}")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Create observations for what alice knows about bob
|
||||
const bobObs = alice.observationsOf("bob");
|
||||
|
||||
// Create a single observation
|
||||
const created = await bobObs.create([
|
||||
{ content: "User prefers dark mode", sessionId: "session-1" }
|
||||
]);
|
||||
|
||||
// Create multiple observations in batch
|
||||
const batchCreated = await bobObs.create([
|
||||
{ content: "User prefers dark mode", sessionId: "session-1" },
|
||||
{ content: "User works late at night", sessionId: "session-1" },
|
||||
{ content: "User enjoys programming", sessionId: "session-1" },
|
||||
]);
|
||||
|
||||
// Returns array of created Observation objects with IDs
|
||||
for (const obs of batchCreated) {
|
||||
console.log(`Created observation: ${obs.id} - ${obs.content}`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
Manually created observations are marked as "explicit" and are treated the same as system-derived observations. Each observation must be tied to a session and the content length is validated against the embedding token limit.
|
||||
</Info>
|
||||
|
||||
### Session
|
||||
|
||||
Manages multi-party conversations:
|
||||
|
|
@ -504,11 +646,19 @@ searched_rep = session.working_rep(
|
|||
# Upload a file to create messages
|
||||
messages = session.upload_file(
|
||||
file=open("document.pdf", "rb"),
|
||||
peer_id="user",
|
||||
peer="user",
|
||||
metadata={"source": "upload"},
|
||||
created_at="2024-01-15T10:30:00Z"
|
||||
)
|
||||
|
||||
# Clone a session (creates a copy with all data)
|
||||
# Copies: messages, metadata, configuration, peers, and peer configurations
|
||||
cloned = session.clone()
|
||||
|
||||
# Clone up to a specific message (inclusive)
|
||||
# Only messages up to and including the specified message are copied
|
||||
cloned_partial = session.clone(message_id="msg-123")
|
||||
|
||||
# Delete session (async - returns 202)
|
||||
session.delete()
|
||||
|
||||
|
|
@ -581,6 +731,14 @@ const messages = await session.uploadFile(
|
|||
}
|
||||
);
|
||||
|
||||
// Clone a session (creates a copy with all data)
|
||||
// Copies: messages, metadata, configuration, peers, and peer configurations
|
||||
const cloned = await session.clone();
|
||||
|
||||
// Clone up to a specific message (inclusive)
|
||||
// Only messages up to and including the specified message are copied
|
||||
const clonedPartial = await session.clone("msg-123");
|
||||
|
||||
// Delete session (async - returns 202)
|
||||
await session.delete();
|
||||
|
||||
|
|
@ -713,7 +871,7 @@ group_chat.add_messages([
|
|||
|
||||
# Query different perspectives
|
||||
user_perspective = users[0].chat("What are people's concerns?")
|
||||
moderator_view = moderator.chat("What feedback am I getting?", session_id=group_chat.id)
|
||||
moderator_view = moderator.chat("What feedback am I getting?", session=group_chat.id)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
|
|
|
|||
|
|
@ -12,17 +12,26 @@ These guides provide concrete examples and implementation patterns for building
|
|||
Each guide focuses on a specific use case with practical examples. The goal is to get you from idea to working prototype as quickly as possible, then provide the depth you need to scale and customize.
|
||||
|
||||
|
||||
## Getting Started
|
||||
Quick integration guides to get up and running:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="MCP Integration" icon="link" href="/v2/integrations/mcp">
|
||||
Get Honcho running with a single prompt in Claude Code
|
||||
</Card>
|
||||
<Card title="LangGraph" icon="diagram-project" href="/v2/integrations/langgraph">
|
||||
Add persistent memory and theory of mind to your LangGraph agents
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Application Interfaces
|
||||
Ready-to-use integration patterns for popular platforms:
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Discord Bot" icon="discord" href="/v2/guides/discord">
|
||||
Build a Discord bot that remembers users across conversations
|
||||
</Card>
|
||||
<Card title="Telegram Bot" icon="telegram" href="/v2/guides/telegram">
|
||||
Create a Telegram bot with persistent user understanding
|
||||
</Card>
|
||||
<Card title="MCP" icon="link" href="/v2/guides/mcp">
|
||||
Get Honcho running with a single prompt in Cursor or Claude Code
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,296 @@
|
|||
---
|
||||
title: 'Migrating from Mem0'
|
||||
description: 'A guide to migrate from Mem0 to Honcho'
|
||||
icon: 'arrow-right-arrow-left'
|
||||
---
|
||||
|
||||
Interested in transferring your data from Mem0 to Honcho? This guide covers why to switch, how to migrate your data, and differences between the two products.
|
||||
|
||||
|
||||
|
||||
## Why Honcho?
|
||||
Mem0 & Honcho both store your data. Only Honcho reasons about it. [Read more about our approach](https://blog.plasticlabs.ai/blog/Memory-as-Reasoning).
|
||||
|
||||
**Compounding Insights** - Honcho extracts insights that build on each other over time. The more your users interact, the richer and more accurate their profiles become.
|
||||
|
||||
**Superior Performance** - Higher accuracy on memory retrieval benchmarks with faster inference times (more details soon!).
|
||||
|
||||
**Competitive Pricing** - Mem0 charges for retrieval, not ingestion. Meaning you pay to access your own data. Honcho offers straightforward pricing with a generous free tier.
|
||||
|
||||
**Advanced Multi-Peer Sessions** - Honcho offers configurable observation settings (who builds memories about whom), representation-based queries between participants, and first-class peer objects.
|
||||
|
||||
<Note>
|
||||
We would love to support the transfer and cost—just [book a call!](https://cal.com/team/plasticlabs/migration-to-honcho)
|
||||
</Note>
|
||||
|
||||
## Quick Migration
|
||||
|
||||
For the best results, we recommend importing your raw messages directly into Honcho. This gives Honcho the full context to build rich, accurate representations and enables features like session summaries.
|
||||
|
||||
However, if you'd like to get started quickly, you can migrate your existing Mem0 memories directly as **observations**.
|
||||
|
||||
<Info>
|
||||
Get your API key at [app.honcho.dev/api-keys](https://app.honcho.dev/api-keys). New accounts start with $100 credits.
|
||||
</Info>
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# pip install mem0ai honcho-ai
|
||||
from mem0 import MemoryClient
|
||||
from honcho import Honcho
|
||||
|
||||
# Export from Mem0
|
||||
mem0 = MemoryClient(api_key="your-mem0-api-key")
|
||||
memories = mem0.get_all(filters={"user_id": "user123"}, page_size=100)
|
||||
|
||||
# Initialize Honcho
|
||||
honcho = Honcho(api_key="your-honcho-api-key")
|
||||
user = honcho.peer("user123")
|
||||
session = honcho.session("imported")
|
||||
session.add_peers([user])
|
||||
|
||||
# Import memories directly as observations
|
||||
observations = []
|
||||
for memory in memories['results']:
|
||||
content = memory.get("memory") or memory.get("messages", [{}])[0].get("content", "")
|
||||
if content:
|
||||
observations.append({"content": content, "session_id": "imported"})
|
||||
|
||||
# Batch create observations (up to 100 at a time)
|
||||
if observations:
|
||||
user.observations.create(observations)
|
||||
|
||||
print(f"Migrated {len(observations)} memories as observations!")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// npm install mem0ai @honcho-ai/sdk
|
||||
import MemoryClient from "mem0ai";
|
||||
import { Honcho } from "@honcho-ai/sdk";
|
||||
|
||||
// Export from Mem0
|
||||
const mem0 = new MemoryClient({ apiKey: "your-mem0-api-key" });
|
||||
const memories = await mem0.getAll({ filters: { user_id: "user123" }, page_size: 100 });
|
||||
|
||||
// Initialize Honcho
|
||||
const honcho = new Honcho({ apiKey: "your-honcho-api-key" });
|
||||
const user = await honcho.peer("user123");
|
||||
const session = await honcho.session("imported");
|
||||
await session.addPeers([user]);
|
||||
|
||||
// Import memories directly as observations
|
||||
const observations = memories.results
|
||||
.map(memory => ({
|
||||
content: memory.memory || memory.messages?.[0]?.content || "",
|
||||
session_id: "imported"
|
||||
}))
|
||||
.filter(obs => obs.content);
|
||||
|
||||
// Batch create observations (up to 100 at a time)
|
||||
if (observations.length > 0) {
|
||||
await user.observations.create(observations);
|
||||
}
|
||||
|
||||
console.log(`Migrated ${observations.length} memories as observations!`);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
That's it! The user's Mem0 memories are now searchable in Honcho as observations. For richer representations with deductive reasoning and session summaries, consider importing your raw messages as described in the [Step-by-Step Migration](#step-by-step-migration) section.
|
||||
|
||||
For more details on replacing Mem0 API calls with Honcho equivalents go to [API Comparison](#api-comparison).
|
||||
|
||||
## Step-by-Step Migration
|
||||
|
||||
Prefer a more detailed walkthrough? Follow these steps:
|
||||
|
||||
### 1. Export User Messages
|
||||
|
||||
Importing raw user messages gives Honcho the full conversational context to build the most accurate representations. We recommend using a data structure that preserves the session and peer structure.
|
||||
|
||||
<Note>
|
||||
If you need any help with this transfer or have any questions, please reach out at hello@plasticlabs.ai or [book a call!](https://cal.com/team/plasticlabs/migration-to-honcho)
|
||||
</Note>
|
||||
|
||||
Alternatively, if you want to import the Mem0 memories, follow the example above and find more info in Mem0's [export API documentation](https://docs.mem0.ai/cookbooks/essentials/exporting-memories).
|
||||
|
||||
### 2. Install the Honcho SDK
|
||||
|
||||
<CodeGroup>
|
||||
```bash Python (uv)
|
||||
uv add honcho-ai
|
||||
```
|
||||
|
||||
```bash Python (pip)
|
||||
pip install honcho-ai
|
||||
```
|
||||
|
||||
```bash TypeScript (npm)
|
||||
npm install @honcho-ai/sdk
|
||||
```
|
||||
|
||||
```bash TypeScript (yarn)
|
||||
yarn add @honcho-ai/sdk
|
||||
```
|
||||
|
||||
```bash TypeScript (pnpm)
|
||||
pnpm add @honcho-ai/sdk
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 3. Initialize the Honcho Client
|
||||
|
||||
<Info>
|
||||
Get your API key at [app.honcho.dev/api-keys](https://app.honcho.dev/api-keys). New accounts start with $100 credits.
|
||||
</Info>
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from honcho import Honcho
|
||||
|
||||
honcho = Honcho( api_key="your-api-key" )
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Honcho } from '@honcho-ai/sdk';
|
||||
|
||||
const honcho = new Honcho({apiKey: process.env.HONCHO_API_KEY!});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 4. Import Your Data
|
||||
This is a possible implementation using raw user messages. Adapt the data structure to match your exported format.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Example data structure (preserving message history with timestamps):
|
||||
exported_data = {
|
||||
"session-1": {
|
||||
"user123": [
|
||||
{"content": "I prefer dark mode", "timestamp": "2024-01-15T10:30:00Z"},
|
||||
{"content": "My name is Alex", "timestamp": "2024-01-15T10:31:00Z"},
|
||||
],
|
||||
"user456": [
|
||||
{"content": "I work in finance", "timestamp": "2024-01-15T11:00:00Z"},
|
||||
{"content": "I like concise responses", "timestamp": "2024-01-15T11:02:00Z"},
|
||||
],
|
||||
},
|
||||
"session-2": {
|
||||
"user123": [
|
||||
{"content": "Meeting notes from last week...", "timestamp": "2024-01-16T09:00:00Z"},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
# Import into Honcho
|
||||
for session_name, users in exported_data.items():
|
||||
session = honcho.session(session_name)
|
||||
|
||||
for user_id, messages in users.items():
|
||||
peer = honcho.peer(user_id)
|
||||
session.add_peers([peer])
|
||||
|
||||
# Sort by timestamp to preserve message order
|
||||
sorted_messages = sorted(messages, key=lambda m: m["timestamp"])
|
||||
session.add_messages([peer.message(m["content"]) for m in sorted_messages])
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Example data structure (preserving message history with timestamps):
|
||||
interface Message {
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
const exportedData: Record<string, Record<string, Message[]>> = {
|
||||
"session-1": {
|
||||
"user123": [
|
||||
{ content: "I prefer dark mode", timestamp: "2024-01-15T10:30:00Z" },
|
||||
{ content: "My name is Alex", timestamp: "2024-01-15T10:31:00Z" },
|
||||
],
|
||||
"user456": [
|
||||
{ content: "I work in finance", timestamp: "2024-01-15T11:00:00Z" },
|
||||
{ content: "I like concise responses", timestamp: "2024-01-15T11:02:00Z" },
|
||||
],
|
||||
},
|
||||
"session-2": {
|
||||
"user123": [
|
||||
{ content: "Meeting notes from last week...", timestamp: "2024-01-16T09:00:00Z" },
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
// Import into Honcho
|
||||
for (const [sessionName, users] of Object.entries(exportedData)) {
|
||||
const session = await honcho.session(sessionName);
|
||||
|
||||
for (const [userId, messages] of Object.entries(users)) {
|
||||
const peer = await honcho.peer(userId);
|
||||
await session.addPeers([peer]);
|
||||
|
||||
// Sort by timestamp to preserve message order
|
||||
const sortedMessages = messages.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
await session.addMessages(sortedMessages.map((m) => peer.message(m.content)));
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 5. Update Your Application Code
|
||||
|
||||
Reference the [API Comparison](#api-comparison) to replace your Mem0 API calls with the Honcho equivalents.
|
||||
|
||||
## API Comparison
|
||||
|
||||
### Core Operations
|
||||
|
||||
| Operation | Mem0 | Honcho | Notes |
|
||||
|-----------|------|--------|-------|
|
||||
| **Initialize** | `MemoryClient(api_key=...)` | `Honcho(api_key=...)` | |
|
||||
| **Identity** | `user_id` string param | `peer = honcho.peer("id")` | Peers can be users or AI agents |
|
||||
| **Add messages** | `client.add(messages, user_id=...)` | `session.add_messages([peer.message(...)])` | Session-scoped, triggers reasoning |
|
||||
| **Add observations** | | `peer.observations.create([...])` | Direct observation or "memory" import, no processing |
|
||||
| **Search** | `client.search(query, filters={"user_id": ...})` | `peer.search(query)` or `peer.observations.query(...)` | Scoped to peer or session |
|
||||
| **List all** | `client.get_all(filters={"user_id": ...})` | `session.get_messages()` or `peer.observations.list()` | Messages or observations |
|
||||
| **Update** | `client.update(memory_id, data=...)` | `honcho.update_message(message, metadata=...)` | Metadata updates only |
|
||||
| **Delete** | `client.delete(memory_id)` | `peer.observations.delete(id)` or `session.delete()` | Observation or session-level |
|
||||
|
||||
### Honcho-Only Capabilities
|
||||
|
||||
Mem0 requires manual assembly of context from `search()` results. Honcho's `session.get_context()` returns a ready-to-use `SessionContext` object with built-in token limits, auto-included summaries, and format helpers (`.to_openai()`, `.to_anthropic()`).
|
||||
|
||||
<Card title="Get Context" icon="window-restore" href="../../documentation/core-concepts/features/get-context">
|
||||
Learn more about token-optimized context retrieval
|
||||
</Card>
|
||||
|
||||
|
||||
Mem0's `search()` returns basic vector, semantic, or raw memory matches. Honcho's `peer.chat()` enables your agent to *reason* about what it knows—returning synthesized natural language insights with streaming support and scoped queries.
|
||||
|
||||
<Card title="Dialectic Endpoint" icon="brain" href="../../documentation/core-concepts/features/dialectic-endpoint">
|
||||
Learn more about inference-powered queries
|
||||
</Card>
|
||||
|
||||
Additional features with **no Mem0 equivalent**:
|
||||
|
||||
| Honcho Method | Description | Use Case |
|
||||
|---------------|-------------|----------|
|
||||
| `peer.card()` | Stable biographical facts (name, preferences, background) | User profiles, personalization |
|
||||
| `session.working_rep(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
|
||||
| `session.get_summaries()` | Auto-generated short/long session summaries | Conversation continuity |
|
||||
| `SessionPeerConfig` | Configure observation settings (who learns about whom) | Privacy controls, role-based learning |
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Architecture" icon="rocket" href="../../documentation/core-concepts/architecture">
|
||||
Understand peers and sessions
|
||||
</Card>
|
||||
<Card title="Dialectic API" icon="brain" href="../../documentation/core-concepts/features/dialectic-endpoint">
|
||||
Inference responses
|
||||
</Card>
|
||||
<Card title="Guides" icon="book" href="../../guides/overview">
|
||||
Integration examples
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Questions? Join our [Discord](https://discord.gg/honcho) or open an issue on [GitHub](https://github.com/plastic-labs/honcho/issues).
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# Honcho CrewAI Integration
|
||||
|
||||
Build CrewAI agents with persistent memory and reasoning capabilities powered by Honcho.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install honcho-crewai
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from honcho_crewai import HonchoStorage
|
||||
|
||||
# Initialize Honcho storage
|
||||
storage = HonchoStorage(user_id="user-123")
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
|
||||
# Create agent with memory
|
||||
agent = Agent(
|
||||
role="AI Assistant",
|
||||
goal="Help users with persistent memory",
|
||||
backstory="You remember past conversations.",
|
||||
)
|
||||
|
||||
# Create crew with external memory
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
external_memory=external_memory
|
||||
)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Memory**: CrewAI agents automatically store and retrieve conversation context
|
||||
- **Semantic Search**: Find relevant past messages using vector similarity
|
||||
- **Logical Reasoning**: Query what the system knows about users via the Dialectic API
|
||||
- **Multi-Agent Support**: Give each agent distinct memory and identity
|
||||
- **Tools Integration**: `HonchoGetContextTool`, `HonchoDialecticTool`, and `HonchoSearchTool` for explicit memory control
|
||||
|
||||
## Documentation
|
||||
|
||||
For comprehensive guides, examples, and API reference, visit:
|
||||
**[https://docs.honcho.dev/v2/integrations/crewai](https://docs.honcho.dev/v2/integrations/crewai)**
|
||||
|
||||
## Examples
|
||||
|
||||
Check out complete examples in the [GitHub repository](https://github.com/plastic-labs/honcho/tree/main/examples/crewai/python/examples).
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0-or-later
|
||||
|
||||
## Support
|
||||
|
||||
- Report issues: [GitHub Issues](https://github.com/plastic-labs/honcho/issues)
|
||||
- Documentation: [docs.honcho.dev](https://docs.honcho.dev)
|
||||
- Website: [honcho.dev](https://honcho.dev)
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
Hybrid Memory Example: Combining Automatic Memory + Explicit Tools
|
||||
|
||||
Demonstrates combining automatic memory (HonchoStorage) with explicit memory tools.
|
||||
The agent gets baseline context automatically but can also make targeted queries.
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from honcho import Honcho
|
||||
from honcho_crewai import (
|
||||
HonchoStorage,
|
||||
HonchoSearchTool,
|
||||
HonchoDialecticTool,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
"""Hybrid memory example with automatic baseline + explicit tools."""
|
||||
# Initialize Honcho
|
||||
honcho = Honcho()
|
||||
user_id = "hybrid-demo-user"
|
||||
session_id = "hybrid-demo-session"
|
||||
|
||||
# Setup automatic memory
|
||||
storage = HonchoStorage(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
honcho_client=honcho
|
||||
)
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
|
||||
# Add conversation history
|
||||
messages = [
|
||||
("user", "I'm planning a trip to Japan next spring"),
|
||||
("assistant", "How exciting! Japan is beautiful in spring."),
|
||||
("user", "I love Japanese cuisine, especially ramen and sushi"),
|
||||
("assistant", "You'll find amazing food there!"),
|
||||
("user", "My budget is around $3000 for the whole trip"),
|
||||
("assistant", "That's a good budget for a memorable trip."),
|
||||
("user", "I prefer cultural experiences over touristy attractions"),
|
||||
]
|
||||
|
||||
for role, message in messages:
|
||||
external_memory.save(message, metadata={"agent": role})
|
||||
|
||||
# Create memory tools for targeted queries
|
||||
search_tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
dialectic_tool = HonchoDialecticTool(
|
||||
honcho=honcho, session_id=session_id, peer_id=user_id
|
||||
)
|
||||
|
||||
# Create agent with both automatic memory AND tools
|
||||
travel_agent = Agent(
|
||||
role="Travel Advisor",
|
||||
goal="Create personalized travel recommendations using memory",
|
||||
backstory=(
|
||||
"You are a travel advisor with access to conversation history. "
|
||||
"You can use tools to search for specific details or understand preferences."
|
||||
),
|
||||
tools=[search_tool, dialectic_tool],
|
||||
verbose=True,
|
||||
allow_delegation=False
|
||||
)
|
||||
|
||||
# Create task
|
||||
task = Task(
|
||||
description=(
|
||||
"Create a 3-day Tokyo itinerary for the user.\n\n"
|
||||
"Use search_tool to find their budget and food preferences.\n"
|
||||
"Use query_peer_knowledge to understand their travel style.\n"
|
||||
"Then create a personalized itinerary with activities and restaurant recommendations."
|
||||
),
|
||||
expected_output="A 3-day Tokyo itinerary with daily activities and dining suggestions",
|
||||
agent=travel_agent
|
||||
)
|
||||
|
||||
# Execute with hybrid memory: automatic baseline + explicit tools
|
||||
crew = Crew(
|
||||
agents=[travel_agent],
|
||||
tasks=[task],
|
||||
process=Process.sequential,
|
||||
external_memory=external_memory, # Automatic memory!
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("RESULT")
|
||||
print("=" * 70)
|
||||
print(result.raw)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
"""
|
||||
CrewAI Integration with Honcho and OpenAI
|
||||
|
||||
This example demonstrates how to build AI agents with persistent memory using
|
||||
CrewAI for agent orchestration, OpenAI for the AI model, and Honcho for memory
|
||||
management via the honcho_crewai package.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from honcho_crewai import HonchoStorage
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def run_conversation_turn(
|
||||
user_id: str,
|
||||
user_input: str,
|
||||
session_id: Optional[str] = None,
|
||||
storage: Optional[HonchoStorage] = None
|
||||
) -> tuple[str, HonchoStorage]:
|
||||
"""
|
||||
Run a single conversation turn with the CrewAI agent.
|
||||
|
||||
Args:
|
||||
user_id: Unique identifier for the user
|
||||
user_input: User's message
|
||||
session_id: Optional session ID for conversation continuity
|
||||
storage: Optional existing HonchoStorage instance
|
||||
|
||||
Returns:
|
||||
Tuple of (agent_response, storage_instance)
|
||||
"""
|
||||
# Initialize or reuse storage
|
||||
if storage is None:
|
||||
if not session_id:
|
||||
session_id = f"session_{user_id}"
|
||||
storage = HonchoStorage(user_id=user_id, session_id=session_id)
|
||||
|
||||
# Create ExternalMemory wrapper for automatic context retrieval
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
|
||||
# Save user input to memory
|
||||
external_memory.save(user_input, metadata={"agent": "user"})
|
||||
|
||||
# Create an agent with memory
|
||||
agent = Agent(
|
||||
role="AI Assistant",
|
||||
goal="Help users with their questions and remember context from previous conversations",
|
||||
backstory=(
|
||||
"You are a helpful AI assistant with the ability to remember past conversations. "
|
||||
"You use context from previous interactions to provide personalized and relevant responses."
|
||||
),
|
||||
verbose=False,
|
||||
allow_delegation=False
|
||||
)
|
||||
|
||||
# Create task for the agent
|
||||
task = Task(
|
||||
description=f"Respond to the user's message: {user_input}",
|
||||
expected_output="A helpful and contextually relevant response that considers conversation history",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create crew with external memory - enables automatic context retrieval
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process=Process.sequential,
|
||||
external_memory=external_memory,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Execute - CrewAI automatically retrieves relevant context from Honcho
|
||||
result = crew.kickoff()
|
||||
|
||||
# Save assistant response back to memory
|
||||
response_text = str(result.raw)
|
||||
external_memory.save(response_text, metadata={"agent": "assistant"})
|
||||
|
||||
return response_text, storage
|
||||
|
||||
|
||||
def main():
|
||||
"""Interactive chat loop with CrewAI agent powered by Honcho memory."""
|
||||
print("Welcome to the AI Assistant powered by CrewAI and Honcho!")
|
||||
print("Type 'quit' or 'exit' to end the conversation.\n")
|
||||
|
||||
user_id = "demo-user-123"
|
||||
storage = None
|
||||
|
||||
while True:
|
||||
user_input = input("You: ")
|
||||
if user_input.lower() in ['quit', 'exit']:
|
||||
print("Goodbye!")
|
||||
break
|
||||
|
||||
if not user_input.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
response, storage = run_conversation_turn(
|
||||
user_id=user_id,
|
||||
user_input=user_input,
|
||||
storage=storage
|
||||
)
|
||||
print(f"Assistant: {response}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
"""
|
||||
Simple Honcho + CrewAI Example
|
||||
|
||||
A minimal example showing how to use Honcho's ExternalMemory with CrewAI agents.
|
||||
This demonstrates the basic pattern for persistent conversation memory.
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from honcho_crewai import HonchoStorage
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
"""Simple example of CrewAI agent with Honcho memory."""
|
||||
# Initialize Honcho storage
|
||||
storage = HonchoStorage(user_id="simple-demo-user")
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
|
||||
# Add some conversation history
|
||||
messages = [
|
||||
("user", "I'm learning Python programming"),
|
||||
("assistant", "Great! Python is an excellent language to learn."),
|
||||
("user", "I'm particularly interested in web development"),
|
||||
]
|
||||
|
||||
for role, message in messages:
|
||||
external_memory.save(message, metadata={"agent": role})
|
||||
|
||||
# Create agent with memory
|
||||
agent = Agent(
|
||||
role="Programming Mentor",
|
||||
goal="Help users learn programming by remembering their interests and progress",
|
||||
backstory=(
|
||||
"You are a patient programming mentor who remembers what students "
|
||||
"have told you about their learning journey and interests."
|
||||
),
|
||||
verbose=True,
|
||||
allow_delegation=False
|
||||
)
|
||||
|
||||
# Create task
|
||||
task = Task(
|
||||
description=(
|
||||
"Based on what you know about the user's interests, "
|
||||
"suggest a simple web development project they could build to practice Python."
|
||||
),
|
||||
expected_output="A specific project suggestion with brief explanation",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Execute with memory - CrewAI automatically retrieves relevant context!
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
process=Process.sequential,
|
||||
external_memory=external_memory,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("RESULT")
|
||||
print("=" * 70)
|
||||
print(result.raw)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Honcho Tools with CrewAI Example
|
||||
|
||||
Demonstrates how to equip CrewAI agents with Honcho's memory tools:
|
||||
- HonchoGetContextTool: Retrieve session context with token limits
|
||||
- HonchoDialecticTool: Query representations about peers
|
||||
- HonchoSearchTool: Perform semantic search across session messages
|
||||
|
||||
These tools give agents explicit control over memory retrieval, beyond the
|
||||
automatic memory provided by ExternalMemory.
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from honcho import Honcho
|
||||
from honcho_crewai import (
|
||||
HonchoGetContextTool,
|
||||
HonchoDialecticTool,
|
||||
HonchoSearchTool,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate Honcho tools with CrewAI agents."""
|
||||
print("=" * 70)
|
||||
print("HONCHO TOOLS + CREWAI EXAMPLE")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
# Step 1: Setup session with conversation history
|
||||
print("1. Setting up session with conversation history...\n")
|
||||
|
||||
honcho = Honcho()
|
||||
user_id = "demo-user-45"
|
||||
session_id = "tools-demo-session"
|
||||
|
||||
user = honcho.peer(user_id)
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add conversation history
|
||||
messages = [
|
||||
"I'm planning a trip to Japan in March",
|
||||
"I love trying authentic local cuisine, especially ramen and sushi",
|
||||
"My budget is around $3000 for a 10-day trip",
|
||||
"I'm interested in visiting both Tokyo and Kyoto",
|
||||
"I prefer staying in traditional ryokans over hotels",
|
||||
]
|
||||
|
||||
for msg in messages:
|
||||
print(f" • {msg}")
|
||||
session.add_messages([user.message(msg)])
|
||||
|
||||
print("\n ✓ Session created with 5 messages\n")
|
||||
|
||||
# Step 2: Create Honcho tools
|
||||
print("2. Creating Honcho memory tools...\n")
|
||||
|
||||
context_tool = HonchoGetContextTool(
|
||||
honcho=honcho, session_id=session_id, peer_id=user_id
|
||||
)
|
||||
print(" ✓ get_session_context - Retrieve conversation context")
|
||||
|
||||
dialectic_tool = HonchoDialecticTool(
|
||||
honcho=honcho, session_id=session_id, peer_id=user_id
|
||||
)
|
||||
print(" ✓ query_peer_knowledge - Ask about user preferences")
|
||||
|
||||
search_tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
print(" ✓ search_session_messages - Semantic search messages\n")
|
||||
|
||||
# Step 3: Create agent with tools
|
||||
print("3. Creating travel planning agent with memory tools...\n")
|
||||
|
||||
travel_agent = Agent(
|
||||
role="Travel Planning Specialist",
|
||||
goal="Create personalized travel recommendations using memory tools",
|
||||
backstory=(
|
||||
"You are an expert travel planner with access to conversation memory tools. "
|
||||
"Use the tools to understand the user's preferences before making recommendations."
|
||||
),
|
||||
tools=[context_tool, dialectic_tool, search_tool],
|
||||
verbose=True,
|
||||
allow_delegation=False
|
||||
)
|
||||
print(" ✓ Agent created with 3 Honcho tools\n")
|
||||
|
||||
# Step 4: Create task
|
||||
print("4. Creating task...\n")
|
||||
|
||||
task = Task(
|
||||
description=(
|
||||
"Create a personalized 3-day Tokyo itinerary. "
|
||||
"Use the memory tools to understand:\n"
|
||||
" • Food preferences (search for 'cuisine' or 'food')\n"
|
||||
" • Travel style and budget (query user knowledge)\n"
|
||||
" • Recent context (get conversation context)\n"
|
||||
"Then create a detailed plan matching their interests."
|
||||
),
|
||||
expected_output=(
|
||||
"A 3-day Tokyo itinerary with:\n"
|
||||
" • Daily activities matching user interests\n"
|
||||
" • Restaurant recommendations\n"
|
||||
" • Accommodation suggestions\n"
|
||||
" • Budget considerations"
|
||||
),
|
||||
agent=travel_agent
|
||||
)
|
||||
print(" ✓ Task created\n")
|
||||
|
||||
# Step 5: Execute
|
||||
print("5. Executing crew (agent will use tools to retrieve memory)...\n")
|
||||
print("-" * 70 + "\n")
|
||||
|
||||
crew = Crew(
|
||||
agents=[travel_agent],
|
||||
tasks=[task],
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
crew.kickoff()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
[project]
|
||||
name = "honcho-crewai"
|
||||
version = "0.1.0"
|
||||
description = "CrewAI integration with Honcho for persistent agent memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "AGPL-3.0-or-later"}
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"}
|
||||
]
|
||||
keywords = [
|
||||
"honcho",
|
||||
"crewai",
|
||||
"ai-agents",
|
||||
"memory",
|
||||
"agent-memory",
|
||||
"persistent-memory"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Framework :: Pydantic",
|
||||
]
|
||||
dependencies = [
|
||||
"crewai>=0.134.0",
|
||||
"honcho-ai>=0.2.0",
|
||||
"openai>=1.0.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://honcho.dev"
|
||||
Documentation = "https://docs.honcho.dev/v2/integrations/crewai"
|
||||
Repository = "https://github.com/plastic-labs/honcho"
|
||||
"Bug Tracker" = "https://github.com/plastic-labs/honcho/issues"
|
||||
Changelog = "https://github.com/plastic-labs/honcho/blob/main/CHANGELOG.md"
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
"""
|
||||
Honcho CrewAI Integration
|
||||
|
||||
This package provides seamless integration between Honcho and CrewAI,
|
||||
enabling AI agents to maintain persistent memory across conversations.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from honcho_crewai import HonchoStorage, HonchoSearchTool, HonchoGetContextTool, HonchoDialecticTool
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from crewai import Agent, Task, Crew
|
||||
from honcho import Honcho
|
||||
|
||||
# Initialize Honcho client and storage
|
||||
honcho = Honcho()
|
||||
storage = HonchoStorage(user_id="user123", honcho_client=honcho)
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
|
||||
# Create tools for agents
|
||||
search_tool = HonchoSearchTool(honcho=honcho, session_id=storage.session_id)
|
||||
context_tool = HonchoGetContextTool(honcho=honcho, session_id=storage.session_id, peer_id="user123")
|
||||
dialectic_tool = HonchoDialecticTool(honcho=honcho, session_id=storage.session_id, peer_id="user123")
|
||||
|
||||
# Create agent with memory and tools
|
||||
agent = Agent(
|
||||
role="AI Assistant",
|
||||
goal="Help users with persistent memory",
|
||||
backstory="You remember past conversations.",
|
||||
tools=[search_tool, context_tool, dialectic_tool],
|
||||
)
|
||||
|
||||
# Define a task for the crew
|
||||
task = Task(
|
||||
description="Help the user with their request",
|
||||
expected_output="A helpful response",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
# Create crew with external memory
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
external_memory=external_memory
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
from honcho_crewai.exceptions import HonchoDependencyError
|
||||
from honcho_crewai.storage import HonchoStorage
|
||||
from honcho_crewai.tools import (
|
||||
HonchoDialecticTool,
|
||||
HonchoGetContextTool,
|
||||
HonchoSearchTool,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = [
|
||||
"HonchoDependencyError",
|
||||
"HonchoDialecticTool",
|
||||
"HonchoGetContextTool",
|
||||
"HonchoSearchTool",
|
||||
"HonchoStorage",
|
||||
]
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
"""
|
||||
Exception classes for CrewAI integration.
|
||||
"""
|
||||
|
||||
|
||||
class HonchoDependencyError(ImportError):
|
||||
"""Raised when required CrewAI dependencies are not installed."""
|
||||
|
||||
def __init__(self, framework: str, install_command: str) -> None:
|
||||
self.framework = framework
|
||||
self.install_command = install_command
|
||||
super().__init__(
|
||||
f"{framework} dependencies not found. Install with: {install_command}"
|
||||
)
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
"""
|
||||
Honcho Storage for CrewAI External Memory
|
||||
|
||||
This module provides a Honcho-backed storage provider for CrewAI's external memory
|
||||
system, enabling AI agents to maintain persistent conversation memory across sessions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Optional
|
||||
|
||||
from crewai.memory.storage.interface import Storage
|
||||
from honcho import Honcho
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HonchoStorage(Storage):
|
||||
"""
|
||||
Honcho-backed storage provider for CrewAI external memory.
|
||||
|
||||
Implements CrewAI's Storage interface using Honcho's session-based memory,
|
||||
allowing agents to maintain context across conversations.
|
||||
|
||||
Attributes:
|
||||
honcho: The Honcho client instance
|
||||
user: Peer representing the user
|
||||
assistant: Peer representing the AI assistant
|
||||
session: The conversation session
|
||||
session_id: Unique identifier for the session
|
||||
|
||||
Example:
|
||||
```python
|
||||
from honcho_crewai import HonchoStorage
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
# Initialize storage
|
||||
storage = HonchoStorage(user_id="user123")
|
||||
|
||||
# Use with CrewAI's external memory
|
||||
external_memory = ExternalMemory(storage=storage)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: str,
|
||||
session_id: Optional[str] = None,
|
||||
honcho_client: Optional[Honcho] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize Honcho storage for a specific user and session.
|
||||
|
||||
Args:
|
||||
user_id: Unique identifier for the user
|
||||
session_id: Optional session ID. If not provided, a UUID will be generated
|
||||
honcho_client: Optional Honcho client instance. If not provided, creates one
|
||||
using the demo environment (https://demo.honcho.dev)
|
||||
"""
|
||||
self.honcho = honcho_client or Honcho()
|
||||
|
||||
# Initialize user and assistant peers
|
||||
self.user = self.honcho.peer(user_id)
|
||||
self.assistant = self.honcho.peer("assistant")
|
||||
|
||||
# Create or use existing session
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
self.session = self.honcho.session(session_id)
|
||||
self.session_id = session_id
|
||||
|
||||
def save(self, value: Any, metadata: dict[str, Any]) -> None:
|
||||
"""
|
||||
Save a message to Honcho session.
|
||||
|
||||
This method is called by CrewAI to store messages and context. Messages
|
||||
are associated with the appropriate peer (user or assistant) based on
|
||||
the metadata.
|
||||
|
||||
Args:
|
||||
value: Message content to save
|
||||
metadata: Metadata dict that may contain 'role', 'agent', or 'type' info
|
||||
Common keys: 'role', 'agent', 'type'
|
||||
"""
|
||||
try:
|
||||
# Determine if this is from user or assistant based on metadata
|
||||
# Check various metadata keys that might indicate the role
|
||||
role = metadata.get("role", metadata.get("agent", "assistant"))
|
||||
is_user = role == "user"
|
||||
peer = self.user if is_user else self.assistant
|
||||
|
||||
content_str = str(value)
|
||||
|
||||
# Add message to session
|
||||
self.session.add_messages([peer.message(content_str, metadata=metadata)])
|
||||
|
||||
logger.debug(
|
||||
"Saved message from %s: %s...",
|
||||
metadata.get("name", role),
|
||||
content_str[:100],
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error saving to Honcho")
|
||||
raise
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
limit: int = 10,
|
||||
score_threshold: float = 0.5,
|
||||
filters: Optional[dict[str, Any]] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Search for relevant messages using semantic search.
|
||||
|
||||
This method uses Honcho's semantic vector search to find messages most
|
||||
relevant to the query.
|
||||
|
||||
Args:
|
||||
query: Search query used for semantic matching
|
||||
limit: Maximum number of messages to retrieve
|
||||
score_threshold: Minimum relevance score (not currently used by Honcho API)
|
||||
filters: Optional filters to scope the search. Supports Honcho's filter syntax
|
||||
including logical operators (AND, OR, NOT), comparison operators
|
||||
(gt, gte, lt, lte, eq, ne), and metadata filtering.
|
||||
Example: {"peer_id": "user123"} or {"metadata": {"type": "important"}}
|
||||
See: https://docs.honcho.dev/v2/documentation/core-concepts/features/using-filters
|
||||
|
||||
Returns:
|
||||
List of message dictionaries in CrewAI expected format.
|
||||
Each dict contains:
|
||||
- content: The message content
|
||||
- memory: The message content (required by CrewAI)
|
||||
- context: The message content (for compatibility)
|
||||
- metadata: Message metadata including peer_id, created_at, and custom metadata
|
||||
"""
|
||||
try:
|
||||
results = []
|
||||
# Use semantic search to find relevant messages
|
||||
# This performs vector similarity search on message content
|
||||
messages = self.session.search(query=query, filters=filters, limit=limit)
|
||||
|
||||
# Convert to CrewAI expected format
|
||||
for msg in messages:
|
||||
# Build base metadata with peer_id and created_at
|
||||
metadata = {
|
||||
"peer_id": msg.peer_id,
|
||||
"created_at": str(msg.created_at) if hasattr(msg, "created_at") else None,
|
||||
}
|
||||
|
||||
# Merge custom metadata if present
|
||||
if hasattr(msg, "metadata") and msg.metadata:
|
||||
metadata.update(msg.metadata)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"content": msg.content,
|
||||
"memory": msg.content,
|
||||
"context": msg.content,
|
||||
"metadata": metadata,
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug("Search for '%s' returned %d results", query, len(results))
|
||||
return results
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error searching Honcho")
|
||||
raise
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Create a new session, effectively resetting memory.
|
||||
|
||||
This creates a new Honcho session with a fresh UUID, allowing the agent
|
||||
to start a new conversation without the previous context.
|
||||
"""
|
||||
try:
|
||||
new_session_id = str(uuid.uuid4())
|
||||
self.session = self.honcho.session(new_session_id)
|
||||
self.session_id = new_session_id
|
||||
|
||||
logger.debug("Reset session. New session ID: %s", new_session_id)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error resetting Honcho session")
|
||||
raise
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
"""
|
||||
Honcho Tools for CrewAI
|
||||
|
||||
This module provides tools that allow CrewAI agents to interact with Honcho's
|
||||
session context, dialectic API, and semantic search capabilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
from honcho import Honcho
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Input Schemas
|
||||
class GetContextInput(BaseModel):
|
||||
"""Input schema for get_context tool."""
|
||||
|
||||
tokens: Optional[int] = Field(
|
||||
default=None, gt=0, description="Maximum number of tokens to include in the context"
|
||||
)
|
||||
peer_target: Optional[str] = Field(
|
||||
default=None, description="A peer ID to get context for (retrieves representation and peer card)"
|
||||
)
|
||||
summary: bool = Field(
|
||||
default=True, description="Whether to include session summary in the context"
|
||||
)
|
||||
peer_perspective: Optional[str] = Field(
|
||||
default=None, description="Peer ID to use as the perspective for context retrieval"
|
||||
)
|
||||
|
||||
|
||||
class DialecticInput(BaseModel):
|
||||
"""Input schema for dialectic (chat) tool."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Natural language question to ask")
|
||||
target: Optional[str] = Field(
|
||||
default=None, description="Optional target peer for local representation query"
|
||||
)
|
||||
session_id: Optional[str] = Field(
|
||||
default=None, description="Optional session ID to scope query to specific session"
|
||||
)
|
||||
|
||||
|
||||
class SearchInput(BaseModel):
|
||||
"""Input schema for search tool."""
|
||||
|
||||
query: str = Field(..., min_length=1, description="Search query for semantic matching")
|
||||
limit: int = Field(default=10, ge=1, le=100, description="Number of results to return (1-100)")
|
||||
filters: Optional[dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional filters to scope the search. Supports Honcho's filter syntax including "
|
||||
"logical operators (AND, OR, NOT), comparison operators (gt, gte, lt, lte, eq, ne), "
|
||||
"and metadata filtering. Examples: {'peer_id': 'user123'}, {'metadata': {'priority': 'high'}}, "
|
||||
"{'created_at': {'gte': '2024-01-01'}}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Tool Implementations
|
||||
class HonchoGetContextTool(BaseTool):
|
||||
"""
|
||||
Tool to retrieve session context with token limits.
|
||||
|
||||
This tool fetches the conversation history and session summary within
|
||||
a specified token budget, optimized for LLM context windows.
|
||||
"""
|
||||
|
||||
name: str = "get_session_context"
|
||||
description: str = (
|
||||
"Retrieve recent conversation context within token limits. "
|
||||
"Returns formatted messages with optional summary and peer information. "
|
||||
"Useful for getting optimized context that fits within token budgets."
|
||||
)
|
||||
args_schema: type[BaseModel] = GetContextInput
|
||||
|
||||
_honcho: Honcho = PrivateAttr()
|
||||
_session_id: str = PrivateAttr()
|
||||
_peer_id: str = PrivateAttr()
|
||||
|
||||
def __init__(self, honcho: Honcho, session_id: str, peer_id: str) -> None:
|
||||
"""
|
||||
Initialize the get_context tool.
|
||||
|
||||
Args:
|
||||
honcho: Honcho client instance
|
||||
session_id: ID of the session to get context from
|
||||
peer_id: ID of the peer requesting context
|
||||
"""
|
||||
super().__init__()
|
||||
self._honcho = honcho
|
||||
self._session_id = session_id
|
||||
self._peer_id = peer_id
|
||||
|
||||
def _run(
|
||||
self,
|
||||
tokens: Optional[int] = None,
|
||||
peer_target: Optional[str] = None,
|
||||
*,
|
||||
summary: bool = True,
|
||||
peer_perspective: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Execute get_context and format results.
|
||||
|
||||
Args:
|
||||
tokens: Maximum tokens to include
|
||||
peer_target: Target peer ID for representation
|
||||
summary: Whether to include summary
|
||||
peer_perspective: Peer ID to use as perspective
|
||||
|
||||
Returns:
|
||||
Formatted string containing context information
|
||||
"""
|
||||
try:
|
||||
session = self._honcho.session(self._session_id)
|
||||
context = session.get_context(
|
||||
summary=summary,
|
||||
tokens=tokens,
|
||||
peer_target=peer_target,
|
||||
peer_perspective=peer_perspective,
|
||||
)
|
||||
|
||||
# Format for agent consumption
|
||||
result = []
|
||||
|
||||
# Add summary if present
|
||||
if context.summary:
|
||||
result.append("=== Session Summary ===")
|
||||
result.append(context.summary.content)
|
||||
result.append("")
|
||||
|
||||
# Add peer representation if present
|
||||
if context.peer_representation:
|
||||
result.append("=== Peer Representation ===")
|
||||
result.append(context.peer_representation)
|
||||
result.append("")
|
||||
|
||||
# Add peer card if present
|
||||
if context.peer_card:
|
||||
result.append("=== Peer Card ===")
|
||||
result.extend(context.peer_card)
|
||||
result.append("")
|
||||
|
||||
# Add messages
|
||||
if context.messages:
|
||||
result.append(f"=== Messages ({len(context.messages)}) ===")
|
||||
for msg in context.messages:
|
||||
result.append(f"{msg.peer_id}: {msg.content}")
|
||||
|
||||
return "\n".join(result) if result else "No context available"
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error retrieving context")
|
||||
return f"Error retrieving context: {e!s}"
|
||||
|
||||
|
||||
class HonchoDialecticTool(BaseTool):
|
||||
"""
|
||||
Tool to query Honcho's dialectic API (peer representations).
|
||||
|
||||
This tool allows agents to ask questions about what the system knows
|
||||
about users or other peers, leveraging Honcho's reasoning capabilities.
|
||||
"""
|
||||
|
||||
name: str = "query_peer_knowledge"
|
||||
description: str = (
|
||||
"Query the system's representation about peers. "
|
||||
"Ask questions like 'What does the user like?' or 'What are their preferences?' "
|
||||
"to retrieve information from the peer's long-term representation. "
|
||||
"Can optionally query what one peer knows about another (local representation)."
|
||||
)
|
||||
args_schema: type[BaseModel] = DialecticInput
|
||||
|
||||
_honcho: Honcho = PrivateAttr()
|
||||
_session_id: str = PrivateAttr()
|
||||
_peer_id: str = PrivateAttr()
|
||||
|
||||
def __init__(self, honcho: Honcho, session_id: str, peer_id: str) -> None:
|
||||
"""
|
||||
Initialize the dialectic tool.
|
||||
|
||||
Args:
|
||||
honcho: Honcho client instance
|
||||
session_id: Default session ID for scoped queries
|
||||
peer_id: ID of the peer to query about
|
||||
"""
|
||||
super().__init__()
|
||||
self._honcho = honcho
|
||||
self._session_id = session_id
|
||||
self._peer_id = peer_id
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
target: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Execute dialectic query.
|
||||
|
||||
Args:
|
||||
query: Natural language question to ask
|
||||
target: Optional target peer for local representation
|
||||
session_id: Optional session ID to scope the query
|
||||
|
||||
Returns:
|
||||
String response from the dialectic API
|
||||
"""
|
||||
try:
|
||||
peer = self._honcho.peer(self._peer_id)
|
||||
|
||||
# Use provided session_id or fall back to default
|
||||
scope_session_id = session_id or self._session_id
|
||||
|
||||
# Query the dialectic API (non-streaming)
|
||||
response = peer.chat(
|
||||
query=query,
|
||||
stream=False,
|
||||
target=target,
|
||||
session_id=scope_session_id,
|
||||
)
|
||||
|
||||
# Return the response or a default message
|
||||
if response:
|
||||
return str(response)
|
||||
else:
|
||||
return "No relevant information found."
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error querying dialectic API")
|
||||
return f"Error querying peer knowledge: {e!s}"
|
||||
|
||||
|
||||
class HonchoSearchTool(BaseTool):
|
||||
"""
|
||||
Tool to perform semantic search across session messages.
|
||||
|
||||
This tool enables agents to find relevant past messages using
|
||||
semantic similarity search, useful for retrieving specific information
|
||||
from conversation history.
|
||||
"""
|
||||
|
||||
name: str = "search_session_messages"
|
||||
description: str = (
|
||||
"Search through session messages using semantic similarity. "
|
||||
"Finds messages that are semantically related to the query, "
|
||||
"useful for retrieving specific information from past conversations."
|
||||
)
|
||||
args_schema: type[BaseModel] = SearchInput
|
||||
|
||||
_honcho: Honcho = PrivateAttr()
|
||||
_session_id: str = PrivateAttr()
|
||||
|
||||
def __init__(self, honcho: Honcho, session_id: str) -> None:
|
||||
"""
|
||||
Initialize the search tool.
|
||||
|
||||
Args:
|
||||
honcho: Honcho client instance
|
||||
session_id: ID of the session to search in
|
||||
"""
|
||||
super().__init__()
|
||||
self._honcho = honcho
|
||||
self._session_id = session_id
|
||||
|
||||
def _run(self, query: str, limit: int = 10, filters: Optional[dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
Execute semantic search.
|
||||
|
||||
Args:
|
||||
query: Search query for semantic matching
|
||||
limit: Number of results to return (1-100)
|
||||
filters: Optional filters to apply to search results
|
||||
|
||||
Returns:
|
||||
Formatted string with search results
|
||||
"""
|
||||
try:
|
||||
session = self._honcho.session(self._session_id)
|
||||
|
||||
# Perform semantic search
|
||||
messages = session.search(query=query, limit=limit, filters=filters)
|
||||
|
||||
if not messages:
|
||||
return f"No messages found matching '{query}'"
|
||||
|
||||
# Format results for agent consumption
|
||||
result = [f"=== Search Results for '{query}' ({len(messages)} found) ==="]
|
||||
for i, msg in enumerate(messages, 1):
|
||||
result.append(f"\n{i}. [{msg.peer_id}] {msg.content}")
|
||||
if hasattr(msg, "created_at") and msg.created_at:
|
||||
result.append(f" Created: {msg.created_at}")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error searching messages: %s", e)
|
||||
return f"Error searching messages: {e!s}"
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
"""
|
||||
Basic tests for honcho_crewai package
|
||||
|
||||
Validates package structure, imports, and basic functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_package_import():
|
||||
"""Test that honcho_crewai imports successfully."""
|
||||
import honcho_crewai
|
||||
|
||||
assert honcho_crewai is not None
|
||||
|
||||
|
||||
def test_storage_import():
|
||||
"""Test that HonchoStorage can be imported."""
|
||||
from honcho_crewai import HonchoStorage
|
||||
|
||||
assert HonchoStorage is not None
|
||||
|
||||
|
||||
def test_tools_import():
|
||||
"""Test that tool classes can be imported."""
|
||||
from honcho_crewai import (
|
||||
HonchoGetContextTool,
|
||||
HonchoDialecticTool,
|
||||
HonchoSearchTool,
|
||||
)
|
||||
|
||||
assert HonchoGetContextTool is not None
|
||||
assert HonchoDialecticTool is not None
|
||||
assert HonchoSearchTool is not None
|
||||
|
||||
|
||||
class TestPackageMetadata:
|
||||
"""Test package metadata and structure."""
|
||||
|
||||
def test_package_has_version(self):
|
||||
"""Test that package exposes version information."""
|
||||
import honcho_crewai
|
||||
|
||||
assert hasattr(honcho_crewai, "__version__")
|
||||
assert isinstance(honcho_crewai.__version__, str)
|
||||
assert len(honcho_crewai.__version__) > 0
|
||||
|
||||
def test_package_all_exports(self):
|
||||
"""Test that __all__ contains expected exports."""
|
||||
import honcho_crewai
|
||||
|
||||
assert hasattr(honcho_crewai, "__all__")
|
||||
expected_exports = [
|
||||
"HonchoStorage",
|
||||
"HonchoGetContextTool",
|
||||
"HonchoDialecticTool",
|
||||
"HonchoSearchTool",
|
||||
"HonchoDependencyError",
|
||||
]
|
||||
|
||||
for export in expected_exports:
|
||||
assert export in honcho_crewai.__all__, f"{export} not in __all__"
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
Tests for HonchoStorage
|
||||
|
||||
Tests the CrewAI-Honcho integration layer, focusing on:
|
||||
- CrewAI Storage interface compliance
|
||||
- Metadata mapping (agent/role -> peer_id)
|
||||
- Format conversion (Honcho -> CrewAI format)
|
||||
"""
|
||||
|
||||
from honcho_crewai import HonchoStorage
|
||||
|
||||
|
||||
class TestHonchoStorage:
|
||||
"""Tests for HonchoStorage integration layer."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that HonchoStorage initializes with correct peers and session."""
|
||||
storage = HonchoStorage(user_id="test_user")
|
||||
|
||||
assert storage is not None
|
||||
assert storage.session_id is not None
|
||||
assert storage.user is not None
|
||||
assert storage.assistant is not None
|
||||
assert storage.session is not None
|
||||
|
||||
def test_initialization_with_custom_session(self):
|
||||
"""Test that custom session_id is preserved."""
|
||||
custom_session_id = "my_custom_session"
|
||||
storage = HonchoStorage(user_id="test_user", session_id=custom_session_id)
|
||||
|
||||
assert storage.session_id == custom_session_id
|
||||
|
||||
def test_save_with_different_roles(self):
|
||||
"""Test that save handles different agent/role metadata."""
|
||||
storage = HonchoStorage(user_id="test_user_roles")
|
||||
|
||||
# Save with different metadata patterns
|
||||
storage.save("User via agent", metadata={"agent": "user"})
|
||||
storage.save("User via role", metadata={"role": "user"})
|
||||
storage.save("Assistant via agent", metadata={"agent": "assistant"})
|
||||
storage.save("Default (no metadata)", metadata={})
|
||||
|
||||
# If no exceptions raised, metadata mapping works
|
||||
|
||||
def test_search_returns_crewai_format(self):
|
||||
"""Test that search returns results in CrewAI format."""
|
||||
storage = HonchoStorage(user_id="test_user_search")
|
||||
|
||||
# Add a message
|
||||
storage.save("Test message", metadata={"agent": "user"})
|
||||
|
||||
# Search
|
||||
results = storage.search("test", limit=10)
|
||||
|
||||
# Verify CrewAI format
|
||||
assert isinstance(results, list)
|
||||
for result in results:
|
||||
# Required keys for CrewAI
|
||||
assert "memory" in result
|
||||
assert "context" in result
|
||||
assert "content" in result
|
||||
assert "metadata" in result
|
||||
|
||||
def test_search_includes_all_required_fields(self):
|
||||
"""Test that all search results have required CrewAI fields."""
|
||||
storage = HonchoStorage(user_id="test_user_format")
|
||||
|
||||
# Add a message
|
||||
storage.save("Test message", metadata={"agent": "user"})
|
||||
|
||||
# Search
|
||||
results = storage.search("test", limit=5)
|
||||
|
||||
# Verify all results have required fields with correct types
|
||||
for result in results:
|
||||
assert isinstance(result["content"], str)
|
||||
assert isinstance(result["memory"], str)
|
||||
assert isinstance(result["context"], str)
|
||||
assert isinstance(result["metadata"], dict)
|
||||
|
||||
def test_search_formats_summaries_correctly(self):
|
||||
"""Test that session summaries are formatted with [Session Summary] prefix."""
|
||||
storage = HonchoStorage(user_id="test_user_summaries")
|
||||
|
||||
# Add enough messages to potentially trigger summaries
|
||||
for i in range(25):
|
||||
storage.save(
|
||||
f"Message {i}",
|
||||
metadata={"agent": "user" if i % 2 == 0 else "assistant"},
|
||||
)
|
||||
|
||||
# Search
|
||||
results = storage.search("message", limit=10)
|
||||
|
||||
# Check summary formatting (if summaries exist)
|
||||
summary_results = [r for r in results if r["metadata"].get("type") == "summary"]
|
||||
|
||||
for summary in summary_results:
|
||||
# Verify our formatting logic
|
||||
assert "summary_type" in summary["metadata"]
|
||||
assert "[Session Summary]" in summary["context"] # Our formatting
|
||||
|
||||
def test_reset_creates_new_session_id(self):
|
||||
"""Test that reset() creates a new session with different ID."""
|
||||
storage = HonchoStorage(user_id="test_user_reset")
|
||||
|
||||
original_session_id = storage.session_id
|
||||
|
||||
# Reset
|
||||
storage.reset()
|
||||
|
||||
# Verify new session ID was created
|
||||
assert storage.session_id != original_session_id
|
||||
|
||||
def test_search_with_filters(self):
|
||||
"""Test that search accepts and uses filters parameter."""
|
||||
storage = HonchoStorage(user_id="test_user_filters")
|
||||
|
||||
# Add messages with different metadata
|
||||
storage.save("User question about Python", metadata={"agent": "user", "topic": "python"})
|
||||
storage.save("Assistant answer about Python", metadata={"agent": "assistant", "topic": "python"})
|
||||
storage.save("User question about JavaScript", metadata={"agent": "user", "topic": "javascript"})
|
||||
|
||||
# Search with peer_id filter - filter to only user messages
|
||||
results = storage.search(
|
||||
"programming",
|
||||
limit=10,
|
||||
filters={"peer_id": storage.user.id}
|
||||
)
|
||||
|
||||
# Verify results are returned and in correct format
|
||||
assert isinstance(results, list)
|
||||
for result in results:
|
||||
assert "memory" in result
|
||||
assert "content" in result
|
||||
assert "context" in result
|
||||
assert "metadata" in result
|
||||
|
||||
def test_search_with_metadata_filters(self):
|
||||
"""Test that search works with metadata filters."""
|
||||
storage = HonchoStorage(user_id="test_user_metadata_filters")
|
||||
|
||||
# Add messages with specific metadata
|
||||
storage.save("Important message", metadata={"agent": "user", "priority": "high"})
|
||||
storage.save("Regular message", metadata={"agent": "user", "priority": "low"})
|
||||
|
||||
# Search with metadata filter
|
||||
results = storage.search(
|
||||
"message",
|
||||
limit=10,
|
||||
filters={"metadata": {"priority": "high"}}
|
||||
)
|
||||
|
||||
# Verify results are in correct format
|
||||
assert isinstance(results, list)
|
||||
for result in results:
|
||||
assert "memory" in result
|
||||
assert "metadata" in result
|
||||
|
||||
def test_search_without_filters(self):
|
||||
"""Test that search works without filters."""
|
||||
storage = HonchoStorage(user_id="test_user_no_filters")
|
||||
|
||||
# Add a message
|
||||
storage.save("Test message for search", metadata={"agent": "user"})
|
||||
|
||||
# Search without filters
|
||||
results = storage.search("test", limit=5)
|
||||
|
||||
# Verify it works and returns correct format
|
||||
assert isinstance(results, list)
|
||||
for result in results:
|
||||
assert "memory" in result
|
||||
assert "content" in result
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
Tests for Honcho CrewAI Tools
|
||||
|
||||
Tests the CrewAI-Honcho tool integration layer using real Honcho SDK.
|
||||
Focuses on tool interface compliance and result formatting.
|
||||
"""
|
||||
|
||||
from honcho import Honcho
|
||||
from honcho_crewai import (
|
||||
HonchoDialecticTool,
|
||||
HonchoGetContextTool,
|
||||
HonchoSearchTool,
|
||||
)
|
||||
|
||||
|
||||
class TestGetContextTool:
|
||||
"""Tests for HonchoGetContextTool."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that tool initializes with correct attributes."""
|
||||
honcho = Honcho()
|
||||
tool = HonchoGetContextTool(
|
||||
honcho=honcho, session_id="test_session", peer_id="test_peer"
|
||||
)
|
||||
|
||||
assert tool is not None
|
||||
assert tool.name == "get_session_context"
|
||||
assert tool.description is not None
|
||||
assert tool.args_schema is not None
|
||||
|
||||
def test_returns_formatted_context(self):
|
||||
"""Test that tool returns formatted context string."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("context_test_user")
|
||||
session_id = "context_test_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add test message
|
||||
session.add_messages([peer.message("Test message for context")])
|
||||
|
||||
# Create and execute tool
|
||||
tool = HonchoGetContextTool(
|
||||
honcho=honcho, session_id=session_id, peer_id="context_test_user"
|
||||
)
|
||||
result = tool._run()
|
||||
|
||||
# Verify result is a formatted string
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
class TestDialecticTool:
|
||||
"""Tests for HonchoDialecticTool."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that tool initializes with correct attributes."""
|
||||
honcho = Honcho()
|
||||
tool = HonchoDialecticTool(
|
||||
honcho=honcho, session_id="test_session", peer_id="test_peer"
|
||||
)
|
||||
|
||||
assert tool is not None
|
||||
assert tool.name == "query_peer_knowledge"
|
||||
assert tool.description is not None
|
||||
|
||||
def test_returns_response(self):
|
||||
"""Test that tool returns a response string."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("dialectic_test_user")
|
||||
session_id = "dialectic_test_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add test messages
|
||||
session.add_messages([peer.message("I love pizza and Italian food")])
|
||||
|
||||
# Create and execute tool
|
||||
tool = HonchoDialecticTool(
|
||||
honcho=honcho, session_id=session_id, peer_id="dialectic_test_user"
|
||||
)
|
||||
result = tool._run(query="What does the user like?")
|
||||
|
||||
# Verify result is a string
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
class TestSearchTool:
|
||||
"""Tests for HonchoSearchTool."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that tool initializes with correct attributes."""
|
||||
honcho = Honcho()
|
||||
tool = HonchoSearchTool(honcho=honcho, session_id="test_session")
|
||||
|
||||
assert tool is not None
|
||||
assert tool.name == "search_session_messages"
|
||||
assert tool.description is not None
|
||||
|
||||
def test_returns_formatted_results(self):
|
||||
"""Test that tool returns formatted search results."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("search_test_user")
|
||||
session_id = "search_test_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add test messages
|
||||
session.add_messages([peer.message("I love pizza and pasta")])
|
||||
|
||||
# Create and execute tool
|
||||
tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
result = tool._run(query="food", limit=5)
|
||||
|
||||
# Verify result is a formatted string
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
# Should have either results or "No messages found"
|
||||
assert "Search Results" in result or "No messages found" in result
|
||||
|
||||
def test_search_with_filters(self):
|
||||
"""Test that search tool accepts and uses filters parameter."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("search_filter_test_user")
|
||||
session_id = "search_filter_test_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add test messages
|
||||
session.add_messages([peer.message("Important message about Python")])
|
||||
|
||||
# Create and execute tool with filters
|
||||
tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
result = tool._run(
|
||||
query="Python",
|
||||
limit=5,
|
||||
filters={"peer_id": peer.id}
|
||||
)
|
||||
|
||||
# Verify result is a formatted string
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_search_with_metadata_filters(self):
|
||||
"""Test that search tool works with metadata filters."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("search_metadata_filter_user")
|
||||
session_id = "search_metadata_filter_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add test messages with metadata
|
||||
session.add_messages([peer.message("High priority task", metadata={"priority": "high"})])
|
||||
|
||||
# Create and execute tool with metadata filter
|
||||
tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
result = tool._run(
|
||||
query="task",
|
||||
limit=5,
|
||||
filters={"metadata": {"priority": "high"}}
|
||||
)
|
||||
|
||||
# Verify result is a formatted string
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
class TestToolsWorkTogether:
|
||||
"""Test that all tools can work together."""
|
||||
|
||||
def test_all_tools_in_same_session(self):
|
||||
"""Test that all three tools can be used in the same session."""
|
||||
honcho = Honcho()
|
||||
peer = honcho.peer("combo_test_user")
|
||||
session_id = "combo_test_session"
|
||||
session = honcho.session(session_id)
|
||||
|
||||
# Add messages
|
||||
session.add_messages([peer.message("I enjoy coding in Python")])
|
||||
|
||||
# Create all tools
|
||||
context_tool = HonchoGetContextTool(
|
||||
honcho=honcho, session_id=session_id, peer_id="combo_test_user"
|
||||
)
|
||||
dialectic_tool = HonchoDialecticTool(
|
||||
honcho=honcho, session_id=session_id, peer_id="combo_test_user"
|
||||
)
|
||||
search_tool = HonchoSearchTool(honcho=honcho, session_id=session_id)
|
||||
|
||||
# Execute all tools
|
||||
context_result = context_tool._run()
|
||||
dialectic_result = dialectic_tool._run(query="What does the user like?")
|
||||
search_result = search_tool._run(query="coding", limit=5)
|
||||
|
||||
# Verify all return valid strings
|
||||
assert isinstance(context_result, str) and len(context_result) > 0
|
||||
assert isinstance(dialectic_result, str) and len(dialectic_result) > 0
|
||||
assert isinstance(search_result, str) and len(search_result) > 0
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
LangGraph Integration with Honcho and OpenAI
|
||||
|
||||
This module demonstrates how to build a stateful conversational AI agent using
|
||||
LangGraph for orchestration, OpenAI for the AI model, and Honcho for memory
|
||||
management. It creates a chatbot that remembers conversations across sessions.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from typing_extensions import TypedDict
|
||||
from honcho import Honcho, Peer, Session
|
||||
from openai import OpenAI
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
load_dotenv()
|
||||
|
||||
honcho = Honcho()
|
||||
|
||||
llm = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
|
||||
|
||||
class State(TypedDict):
|
||||
user_message: str
|
||||
assistant_response: str
|
||||
user: Peer
|
||||
assistant: Peer
|
||||
session: Session
|
||||
|
||||
def chatbot(state: State):
|
||||
user_message = state["user_message"]
|
||||
|
||||
# Get objects from state
|
||||
user = state["user"]
|
||||
assistant = state["assistant"]
|
||||
session = state["session"]
|
||||
session.add_messages([user.message(user_message)])
|
||||
|
||||
# Get context in OpenAI format with token limit
|
||||
# tokens=2000 limits the context to 2000 tokens to manage costs and fit within model limits
|
||||
messages = session.get_context(tokens=2000).to_openai(assistant=assistant)
|
||||
|
||||
# Generate response
|
||||
response = llm.chat.completions.create(
|
||||
model="gpt-5.1",
|
||||
messages=messages
|
||||
)
|
||||
assistant_response = response.choices[0].message.content
|
||||
|
||||
# Store assistant response
|
||||
session.add_messages([assistant.message(assistant_response)])
|
||||
|
||||
return {"assistant_response": assistant_response}
|
||||
|
||||
graph = StateGraph(State) \
|
||||
.add_node("chatbot", chatbot) \
|
||||
.add_edge(START, "chatbot") \
|
||||
.add_edge("chatbot", END) \
|
||||
.compile()
|
||||
|
||||
def run_conversation_turn(user_id: str, user_input: str, session_id: str | None = None):
|
||||
if not session_id:
|
||||
session_id = f"session_{user_id}"
|
||||
|
||||
# Initialize Honcho objects
|
||||
user = honcho.peer(user_id)
|
||||
assistant = honcho.peer("assistant")
|
||||
session = honcho.session(session_id)
|
||||
|
||||
result = graph.invoke({
|
||||
"user_message": user_input,
|
||||
"user": user,
|
||||
"assistant": assistant,
|
||||
"session": session
|
||||
})
|
||||
|
||||
return result["assistant_response"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Welcome to the AI Assistant! How can I help you today?")
|
||||
user_id = "test-user-1234"
|
||||
while True:
|
||||
user_input = input("You: ")
|
||||
if user_input.lower() in ['quit', 'exit']:
|
||||
break
|
||||
response = run_conversation_turn(user_id, user_input)
|
||||
print(f"Assistant: {response}\n")
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[project]
|
||||
name = "honcho-langgraph-example"
|
||||
version = "0.1.0"
|
||||
description = "LangGraph integration with Honcho for stateful conversational AI"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"honcho-ai",
|
||||
"langchain-core>=1.0.6",
|
||||
"langgraph>=1.0.3",
|
||||
"openai>=1.99.7",
|
||||
"python-dotenv>=1.1.1",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,179 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "honcho-langgraph-example",
|
||||
"dependencies": {
|
||||
"@honcho-ai/sdk": "^1.5.0",
|
||||
"@langchain/langgraph": "^1.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"openai": "^6.9.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.5.1", "", { "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-lbYtMTcL2AxdcIl5ZKenogeTlVMnE7buJWvAFOCLp0yQxcezyA/R9FPvLz2UGRApLsiplLNWhftML+3QBjIIJA=="],
|
||||
|
||||
"@honcho-ai/sdk": ["@honcho-ai/sdk@1.5.0", "", { "dependencies": { "@honcho-ai/core": "^1.5.1", "@types/node": "^24.0.1", "zod": "4.0.0" } }, "sha512-1V3wnIxyoRw0oYDE1p6GG4vRpwnn0PkwYsb4z4NeJkmWaafS9So1G32+IPhm01i2s4RnxsU31kCVL07BRsqtUw=="],
|
||||
|
||||
"@langchain/core": ["@langchain/core@1.0.6", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.64", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.76 || ^4" } }, "sha512-rDSjXATujCdJlL+OJFfyZhEca8kLmqGr4W2ebJvSHiUgXEDqu/IOWC+ZWgoKKHkGOGFdVTqQ7Qi0j2RnYS9Qlg=="],
|
||||
|
||||
"@langchain/langgraph": ["@langchain/langgraph@1.0.2", "", { "dependencies": { "@langchain/langgraph-checkpoint": "^1.0.0", "@langchain/langgraph-sdk": "~1.0.0", "uuid": "^10.0.0" }, "peerDependencies": { "@langchain/core": "^1.0.1", "zod": "^3.25.32 || ^4.1.0", "zod-to-json-schema": "^3.x" }, "optionalPeers": ["zod-to-json-schema"] }, "sha512-syxzzWTnmpCL+RhUEvalUeOXFoZy/KkzHa2Da2gKf18zsf9Dkbh3rfnRDrTyUGS1XSTejq07s4rg1qntdEDs2A=="],
|
||||
|
||||
"@langchain/langgraph-checkpoint": ["@langchain/langgraph-checkpoint@1.0.0", "", { "dependencies": { "uuid": "^10.0.0" }, "peerDependencies": { "@langchain/core": "^1.0.1" } }, "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A=="],
|
||||
|
||||
"@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.0.0", "", { "dependencies": { "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^9.0.0" }, "peerDependencies": { "@langchain/core": "^1.0.1", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@langchain/core", "react", "react-dom"] }, "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="],
|
||||
|
||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
|
||||
|
||||
"langsmith": ["langsmith@0.3.80", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "p-retry": "4", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-BWpbB9/Hkx06S5X4nJE3W5Wm1mH/j6SIqWcM/WAuT+yulohE9knstIJGmBpmSBULb46nCj+cfjRkyF1Nrc4UmA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"openai": ["openai@6.9.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg=="],
|
||||
|
||||
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
|
||||
|
||||
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
"p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@honcho-ai/sdk/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@langchain/langgraph-sdk/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"@types/node-fetch/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@honcho-ai/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@honcho-ai/sdk/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@types/node-fetch/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* LangGraph Integration with Honcho and OpenAI
|
||||
*
|
||||
* This module demonstrates how to build a stateful conversational AI agent using
|
||||
* LangGraph for orchestration, OpenAI for the AI model, and Honcho for memory
|
||||
* management. It creates a chatbot that remembers conversations across sessions.
|
||||
*/
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import { Honcho, Peer, Session } from "@honcho-ai/sdk";
|
||||
import OpenAI from "openai";
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
import * as readline from "readline/promises";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const honcho = new Honcho({});
|
||||
|
||||
const llm = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
});
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
userMessage: Annotation<string>(),
|
||||
assistantResponse: Annotation<string>(),
|
||||
user: Annotation<Peer>(),
|
||||
assistant: Annotation<Peer>(),
|
||||
session: Annotation<Session>(),
|
||||
});
|
||||
|
||||
type State = typeof StateAnnotation.State;
|
||||
|
||||
async function chatbot(state: State) {
|
||||
const userMessage = state.userMessage;
|
||||
|
||||
// Get objects from state
|
||||
const user = state.user;
|
||||
const assistant = state.assistant;
|
||||
const session = state.session;
|
||||
await session.addMessages([user.message(userMessage)]);
|
||||
|
||||
// Get context in OpenAI format with token limit
|
||||
// tokens: 2000 limits the context to 2000 tokens to manage costs and fit within model limits
|
||||
const messages = (await session.getContext({ tokens: 2000 })).toOpenAI(assistant);
|
||||
|
||||
// Generate response
|
||||
const response = await llm.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: messages
|
||||
});
|
||||
const assistantResponse = response.choices[0].message.content!;
|
||||
|
||||
// Store assistant response
|
||||
await session.addMessages([assistant.message(assistantResponse)]);
|
||||
|
||||
return { assistantResponse: assistantResponse };
|
||||
}
|
||||
|
||||
const graph = new StateGraph(StateAnnotation)
|
||||
.addNode("chatbot", chatbot)
|
||||
.addEdge(START, "chatbot")
|
||||
.addEdge("chatbot", END)
|
||||
.compile();
|
||||
|
||||
async function runConversationTurn(
|
||||
userId: string,
|
||||
userInput: string,
|
||||
sessionId?: string
|
||||
): Promise<string> {
|
||||
if (!sessionId) {
|
||||
sessionId = `session_${userId}`;
|
||||
}
|
||||
|
||||
// Initialize Honcho objects
|
||||
const user = await honcho.peer(userId);
|
||||
const assistant = await honcho.peer("assistant");
|
||||
const session = await honcho.session(sessionId);
|
||||
|
||||
const result = await graph.invoke({
|
||||
userMessage: userInput,
|
||||
user: user,
|
||||
assistant: assistant,
|
||||
session: session,
|
||||
});
|
||||
|
||||
return result.assistantResponse;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Welcome to the AI Assistant! How can I help you today?");
|
||||
const userId = "test-user-1234";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const userInput = await rl.question("You: ");
|
||||
if (userInput.toLowerCase() === "quit" || userInput.toLowerCase() === "exit") {
|
||||
rl.close();
|
||||
break;
|
||||
}
|
||||
const response = await runConversationTurn(userId, userInput);
|
||||
console.log(`Assistant: ${response}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "honcho-langgraph-example",
|
||||
"version": "1.0.0",
|
||||
"description": "LangGraph integration with Honcho for stateful conversational AI",
|
||||
"main": "main.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run main.ts",
|
||||
"dev": "bun --watch main.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"honcho",
|
||||
"langgraph",
|
||||
"openai",
|
||||
"chatbot",
|
||||
"ai"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@honcho-ai/sdk": "^1.5.0",
|
||||
"@langchain/langgraph": "^1.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"openai": "^6.9.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## What is Honcho?
|
||||
|
||||
Honcho is an infrastructure layer for building AI agents with social cognition and theory of mind capabilities. It enables personalized AI interactions by building coherent models of user psychology over time. The Honcho MCP server simplifies the integration to just 3 essential functions. Here's how to use them:
|
||||
Honcho is an infrastructure layer for building AI agents with memory and social cognition. It enables personalized AI interactions by building coherent models of user psychology over time. The Honcho MCP server simplifies the integration to just 3 essential functions. Here's how to use them:
|
||||
|
||||
### Step 1: Start New Conversation (First Message Only)
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ Before responding to any user message, you can query for personalization insight
|
|||
```text
|
||||
get_personalization_insights
|
||||
session_id: [SESSION_ID_FROM_STEP_1]
|
||||
query: [YOUR_THEORY_OF_MIND_QUESTION]
|
||||
query: [YOUR_QUESTION]
|
||||
```
|
||||
|
||||
This query takes a bit of time, so it's best to only perform it when you need personalized insights. If the query can be responded to effectively using what you already know about the user, just go ahead and answer it. However, the insights endpoint is extremely perceptive. It has the capability to reveal aspects of the user's personality, historical use of the application you are operating in, and more.
|
||||
|
|
@ -129,7 +129,7 @@ For subsequent messages in the same conversation:
|
|||
|
||||
## Best Practices for Personalization Queries
|
||||
|
||||
Ask theory-of-mind questions that reveal:
|
||||
Ask questions that reveal:
|
||||
|
||||
**Communication Style**: "How formal/casual should I be?" "What does this reveal about their preferences?"
|
||||
|
||||
|
|
@ -151,6 +151,6 @@ Ask theory-of-mind questions that reveal:
|
|||
1. **Always start with `start_conversation` for new conversations**
|
||||
2. **Store every message exchange with `add_turn`**
|
||||
3. **Use `get_personalization_insights` strategically for better responses**
|
||||
4. **Ask thoughtful theory-of-mind questions**
|
||||
4. **Ask thoughtful questions about `peer` representation**
|
||||
5. **Never expose technical details to the user**
|
||||
6. **The system maintains context automatically between sessions**
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ def run_migrations_online() -> None:
|
|||
connectable = engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
echo=True,
|
||||
echo=False,
|
||||
connect_args={
|
||||
"prepare_threshold": None,
|
||||
"options": "-c statement_timeout=300000", # 5 minutes in milliseconds
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ def constraint_exists(
|
|||
elif type == "primary":
|
||||
constraint = inspector.get_pk_constraint(table_name, schema=schema)
|
||||
return constraint["name"] == constraint_name
|
||||
elif type == "foreignkey":
|
||||
constraints = inspector.get_foreign_keys(table_name, schema=schema)
|
||||
else:
|
||||
raise ValueError(f"Invalid constraint type: {type}")
|
||||
return any(constraint["name"] == constraint_name for constraint in constraints)
|
||||
|
|
@ -151,9 +153,11 @@ def make_column_non_nullable_safe(table_name: str, column_name: str) -> None:
|
|||
)
|
||||
|
||||
# Step 5: Drop the redundant CHECK constraint
|
||||
op.drop_constraint(
|
||||
constraint_name,
|
||||
table_name,
|
||||
type_="check",
|
||||
schema=schema,
|
||||
conn.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
ALTER TABLE {quoted_schema}.{quoted_table}
|
||||
DROP CONSTRAINT {quoted_constraint}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -197,8 +197,12 @@ def upgrade() -> None:
|
|||
# Step 9: Update the collections table name_length check constraint from 512 to 1025
|
||||
print("Updating collections table name_length check constraint from 512 to 1025")
|
||||
|
||||
# Check for both old name and naming-convention-generated name
|
||||
if constraint_exists("collections", "name_length", "check"):
|
||||
op.drop_constraint("name_length", "collections", schema=schema)
|
||||
elif constraint_exists("collections", "ck_collections_name_length", "check"):
|
||||
op.drop_constraint("ck_collections_name_length", "collections", schema=schema)
|
||||
|
||||
op.create_check_constraint(
|
||||
"name_length", "collections", "length(name) <= 1025", schema=schema
|
||||
)
|
||||
|
|
@ -215,8 +219,12 @@ def downgrade() -> None:
|
|||
"Reverting collections table name_length check constraint from 1025 back to 512"
|
||||
)
|
||||
|
||||
# Check for both old name and naming-convention-generated name
|
||||
if constraint_exists("collections", "name_length", "check"):
|
||||
op.drop_constraint("name_length", "collections", schema=schema)
|
||||
elif constraint_exists("collections", "ck_collections_name_length", "check"):
|
||||
op.drop_constraint("ck_collections_name_length", "collections", schema=schema)
|
||||
|
||||
op.create_check_constraint(
|
||||
"name_length", "collections", "length(name) <= 512", schema=schema
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
"""rename_deriver_disabled_to_deriver_enabled
|
||||
|
||||
Revision ID: 110bdf470272
|
||||
Revises: baa22cad81e2
|
||||
Create Date: 2025-10-31 13:04:31.029856
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from migrations.utils import get_schema
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "110bdf470272"
|
||||
down_revision: str | None = "baa22cad81e2"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
schema = get_schema()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Convert deriver_disabled to deriver_enabled in configuration JSONB.
|
||||
|
||||
- deriver_disabled: true -> deriver_enabled: false
|
||||
- deriver_disabled: false -> deriver_enabled: true
|
||||
- Remove deriver_disabled key from configuration
|
||||
"""
|
||||
# Update sessions table in batches
|
||||
# Combine all cases in one pass: set deriver_enabled based on deriver_disabled value,
|
||||
# or just remove deriver_disabled if it's null or other value
|
||||
bind = op.get_bind()
|
||||
batch_size = 5000
|
||||
|
||||
while True:
|
||||
result = bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
WITH batch AS (
|
||||
SELECT id
|
||||
FROM "{schema}".sessions
|
||||
WHERE configuration ? 'deriver_disabled'
|
||||
LIMIT :batch_size
|
||||
)
|
||||
UPDATE "{schema}".sessions s
|
||||
SET configuration = CASE
|
||||
WHEN s.configuration->>'deriver_disabled' = 'true' THEN
|
||||
s.configuration - 'deriver_disabled' || jsonb_build_object('deriver_enabled', false)
|
||||
WHEN s.configuration->>'deriver_disabled' = 'false' THEN
|
||||
s.configuration - 'deriver_disabled' || jsonb_build_object('deriver_enabled', true)
|
||||
ELSE
|
||||
s.configuration - 'deriver_disabled'
|
||||
END
|
||||
FROM batch b
|
||||
WHERE s.id = b.id
|
||||
AND s.configuration ? 'deriver_disabled'
|
||||
"""
|
||||
),
|
||||
{"batch_size": batch_size},
|
||||
)
|
||||
rowcount = result.rowcount
|
||||
result.close()
|
||||
if rowcount == 0:
|
||||
break
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Convert deriver_enabled back to deriver_disabled in configuration JSONB.
|
||||
|
||||
- deriver_enabled: false -> deriver_disabled: true
|
||||
- deriver_enabled: true -> deriver_disabled: false
|
||||
- Remove deriver_enabled key from configuration
|
||||
"""
|
||||
# Update sessions table in batches
|
||||
# Combine all cases in one pass: set deriver_disabled based on deriver_enabled value,
|
||||
# or just remove deriver_enabled if it's null or other value
|
||||
bind = op.get_bind()
|
||||
batch_size = 5000
|
||||
|
||||
while True:
|
||||
result = bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
WITH batch AS (
|
||||
SELECT id
|
||||
FROM "{schema}".sessions
|
||||
WHERE configuration ? 'deriver_enabled'
|
||||
LIMIT :batch_size
|
||||
)
|
||||
UPDATE "{schema}".sessions s
|
||||
SET configuration = CASE
|
||||
WHEN s.configuration->>'deriver_enabled' = 'false' THEN
|
||||
s.configuration - 'deriver_enabled' || jsonb_build_object('deriver_disabled', true)
|
||||
WHEN s.configuration->>'deriver_enabled' = 'true' THEN
|
||||
s.configuration - 'deriver_enabled' || jsonb_build_object('deriver_disabled', false)
|
||||
ELSE
|
||||
s.configuration - 'deriver_enabled'
|
||||
END
|
||||
FROM batch b
|
||||
WHERE s.id = b.id
|
||||
AND s.configuration ? 'deriver_enabled'
|
||||
"""
|
||||
),
|
||||
{"batch_size": batch_size},
|
||||
)
|
||||
rowcount = result.rowcount
|
||||
result.close()
|
||||
if rowcount == 0:
|
||||
break
|
||||
|
|
@ -10,8 +10,9 @@ from collections.abc import Sequence
|
|||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from migrations.utils import constraint_exists
|
||||
from migrations.utils import constraint_exists, get_schema
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "20f89a421aff"
|
||||
|
|
@ -21,6 +22,9 @@ depends_on: str | Sequence[str] | None = None
|
|||
|
||||
|
||||
def upgrade() -> None:
|
||||
schema = get_schema()
|
||||
conn = op.get_bind()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"metamessages",
|
||||
|
|
@ -63,13 +67,38 @@ def upgrade() -> None:
|
|||
)
|
||||
|
||||
# Rename check constraint
|
||||
if constraint_exists("metamessages", "metamessage_type_length", "check"):
|
||||
op.execute("ALTER TABLE metamessages DROP CONSTRAINT metamessage_type_length;")
|
||||
op.create_check_constraint("label_length", "metamessages", "length(label) <= 512")
|
||||
# Handle both naming convention and non-convention names for backward compatibility
|
||||
if constraint_exists(
|
||||
"metamessages", "ck_metamessages_metamessage_type_length", "check"
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f"ALTER TABLE {schema}.metamessages DROP CONSTRAINT ck_metamessages_metamessage_type_length"
|
||||
)
|
||||
)
|
||||
elif constraint_exists("metamessages", "metamessage_type_length", "check"):
|
||||
conn.execute(
|
||||
text(
|
||||
f"ALTER TABLE {schema}.metamessages DROP CONSTRAINT metamessage_type_length"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.metamessages
|
||||
ADD CONSTRAINT label_length
|
||||
CHECK (length(label) <= 512)
|
||||
"""
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
schema = get_schema()
|
||||
conn = op.get_bind()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"metamessages",
|
||||
|
|
@ -112,10 +141,16 @@ def downgrade() -> None:
|
|||
)
|
||||
|
||||
# Revert check constraint rename
|
||||
op.execute("ALTER TABLE metamessages DROP CONSTRAINT label_length;")
|
||||
op.create_check_constraint(
|
||||
"metamessage_type_length",
|
||||
"metamessages",
|
||||
"length(metamessage_type) <= 512",
|
||||
conn.execute(
|
||||
text(f"ALTER TABLE {schema}.metamessages DROP CONSTRAINT label_length")
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.metamessages
|
||||
ADD CONSTRAINT metamessage_type_length
|
||||
CHECK (length(metamessage_type) <= 512)
|
||||
"""
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from collections.abc import Sequence
|
|||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from migrations.utils import constraint_exists, get_schema
|
||||
|
||||
|
|
@ -27,13 +28,18 @@ def upgrade() -> None:
|
|||
connection = op.get_bind()
|
||||
inspector = sa.inspect(connection)
|
||||
|
||||
# Drop CHECK constraint for level
|
||||
# Drop CHECK constraint for level (check both naming convention and plain names)
|
||||
# On old databases: "level_valid"
|
||||
# On new databases with naming convention: "ck_documents_level_valid"
|
||||
if constraint_exists("documents", "level_valid", "check", inspector):
|
||||
op.drop_constraint(
|
||||
"level_valid",
|
||||
"documents",
|
||||
type_="check",
|
||||
schema=schema,
|
||||
connection.execute(
|
||||
text(f'ALTER TABLE {schema}.documents DROP CONSTRAINT "level_valid"')
|
||||
)
|
||||
elif constraint_exists("documents", "ck_documents_level_valid", "check", inspector):
|
||||
connection.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.documents DROP CONSTRAINT "ck_documents_level_valid"'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -66,11 +72,14 @@ def downgrade() -> None:
|
|||
if result.rowcount == 0:
|
||||
break
|
||||
|
||||
# Recreate CHECK constraint for level
|
||||
# Recreate CHECK constraint for level (use plain name to match original migration)
|
||||
if not constraint_exists("documents", "level_valid", "check", inspector):
|
||||
op.create_check_constraint(
|
||||
"level_valid",
|
||||
"documents",
|
||||
"level IN ('explicit', 'deductive')",
|
||||
schema=schema,
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.documents
|
||||
ADD CONSTRAINT level_valid
|
||||
CHECK (level IN ('explicit', 'deductive'))
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -222,10 +222,14 @@ def upgrade() -> None:
|
|||
)
|
||||
|
||||
if not constraint_exists:
|
||||
op.create_check_constraint(
|
||||
"message_requires_session",
|
||||
"metamessages",
|
||||
"(message_id IS NULL) OR (session_id IS NOT NULL)",
|
||||
conn.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.metamessages
|
||||
ADD CONSTRAINT message_requires_session
|
||||
CHECK ((message_id IS NULL) OR (session_id IS NOT NULL))
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("Created message_requires_session constraint")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -100,11 +100,14 @@ def upgrade() -> None:
|
|||
|
||||
# Step 6: Add CHECK constraint for level
|
||||
if not constraint_exists("documents", "level_valid", "check", inspector):
|
||||
op.create_check_constraint(
|
||||
"level_valid",
|
||||
"documents",
|
||||
"level IN ('explicit', 'deductive')",
|
||||
schema=schema,
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.documents
|
||||
ADD CONSTRAINT level_valid
|
||||
CHECK (level IN ('explicit', 'deductive'))
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -115,11 +118,8 @@ def downgrade() -> None:
|
|||
|
||||
# Step 1: Drop CHECK constraint for level
|
||||
if constraint_exists("documents", "level_valid", "check", inspector):
|
||||
op.drop_constraint(
|
||||
"level_valid",
|
||||
"documents",
|
||||
type_="check",
|
||||
schema=schema,
|
||||
connection.execute(
|
||||
text(f"ALTER TABLE {schema}.documents DROP CONSTRAINT level_valid")
|
||||
)
|
||||
|
||||
# Step 2: Copy level and times_derived back to internal_metadata in batches (optional, for safety)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,525 @@
|
|||
"""standardize_constraint_names
|
||||
|
||||
Revision ID: baa22cad81e2
|
||||
Revises: 29ade7350c19
|
||||
Create Date: 2025-11-15 01:16:40.937103
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from migrations.utils import constraint_exists, get_schema, index_exists
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "baa22cad81e2"
|
||||
down_revision: str | None = "29ade7350c19"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
schema = get_schema()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# ============================================================
|
||||
# CONSTRAINT RENAMES (using raw SQL)
|
||||
# ============================================================
|
||||
|
||||
# Unique Constraints
|
||||
if constraint_exists(
|
||||
"active_queue_sessions", "unique_work_unit_key", "unique", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.active_queue_sessions RENAME CONSTRAINT "unique_work_unit_key" TO "uq_active_queue_sessions_work_unit_key"'
|
||||
)
|
||||
)
|
||||
|
||||
if constraint_exists(
|
||||
"collections", "unique_observer_observed_collection", "unique", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.collections RENAME CONSTRAINT "unique_observer_observed_collection" TO "uq_collections_observer_observed_workspace_name"'
|
||||
)
|
||||
)
|
||||
|
||||
if constraint_exists("peers", "unique_name_workspace_peer", "unique", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.peers RENAME CONSTRAINT "unique_name_workspace_peer" TO "uq_peers_name_workspace_name"'
|
||||
)
|
||||
)
|
||||
|
||||
if constraint_exists("sessions", "unique_session_name", "unique", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.sessions RENAME CONSTRAINT "unique_session_name" TO "uq_sessions_name_workspace_name"'
|
||||
)
|
||||
)
|
||||
|
||||
if constraint_exists("messages", "uq_messages_session_seq", "unique", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.messages RENAME CONSTRAINT "uq_messages_session_seq" TO "uq_messages_workspace_name_session_name_seq_in_session"'
|
||||
)
|
||||
)
|
||||
|
||||
if constraint_exists("workspaces", "uq_apps_name", "unique", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.workspaces RENAME CONSTRAINT "uq_apps_name" TO "uq_workspaces_name"'
|
||||
)
|
||||
)
|
||||
|
||||
# Foreign Keys
|
||||
if constraint_exists(
|
||||
"message_embeddings",
|
||||
"message_embeddings_message_id_fkey",
|
||||
"foreignkey",
|
||||
inspector,
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.message_embeddings RENAME CONSTRAINT "message_embeddings_message_id_fkey" TO "fk_message_embeddings_message_id_messages"'
|
||||
)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# INDEX RENAMES (using raw SQL)
|
||||
# ============================================================
|
||||
|
||||
# Refresh inspector after constraint renames to ensure we see current state
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# Collections
|
||||
if index_exists("collections", "idx_collections_observed", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_collections_observed" RENAME TO "ix_collections_observed"'
|
||||
)
|
||||
)
|
||||
if index_exists("collections", "idx_collections_observer", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_collections_observer" RENAME TO "ix_collections_observer"'
|
||||
)
|
||||
)
|
||||
|
||||
# Documents
|
||||
if index_exists("documents", "idx_documents_embedding_hnsw", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_documents_embedding_hnsw" RENAME TO "ix_documents_embedding_hnsw"'
|
||||
)
|
||||
)
|
||||
if index_exists("documents", "idx_documents_observed", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_documents_observed" RENAME TO "ix_documents_observed"'
|
||||
)
|
||||
)
|
||||
if index_exists("documents", "idx_documents_observer", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_documents_observer" RENAME TO "ix_documents_observer"'
|
||||
)
|
||||
)
|
||||
if index_exists("documents", "idx_documents_session_name", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_documents_session_name" RENAME TO "ix_documents_session_name"'
|
||||
)
|
||||
)
|
||||
|
||||
# Message Embeddings
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_created_at", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_created_at" RENAME TO "ix_message_embeddings_created_at"'
|
||||
)
|
||||
)
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_embedding_hnsw", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_embedding_hnsw" RENAME TO "ix_message_embeddings_embedding_hnsw"'
|
||||
)
|
||||
)
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_message_id", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_message_id" RENAME TO "ix_message_embeddings_message_id"'
|
||||
)
|
||||
)
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_peer_name", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_peer_name" RENAME TO "ix_message_embeddings_peer_name"'
|
||||
)
|
||||
)
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_session_name", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_session_name" RENAME TO "ix_message_embeddings_session_name"'
|
||||
)
|
||||
)
|
||||
if index_exists(
|
||||
"message_embeddings", "idx_message_embeddings_workspace_name", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_message_embeddings_workspace_name" RENAME TO "ix_message_embeddings_workspace_name"'
|
||||
)
|
||||
)
|
||||
|
||||
# Messages
|
||||
if index_exists("messages", "idx_messages_content_gin", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_messages_content_gin" RENAME TO "ix_messages_content_gin"'
|
||||
)
|
||||
)
|
||||
if index_exists("messages", "idx_messages_session_lookup", inspector):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_messages_session_lookup" RENAME TO "ix_messages_session_lookup"'
|
||||
)
|
||||
)
|
||||
|
||||
# Webhook Endpoints
|
||||
if index_exists(
|
||||
"webhook_endpoints", "idx_webhook_endpoints_workspace_lookup", inspector
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."idx_webhook_endpoints_workspace_lookup" RENAME TO "ix_webhook_endpoints_workspace_name"'
|
||||
)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# INDEX DELETIONS (redundant indexes being removed)
|
||||
# ============================================================
|
||||
|
||||
# Drop redundant indexes on primary keys and unique columns
|
||||
if index_exists("messages", "ix_messages_id", inspector):
|
||||
op.drop_index("ix_messages_id", table_name="messages", schema=schema)
|
||||
if index_exists("messages", "ix_messages_public_id", inspector):
|
||||
op.drop_index("ix_messages_public_id", table_name="messages", schema=schema)
|
||||
if index_exists("peers", "ix_peers_name", inspector):
|
||||
op.drop_index("ix_peers_name", table_name="peers", schema=schema)
|
||||
if index_exists("peers", "idx_peers_workspace_lookup", inspector):
|
||||
op.drop_index("idx_peers_workspace_lookup", table_name="peers", schema=schema)
|
||||
if index_exists("workspaces", "ix_workspaces_name", inspector):
|
||||
op.drop_index("ix_workspaces_name", table_name="workspaces", schema=schema)
|
||||
|
||||
# Drop redundant index on active_queue_sessions.work_unit_key (unique constraint auto-creates index)
|
||||
if index_exists(
|
||||
"active_queue_sessions", "ix_active_queue_sessions_work_unit_key", inspector
|
||||
):
|
||||
op.drop_index(
|
||||
"ix_active_queue_sessions_work_unit_key",
|
||||
table_name="active_queue_sessions",
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# Drop old queue indexes that will be replaced
|
||||
if index_exists("queue", "ix_queue_workspace_name_processed", inspector):
|
||||
op.drop_index(
|
||||
"ix_queue_workspace_name_processed", table_name="queue", schema=schema
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# NEW INDEXES (being created, not renamed)
|
||||
# ============================================================
|
||||
|
||||
# Queue - new indexes
|
||||
if not index_exists("queue", "ix_queue_processed", inspector):
|
||||
op.create_index(
|
||||
"ix_queue_processed", "queue", ["processed"], unique=False, schema=schema
|
||||
)
|
||||
if not index_exists("queue", "ix_queue_work_unit_key_processed_id", inspector):
|
||||
op.create_index(
|
||||
"ix_queue_work_unit_key_processed_id",
|
||||
"queue",
|
||||
["work_unit_key", "processed", "id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# Sessions - new index
|
||||
if not index_exists("sessions", "ix_sessions_workspace_name", inspector):
|
||||
op.create_index(
|
||||
"ix_sessions_workspace_name",
|
||||
"sessions",
|
||||
["workspace_name"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
|
||||
# Messages - drop FK that's being removed
|
||||
if constraint_exists(
|
||||
"messages", "fk_messages_workspace_name_workspaces", "foreignkey", inspector
|
||||
):
|
||||
op.drop_constraint(
|
||||
"fk_messages_workspace_name_workspaces",
|
||||
"messages",
|
||||
schema=schema,
|
||||
type_="foreignkey",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# FIX FK CASCADE (message_embeddings -> messages)
|
||||
# ============================================================
|
||||
|
||||
# Refresh inspector to get latest state after potential renames
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# Drop and recreate FK with ON DELETE CASCADE
|
||||
# This FK should exist at this point either from:
|
||||
# 1. Being renamed from old name "message_embeddings_message_id_fkey", or
|
||||
# 2. Already having the new name "fk_message_embeddings_message_id_messages" (from main branch)
|
||||
if constraint_exists(
|
||||
"message_embeddings",
|
||||
"fk_message_embeddings_message_id_messages",
|
||||
"foreignkey",
|
||||
inspector,
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f"ALTER TABLE {schema}.message_embeddings DROP CONSTRAINT fk_message_embeddings_message_id_messages"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.message_embeddings
|
||||
ADD CONSTRAINT fk_message_embeddings_message_id_messages
|
||||
FOREIGN KEY (message_id) REFERENCES {schema}.messages(public_id)
|
||||
ON DELETE CASCADE
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
# ============================================================
|
||||
# REVERSE: FIX FK CASCADE (message_embeddings -> messages)
|
||||
# ============================================================
|
||||
|
||||
# Drop FK with CASCADE and recreate without CASCADE
|
||||
if constraint_exists(
|
||||
"message_embeddings",
|
||||
"fk_message_embeddings_message_id_messages",
|
||||
"foreignkey",
|
||||
inspector,
|
||||
):
|
||||
conn.execute(
|
||||
text(
|
||||
f"ALTER TABLE {schema}.message_embeddings DROP CONSTRAINT fk_message_embeddings_message_id_messages"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f"""
|
||||
ALTER TABLE {schema}.message_embeddings
|
||||
ADD CONSTRAINT fk_message_embeddings_message_id_messages
|
||||
FOREIGN KEY (message_id) REFERENCES {schema}.messages(public_id)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# REVERSE: NEW INDEXES AND FKS
|
||||
# ============================================================
|
||||
|
||||
# Recreate FK
|
||||
op.create_foreign_key(
|
||||
"fk_messages_workspace_name_workspaces",
|
||||
"messages",
|
||||
"workspaces",
|
||||
["workspace_name"],
|
||||
["name"],
|
||||
source_schema=schema,
|
||||
referent_schema=schema,
|
||||
)
|
||||
|
||||
# Drop new indexes
|
||||
op.drop_index("ix_sessions_workspace_name", table_name="sessions", schema=schema)
|
||||
op.drop_index("ix_queue_processed", table_name="queue", schema=schema)
|
||||
|
||||
# ============================================================
|
||||
# REVERSE: INDEX DELETIONS (recreate them)
|
||||
# ============================================================
|
||||
|
||||
# Recreate redundant index on active_queue_sessions.work_unit_key
|
||||
op.create_index(
|
||||
"ix_active_queue_sessions_work_unit_key",
|
||||
"active_queue_sessions",
|
||||
["work_unit_key"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_queue_workspace_name_processed",
|
||||
"queue",
|
||||
["workspace_name", "processed"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_workspaces_name", "workspaces", ["name"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
"idx_peers_workspace_lookup",
|
||||
"peers",
|
||||
["workspace_name", "name"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index("ix_peers_name", "peers", ["name"], unique=False, schema=schema)
|
||||
op.create_index(
|
||||
"ix_messages_public_id", "messages", ["public_id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index("ix_messages_id", "messages", ["id"], unique=False, schema=schema)
|
||||
|
||||
# ============================================================
|
||||
# REVERSE: INDEX RENAMES (using raw SQL)
|
||||
# ============================================================
|
||||
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_webhook_endpoints_workspace_name" RENAME TO "idx_webhook_endpoints_workspace_lookup"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_messages_session_lookup" RENAME TO "idx_messages_session_lookup"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_messages_content_gin" RENAME TO "idx_messages_content_gin"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_workspace_name" RENAME TO "idx_message_embeddings_workspace_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_session_name" RENAME TO "idx_message_embeddings_session_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_peer_name" RENAME TO "idx_message_embeddings_peer_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_message_id" RENAME TO "idx_message_embeddings_message_id"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_embedding_hnsw" RENAME TO "idx_message_embeddings_embedding_hnsw"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_message_embeddings_created_at" RENAME TO "idx_message_embeddings_created_at"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_documents_session_name" RENAME TO "idx_documents_session_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_documents_observer" RENAME TO "idx_documents_observer"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_documents_observed" RENAME TO "idx_documents_observed"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_documents_embedding_hnsw" RENAME TO "idx_documents_embedding_hnsw"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_collections_observer" RENAME TO "idx_collections_observer"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER INDEX {schema}."ix_collections_observed" RENAME TO "idx_collections_observed"'
|
||||
)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# REVERSE: CONSTRAINT RENAMES
|
||||
# ============================================================
|
||||
|
||||
# Foreign Keys
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.message_embeddings RENAME CONSTRAINT "fk_message_embeddings_message_id_messages" TO "message_embeddings_message_id_fkey"'
|
||||
)
|
||||
)
|
||||
|
||||
# Unique Constraints
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.workspaces RENAME CONSTRAINT "uq_workspaces_name" TO "uq_apps_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.messages RENAME CONSTRAINT "uq_messages_workspace_name_session_name_seq_in_session" TO "uq_messages_session_seq"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.sessions RENAME CONSTRAINT "uq_sessions_name_workspace_name" TO "unique_session_name"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.peers RENAME CONSTRAINT "uq_peers_name_workspace_name" TO "unique_name_workspace_peer"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.collections RENAME CONSTRAINT "uq_collections_observer_observed_workspace_name" TO "unique_observer_observed_collection"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
f'ALTER TABLE {schema}.active_queue_sessions RENAME CONSTRAINT "uq_active_queue_sessions_work_unit_key" TO "unique_work_unit_key"'
|
||||
)
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.4.2"
|
||||
version = "2.5.0"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
@ -50,6 +50,7 @@ dev-dependencies = [
|
|||
"pytest-cov>=6.2.1",
|
||||
"honcho-ai",
|
||||
"fakeredis>=2.32.0",
|
||||
"scipy>=1.15.3",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,20 @@ 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.6.0] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
|
||||
- Session Clone methods
|
||||
- Peer level get_context method
|
||||
- `ObservationScope` object to perform CRUD operations on observations
|
||||
- Representation object for WorkingRepresentations
|
||||
|
||||
### Changed
|
||||
|
||||
- methods that take IDs, can all optionally take an object of the same type
|
||||
|
||||
## [1.5.0] - 2025-10-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ response = alice.chat("what did alice have for breakfast today?")
|
|||
print("response returned:", response)
|
||||
|
||||
# Chat with alice in the session
|
||||
response = alice.chat("what did alice have for breakfast today?", session_id=session.id)
|
||||
response = alice.chat("what did alice have for breakfast today?", session=session.id)
|
||||
print("response returned:", response)
|
||||
|
||||
# Chat with alice in the session with a target
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ session = honcho.session("file_upload_test_" + str(uuid.uuid4()))
|
|||
|
||||
# Upload the current file directly using a file object
|
||||
with open(__file__, "rb") as file:
|
||||
session.upload_file(file, peer_id="alice")
|
||||
session.upload_file(file, peer="alice")
|
||||
|
||||
# get the messages from the session
|
||||
# should contain the contents of this file!
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import random
|
||||
import uuid
|
||||
|
||||
from honcho import Honcho
|
||||
|
||||
# Create a Honcho client with the default workspace
|
||||
honcho = Honcho(environment="local")
|
||||
|
||||
peers = [
|
||||
honcho.peer("alice"),
|
||||
honcho.peer("bob"),
|
||||
honcho.peer("charlie"),
|
||||
]
|
||||
|
||||
# Create a new session
|
||||
session = honcho.session("context_test_" + str(uuid.uuid4()))
|
||||
|
||||
# Generate some random messages from alice, bob, and charlie and add them to the session
|
||||
messages = []
|
||||
for i in range(10):
|
||||
random_peer = random.choice(peers)
|
||||
messages.append(
|
||||
random_peer.message(f"Hello from {random_peer}! This is message {i}.")
|
||||
)
|
||||
|
||||
session.add_messages(messages)
|
||||
|
||||
alice = peers[0]
|
||||
bob = peers[1]
|
||||
|
||||
# Get alice's working representation in the session
|
||||
representation = session.working_rep(alice)
|
||||
print("working representation returned:", representation)
|
||||
|
||||
# Get alice's working representation *of bob* in the session
|
||||
representation = session.working_rep(alice, target=bob)
|
||||
print("working representation returned:", representation)
|
||||
|
|
@ -60,7 +60,7 @@ async def get_summaries_async():
|
|||
async_client = AsyncHoncho(api_key=api_key)
|
||||
|
||||
# Get a session
|
||||
async_session = async_client.session("my-conversation-session")
|
||||
async_session = await async_client.session("my-conversation-session")
|
||||
|
||||
# Get summaries asynchronously
|
||||
summaries = await async_session.get_summaries()
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ print(
|
|||
"\n\n\033[1m asking bob what alice had for breakfast -- scoped to session 1 \033[0m"
|
||||
)
|
||||
response = bob.chat(
|
||||
"what did alice have for breakfast today?", target=alice, session_id=session.id
|
||||
"what did alice have for breakfast today?", target=alice, session=session.id
|
||||
)
|
||||
print("response:", response)
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ print(
|
|||
"\n\n\033[1m asking bob what alice had for breakfast -- scoped to session 2 \033[0m"
|
||||
)
|
||||
response = bob.chat(
|
||||
"what did alice have for breakfast today?", target=alice, session_id=session2.id
|
||||
"what did alice have for breakfast today?", target=alice, session=session2.id
|
||||
)
|
||||
print("response:", response)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho-ai"
|
||||
version = "1.5.0"
|
||||
version = "1.6.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.5.1",
|
||||
"honcho-core>=1.6.1",
|
||||
"httpx>=0.28.0, <1",
|
||||
"pydantic>=2.0.0, <3",
|
||||
"typing-extensions>=4.12.0; python_version < \"3.12\"",
|
||||
|
|
|
|||
|
|
@ -40,28 +40,45 @@ from .async_client import (
|
|||
AsyncPeer,
|
||||
AsyncSession,
|
||||
)
|
||||
from .base import PeerBase, SessionBase
|
||||
from .client import Honcho
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
from .pagination import SyncPage
|
||||
from .peer import Peer
|
||||
from .session import Session
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .types import DialecticStreamResponse
|
||||
from .types import (
|
||||
DeductiveObservation,
|
||||
DialecticStreamResponse,
|
||||
ExplicitObservation,
|
||||
PeerContext,
|
||||
Representation,
|
||||
)
|
||||
|
||||
__version__ = "1.5.0"
|
||||
__version__ = "1.6.0"
|
||||
__author__ = "Plastic Labs"
|
||||
__email__ = "hello@plasticlabs.ai"
|
||||
|
||||
__all__ = [
|
||||
"AsyncHoncho",
|
||||
"AsyncObservationScope",
|
||||
"AsyncPeer",
|
||||
"AsyncSession",
|
||||
"AsyncPage",
|
||||
"Honcho",
|
||||
"Observation",
|
||||
"ObservationScope",
|
||||
"Peer",
|
||||
"PeerBase",
|
||||
"PeerContext",
|
||||
"Session",
|
||||
"SessionBase",
|
||||
"SessionContext",
|
||||
"SessionSummaries",
|
||||
"Summary",
|
||||
"SyncPage",
|
||||
"DialecticStreamResponse",
|
||||
"Representation",
|
||||
"ExplicitObservation",
|
||||
"DeductiveObservation",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from honcho_core.types.workspaces.session import Session as SessionCore
|
|||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..base import PeerBase, SessionBase
|
||||
from .pagination import AsyncPage
|
||||
from .peer import AsyncPeer
|
||||
from .session import AsyncSession
|
||||
|
|
@ -33,9 +34,11 @@ class AsyncHoncho(BaseModel):
|
|||
`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
|
||||
metadata: Cached metadata for this workspace. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this workspace. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
|
|
@ -46,8 +49,20 @@ class AsyncHoncho(BaseModel):
|
|||
min_length=1,
|
||||
description="Workspace ID for scoping operations",
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this workspace. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this workspace. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def core(self) -> AsyncHonchoCore:
|
||||
"""
|
||||
|
|
@ -218,7 +233,14 @@ class AsyncHoncho(BaseModel):
|
|||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return AsyncPage(
|
||||
peers_page, lambda peer: AsyncPeer(peer.id, self.workspace_id, self._client)
|
||||
peers_page,
|
||||
lambda peer: AsyncPeer(
|
||||
peer.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=peer.metadata,
|
||||
config=peer.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -283,7 +305,13 @@ class AsyncHoncho(BaseModel):
|
|||
)
|
||||
return AsyncPage(
|
||||
sessions_page,
|
||||
lambda session: AsyncSession(session.id, self.workspace_id, self._client),
|
||||
lambda session: AsyncSession(
|
||||
session.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=session.metadata,
|
||||
config=session.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
|
|
@ -292,14 +320,16 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
Makes an async 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.
|
||||
key-value data associated with the workspace. This method also updates the
|
||||
cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's metadata. Returns an empty
|
||||
dictionary if no metadata is set
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
return workspace.metadata or {}
|
||||
self._metadata = workspace.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -311,12 +341,64 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with the current workspace.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for the current workspace.
|
||||
|
||||
Makes an async API call to retrieve configuration associated with the current workspace.
|
||||
Configuration includes settings that control workspace behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's configuration. Returns an empty
|
||||
dictionary if no configuration is set
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._configuration = workspace.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for the current workspace.
|
||||
|
||||
Makes an async API call to update the configuration associated with the current workspace.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.update(
|
||||
self.workspace_id, configuration=configuration
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for the current workspace.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with the current workspace and updates the cached attributes.
|
||||
"""
|
||||
workspace = await self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._metadata = workspace.metadata or {}
|
||||
self._configuration = workspace.configuration or {}
|
||||
|
||||
async def get_workspaces(
|
||||
self, filters: dict[str, object] | None = None
|
||||
|
|
@ -389,29 +471,50 @@ class AsyncHoncho(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
"""
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=session_id,
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -426,9 +529,9 @@ class AsyncHoncho(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
session_id: Optional session ID to scope the status check
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
session: Optional session (ID string or AsyncSession object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -442,9 +545,7 @@ class AsyncHoncho(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = await self.get_deriver_status(
|
||||
observer_id, sender_id, session_id
|
||||
)
|
||||
status = await self.get_deriver_status(observer, sender, session)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
@ -497,10 +598,9 @@ class AsyncHoncho(BaseModel):
|
|||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
session: str | SessionBase | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
description="The session (ID string or Session object) - required if message is a string ID",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
|
|
@ -511,7 +611,7 @@ class AsyncHoncho(BaseModel):
|
|||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
session: The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
|
@ -524,9 +624,9 @@ class AsyncHoncho(BaseModel):
|
|||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
if not session:
|
||||
raise ValueError("session is required when message is a string ID")
|
||||
resolved_session_id = session if isinstance(session, str) else session.id
|
||||
|
||||
return await self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
|
|
|
|||
|
|
@ -1,25 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types.workspaces import PeerCardResponse
|
||||
from honcho_core.types.workspaces.peer_working_representation_response import (
|
||||
PeerWorkingRepresentationResponse,
|
||||
)
|
||||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..base import PeerBase, SessionBase
|
||||
from ..types import DialecticStreamResponse
|
||||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .session import AsyncSession
|
||||
from ..observations import AsyncObservationScope
|
||||
from ..types import PeerContext, Representation
|
||||
|
||||
from .session import AsyncSession
|
||||
|
||||
|
||||
class AsyncPeer(BaseModel):
|
||||
class AsyncPeer(PeerBase):
|
||||
"""
|
||||
Represents a peer in the Honcho system with async operations.
|
||||
|
||||
|
|
@ -29,15 +37,27 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
_client: Reference to the parent AsyncHoncho client instance
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this peer. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this peer. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this peer")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this peer. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this peer. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -52,6 +72,9 @@ class AsyncPeer(BaseModel):
|
|||
client: AsyncHonchoCore = Field(
|
||||
..., description="Reference to the parent AsyncHoncho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new AsyncPeer.
|
||||
|
|
@ -60,9 +83,16 @@ class AsyncPeer(BaseModel):
|
|||
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 to initialize the cached value
|
||||
config: Optional configuration to initialize the cached value
|
||||
"""
|
||||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
super().__init__(
|
||||
id=peer_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
|
|
@ -92,25 +122,30 @@ class AsyncPeer(BaseModel):
|
|||
Returns:
|
||||
A new AsyncPeer instance
|
||||
"""
|
||||
peer = cls(peer_id, workspace_id, client)
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.peers.get_or_create(
|
||||
peer_data = await client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
return cls(
|
||||
peer_id,
|
||||
workspace_id,
|
||||
client,
|
||||
metadata=peer_data.metadata,
|
||||
config=peer_data.configuration,
|
||||
)
|
||||
|
||||
return peer
|
||||
return cls(peer_id, workspace_id, client)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
stream: bool = False,
|
||||
target: str | AsyncPeer | None = None,
|
||||
session_id: str | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
|
@ -124,14 +159,28 @@ class AsyncPeer(BaseModel):
|
|||
stream: Whether to stream the response
|
||||
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
|
||||
session_id: Optional session ID to scope the query to a specific session.
|
||||
If provided, only information from that session is considered
|
||||
querying the peer's global representation. Can be a peer ID string
|
||||
or an AsyncPeer object.
|
||||
session: Optional session to scope the query to. If provided, only
|
||||
information from that session is considered. Can be a session
|
||||
ID string or an AsyncSession object.
|
||||
|
||||
Returns:
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
# Extract IDs from objects if needed
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
if stream:
|
||||
|
||||
async def stream_response() -> AsyncGenerator[str]:
|
||||
|
|
@ -143,8 +192,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
async for line in response.iter_lines():
|
||||
|
|
@ -168,8 +217,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=stream,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
# "If the context provided doesn't help address the query, write absolutely NOTHING but "None""
|
||||
if response.content in ("", None, "None"):
|
||||
|
|
@ -208,6 +257,10 @@ class AsyncPeer(BaseModel):
|
|||
..., min_length=1, description="The text content for the message"
|
||||
),
|
||||
*,
|
||||
config: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the message",
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
|
|
@ -238,6 +291,7 @@ class AsyncPeer(BaseModel):
|
|||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
configuration=config,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
|
@ -248,7 +302,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an async 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.
|
||||
associated with the peer. This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's metadata. Returns an empty dictionary
|
||||
|
|
@ -258,7 +312,8 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.metadata or {}
|
||||
self._metadata = peer.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -272,6 +327,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with this peer.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this peer.
|
||||
|
|
@ -282,13 +338,15 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_peer_config(self) -> dict[str, object]:
|
||||
async def get_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`.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
|
|
@ -297,10 +355,11 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_peer_config(
|
||||
async def set_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
|
|
@ -313,6 +372,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
|
|
@ -323,6 +383,51 @@ class AsyncPeer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
self._configuration = config
|
||||
|
||||
async def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`get_config` instead.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
return await self.get_config()
|
||||
|
||||
@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.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`set_config` instead.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer
|
||||
"""
|
||||
return await self.set_config(config)
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this peer.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with this peer and updates the cached attributes.
|
||||
"""
|
||||
peer = await self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
async def search(
|
||||
|
|
@ -359,7 +464,7 @@ class AsyncPeer(BaseModel):
|
|||
|
||||
async def card(
|
||||
self,
|
||||
target: str | AsyncPeer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
|
@ -379,10 +484,15 @@ class AsyncPeer(BaseModel):
|
|||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
response: PeerCardResponse = await self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
target=target_id,
|
||||
)
|
||||
|
||||
if response.peer_card is None:
|
||||
|
|
@ -391,6 +501,221 @@ class AsyncPeer(BaseModel):
|
|||
items: list[str] = response.peer_card
|
||||
return "\n".join(items)
|
||||
|
||||
async def working_rep(
|
||||
self,
|
||||
session: str | SessionBase | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get a working representation for this peer.
|
||||
|
||||
Args:
|
||||
session: Optional session to scope the representation to.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
returns the representation of the target from the perspective of this peer.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get global representation
|
||||
rep = await peer.working_rep()
|
||||
print(rep)
|
||||
|
||||
# Get representation scoped to a session
|
||||
session_rep = await peer.working_rep(session='session-123')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = await peer.working_rep(
|
||||
search_query='preferences',
|
||||
search_top_k=10,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import Representation as _Representation
|
||||
|
||||
session_id = (
|
||||
None
|
||||
if session is None
|
||||
else session
|
||||
if isinstance(session, str)
|
||||
else session.id
|
||||
)
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
data: PeerWorkingRepresentationResponse = (
|
||||
await self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations
|
||||
if max_observations is not None
|
||||
else omit,
|
||||
)
|
||||
)
|
||||
representation = data.get("representation")
|
||||
if representation is not None:
|
||||
return _Representation.from_dict(cast(dict[str, object], representation))
|
||||
else:
|
||||
return _Representation.from_dict(data)
|
||||
|
||||
async def get_context(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "PeerContext":
|
||||
"""
|
||||
Get context for this peer, including representation and peer card.
|
||||
|
||||
This is a convenience method that retrieves both the working representation
|
||||
and peer card in a single API call.
|
||||
|
||||
Args:
|
||||
target: Optional target peer to get context for. If provided, returns
|
||||
the context for the target from this peer's perspective.
|
||||
Can be an AsyncPeer object or peer ID string.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A PeerContext object containing the representation and peer card
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get own context
|
||||
context = await peer.get_context()
|
||||
print(context.representation)
|
||||
print(context.peer_card)
|
||||
|
||||
# Get context for another peer
|
||||
context = await peer.get_context(target='other-peer-id')
|
||||
|
||||
# Get context with semantic search
|
||||
context = await peer.get_context(
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import PeerContext as _PeerContext
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
response = await self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
return _PeerContext.from_api_response(response)
|
||||
|
||||
@property
|
||||
def observations(self) -> "AsyncObservationScope":
|
||||
"""
|
||||
Access this peer's self-observations (where observer == observed == self).
|
||||
|
||||
This property provides a convenient way to access observations that this peer
|
||||
has made about themselves. Use this for self-observation scenarios.
|
||||
|
||||
Returns:
|
||||
An AsyncObservationScope scoped to this peer's self-observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# List self-observations
|
||||
obs_list = await peer.observations.list()
|
||||
|
||||
# Search self-observations
|
||||
results = await peer.observations.query("preferences")
|
||||
|
||||
# Delete a self-observation
|
||||
await peer.observations.delete("obs-123")
|
||||
```
|
||||
"""
|
||||
from ..observations import AsyncObservationScope as _AsyncObservationScope
|
||||
|
||||
return _AsyncObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | PeerBase) -> "AsyncObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
This method provides scoped access to observations where this peer is the
|
||||
observer and the target is the observed peer.
|
||||
|
||||
Args:
|
||||
target: The target peer (either an AsyncPeer object or peer ID string)
|
||||
|
||||
Returns:
|
||||
An AsyncObservationScope scoped to this peer's observations of the target
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
|
||||
# List observations
|
||||
obs_list = await bob_observations.list()
|
||||
|
||||
# Search observations
|
||||
results = await bob_observations.query("work history")
|
||||
|
||||
# Get the representation from these observations
|
||||
rep = await bob_observations.get_representation()
|
||||
```
|
||||
"""
|
||||
from ..observations import AsyncObservationScope as _AsyncObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, PeerBase) else target
|
||||
return _AsyncObservationScope(
|
||||
self._client, self.workspace_id, self.id, target_id
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the AsyncPeer.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from honcho_core import AsyncHoncho as AsyncHonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from ..base import PeerBase, SessionBase
|
||||
from ..session_context import SessionContext, SessionSummaries, Summary
|
||||
from ..utils import prepare_file_for_upload
|
||||
from .pagination import AsyncPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types import Representation
|
||||
from .peer import AsyncPeer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -33,7 +38,7 @@ class SessionPeerConfig(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class AsyncSession(BaseModel):
|
||||
class AsyncSession(SessionBase):
|
||||
"""
|
||||
Represents a session in Honcho with async operations.
|
||||
|
||||
|
|
@ -43,17 +48,27 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
_client: Reference to the parent AsyncHoncho client instance
|
||||
anonymous: Whether this is an anonymous session
|
||||
summarize: Whether automatic summarization is enabled
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this session. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this session. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this session")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: AsyncHonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this session. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this session. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -66,6 +81,9 @@ class AsyncSession(BaseModel):
|
|||
client: AsyncHonchoCore = Field(
|
||||
..., description="Reference to the parent AsyncHoncho client instance"
|
||||
),
|
||||
*,
|
||||
metadata: dict[str, object] | None = None,
|
||||
config: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new AsyncSession.
|
||||
|
|
@ -74,12 +92,16 @@ class AsyncSession(BaseModel):
|
|||
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
|
||||
metadata: Optional metadata to initialize the cached value
|
||||
config: Optional configuration to initialize the cached value
|
||||
"""
|
||||
super().__init__(
|
||||
id=session_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
|
|
@ -109,27 +131,32 @@ class AsyncSession(BaseModel):
|
|||
Returns:
|
||||
A new AsyncSession instance
|
||||
"""
|
||||
session = cls(session_id, workspace_id, client)
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
await client.workspaces.sessions.get_or_create(
|
||||
session_data = await client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
return cls(
|
||||
session_id,
|
||||
workspace_id,
|
||||
client,
|
||||
metadata=session_data.metadata,
|
||||
config=session_data.configuration,
|
||||
)
|
||||
|
||||
return session
|
||||
return cls(session_id, workspace_id, client)
|
||||
|
||||
async def add_peers(
|
||||
self,
|
||||
peers: str
|
||||
| AsyncPeer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[AsyncPeer, SessionPeerConfig]
|
||||
| list[AsyncPeer | str]
|
||||
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
|
||||
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -174,12 +201,12 @@ class AsyncSession(BaseModel):
|
|||
async def set_peers(
|
||||
self,
|
||||
peers: str
|
||||
| AsyncPeer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[AsyncPeer, SessionPeerConfig]
|
||||
| list[AsyncPeer | str]
|
||||
| list[tuple[AsyncPeer | str, SessionPeerConfig]]
|
||||
| list[AsyncPeer | str | tuple[AsyncPeer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to set for the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -222,7 +249,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
async def remove_peers(
|
||||
self,
|
||||
peers: str | AsyncPeer | list[AsyncPeer | str] = Field(
|
||||
peers: str | PeerBase | list[PeerBase | str] = Field(
|
||||
..., description="Peers to remove from the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -272,15 +299,14 @@ class AsyncSession(BaseModel):
|
|||
for peer in peers_page.items
|
||||
]
|
||||
|
||||
async def get_peer_config(self, peer: str | AsyncPeer) -> SessionPeerConfig:
|
||||
async def get_peer_config(self, peer: str | PeerBase) -> SessionPeerConfig:
|
||||
"""
|
||||
Get the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
peer_get_config_response = (
|
||||
await self._client.workspaces.sessions.peers.get_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
|
@ -291,15 +317,14 @@ class AsyncSession(BaseModel):
|
|||
)
|
||||
|
||||
async def set_peer_config(
|
||||
self, peer: str | AsyncPeer, config: SessionPeerConfig
|
||||
self, peer: str | PeerBase, config: SessionPeerConfig
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
await self._client.workspaces.sessions.peers.set_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=omit
|
||||
|
|
@ -314,7 +339,7 @@ class AsyncSession(BaseModel):
|
|||
messages: MessageCreateParam | list[MessageCreateParam] = Field(
|
||||
..., description="Messages to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Add one or more messages to this session.
|
||||
|
||||
|
|
@ -330,7 +355,7 @@ class AsyncSession(BaseModel):
|
|||
if not isinstance(messages, list):
|
||||
messages = [messages]
|
||||
|
||||
await self._client.workspaces.sessions.messages.create(
|
||||
return await self._client.workspaces.sessions.messages.create(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
messages=[MessageCreateParam(**message) for message in messages],
|
||||
|
|
@ -370,21 +395,75 @@ class AsyncSession(BaseModel):
|
|||
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
Delete this session.
|
||||
Delete this session and all associated data.
|
||||
|
||||
Makes an async API call to delete this session.
|
||||
Makes an async API call to permanently delete this session and all related data including:
|
||||
- Messages
|
||||
- Message embeddings
|
||||
- Observations
|
||||
- Session-Peer associations
|
||||
- Background processing queue items
|
||||
|
||||
This action cannot be undone.
|
||||
"""
|
||||
await self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
async def clone(
|
||||
self,
|
||||
*,
|
||||
message_id: str | None = None,
|
||||
) -> "AsyncSession":
|
||||
"""
|
||||
Clone this session, optionally up to a specific message.
|
||||
|
||||
Makes an async API call to create a copy of this session with a new ID.
|
||||
All messages and peers from the original session are copied to the new session.
|
||||
If a message_id is provided, only messages up to and including that message
|
||||
are copied.
|
||||
|
||||
Args:
|
||||
message_id: Optional message ID to cut off the clone at. If provided,
|
||||
the cloned session will only contain messages up to and
|
||||
including this message.
|
||||
|
||||
Returns:
|
||||
A new AsyncSession object representing the cloned session
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Clone entire session
|
||||
cloned = await session.clone()
|
||||
|
||||
# Clone session up to a specific message
|
||||
cloned = await session.clone(message_id="msg_abc123")
|
||||
```
|
||||
"""
|
||||
# Make the API call using the core SDK's clone method
|
||||
cloned_session_data = await self._client.workspaces.sessions.clone(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
message_id=message_id if message_id is not None else omit,
|
||||
)
|
||||
|
||||
# Return a new AsyncSession object with the cloned session's data
|
||||
return AsyncSession(
|
||||
cloned_session_data.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=cloned_session_data.metadata,
|
||||
config=cloned_session_data.configuration,
|
||||
)
|
||||
|
||||
async def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
Get metadata for this session.
|
||||
|
||||
Makes an async API call to retrieve the current metadata associated with this session.
|
||||
Metadata can include custom attributes, settings, or any other key-value data.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's metadata. Returns an empty dictionary
|
||||
|
|
@ -394,7 +473,8 @@ class AsyncSession(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return session.metadata or {}
|
||||
self._metadata = session.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
async def set_metadata(
|
||||
|
|
@ -408,6 +488,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
Makes an async API call to update the metadata associated with this session.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this session.
|
||||
|
|
@ -418,6 +499,65 @@ class AsyncSession(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
async def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for this session.
|
||||
|
||||
Makes an async API call to retrieve the current configuration associated with this session.
|
||||
Configuration includes settings that control session behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's configuration. Returns an empty dictionary
|
||||
if no configuration is set
|
||||
"""
|
||||
session = await self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._configuration = session.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
async def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this session"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for this session.
|
||||
|
||||
Makes an async API call to update the configuration associated with this session.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with this session.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
await self._client.workspaces.sessions.update(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=configuration,
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this session.
|
||||
|
||||
Makes a single async API call to retrieve the latest metadata and configuration
|
||||
associated with this session and updates the cached attributes.
|
||||
"""
|
||||
session = await self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session.metadata or {}
|
||||
self._configuration = session.configuration or {}
|
||||
|
||||
@validate_call
|
||||
async def get_context(
|
||||
|
|
@ -439,6 +579,32 @@ class AsyncSession(BaseModel):
|
|||
None,
|
||||
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
|
||||
),
|
||||
limit_to_session: bool = Field(
|
||||
False,
|
||||
description="Whether to limit the representation to this session only. If True, only observations from this session will be included.",
|
||||
),
|
||||
search_top_k: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of semantically relevant facts to return when searching with `last_user_message`.",
|
||||
),
|
||||
search_max_distance: float | None = Field(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.",
|
||||
),
|
||||
include_most_derived: bool | None = Field(
|
||||
None,
|
||||
description="Whether to include the most derived observations in the representation.",
|
||||
),
|
||||
max_observations: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of observations to include in the representation.",
|
||||
),
|
||||
) -> SessionContext:
|
||||
"""
|
||||
Get optimized context for this session within a token limit.
|
||||
|
|
@ -455,6 +621,11 @@ class AsyncSession(BaseModel):
|
|||
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
|
||||
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
|
||||
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
|
||||
limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included.
|
||||
search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`.
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.
|
||||
include_most_derived: Whether to include the most derived observations in the representation.
|
||||
max_observations: Maximum number of observations to include in the representation.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history and
|
||||
|
|
@ -491,6 +662,15 @@ class AsyncSession(BaseModel):
|
|||
else omit,
|
||||
peer_target=peer_target if peer_target is not None else omit,
|
||||
peer_perspective=peer_perspective if peer_perspective is not None else omit,
|
||||
limit_to_session=limit_to_session,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
|
|
@ -600,14 +780,28 @@ class AsyncSession(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def upload_file(
|
||||
self,
|
||||
file: tuple[str, bytes, str] | tuple[str, Any, str] | Any = Field(
|
||||
...,
|
||||
description="File to upload. Can be a file object, (filename, bytes, content_type) tuple, or (filename, fileobj, content_type) tuple.",
|
||||
),
|
||||
peer_id: str = Field(..., description="ID of the peer creating the messages"),
|
||||
peer: str | PeerBase = Field(
|
||||
..., description="The peer creating the messages (ID string or Peer object)"
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
),
|
||||
configuration: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the messages",
|
||||
),
|
||||
created_at: str | datetime | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.",
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Upload file to create message(s) in this session.
|
||||
|
|
@ -624,7 +818,11 @@ class AsyncSession(BaseModel):
|
|||
- a file object (must have .name and .read())
|
||||
- a tuple (filename, bytes, content_type)
|
||||
- a tuple (filename, fileobj, content_type)
|
||||
peer_id: ID of the peer who will be attributed as the creator of the messages
|
||||
peer: The peer who will be attributed as the creator of the messages.
|
||||
Can be a peer ID string or an AsyncPeer object.
|
||||
metadata: Optional metadata dictionary to associate with the messages
|
||||
configuration: Optional configuration dictionary to associate with the messages
|
||||
created_at: Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
|
||||
Returns:
|
||||
A list of Message objects representing the created messages
|
||||
|
|
@ -638,22 +836,44 @@ class AsyncSession(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Call the upload endpoint
|
||||
# Extract peer ID from AsyncPeer object if needed
|
||||
resolved_peer_id = peer if isinstance(peer, str) else peer.id
|
||||
|
||||
# Build extra_body dict with optional fields as JSON strings (backend expects Form fields)
|
||||
extra_body_data: dict[str, str] = {}
|
||||
if metadata is not None:
|
||||
extra_body_data["metadata"] = json.dumps(metadata)
|
||||
if configuration is not None:
|
||||
extra_body_data["configuration"] = json.dumps(configuration)
|
||||
if created_at is not None:
|
||||
# Ensure created_at is a string (ISO format)
|
||||
if isinstance(created_at, datetime):
|
||||
extra_body_data["created_at"] = created_at.isoformat()
|
||||
else:
|
||||
extra_body_data["created_at"] = created_at
|
||||
|
||||
# Call the upload endpoint with extra_body for the additional form fields
|
||||
response = await self._client.workspaces.sessions.messages.upload(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
peer_id=resolved_peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
return [Message.model_validate(msg) for msg in response]
|
||||
|
||||
async def working_rep(
|
||||
self,
|
||||
peer: str | AsyncPeer,
|
||||
peer: str | PeerBase,
|
||||
*,
|
||||
target: str | AsyncPeer | None = None,
|
||||
) -> dict[str, object]:
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the current working representation of the peer in this session.
|
||||
|
||||
|
|
@ -661,40 +881,94 @@ class AsyncSession(BaseModel):
|
|||
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`.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
"""
|
||||
from .peer import AsyncPeer
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
return await self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, AsyncPeer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, AsyncPeer) else target,
|
||||
Example:
|
||||
```python
|
||||
# Get peer's representation in this session
|
||||
rep = await session.working_rep('user123')
|
||||
print(rep)
|
||||
|
||||
# Get what user123 knows about assistant in this session
|
||||
local_rep = await session.working_rep('user123', target='assistant')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = await session.working_rep(
|
||||
'user123',
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from ..types import Representation as _Representation
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
data = await self._client.workspaces.peers.working_representation(
|
||||
peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
"""
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
|
||||
return await self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -709,8 +983,8 @@ class AsyncSession(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
observer: Optional observer (ID string or AsyncPeer object) to scope the status check
|
||||
sender: Optional sender (ID string or AsyncPeer object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -724,7 +998,7 @@ class AsyncSession(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = await self.get_deriver_status(observer_id, sender_id)
|
||||
status = await self.get_deriver_status(observer, sender)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
"""Base classes for Honcho SDK entities.
|
||||
|
||||
This module provides base classes that contain only the essential data fields
|
||||
shared by both sync and async variants of Peer and Session. These base classes
|
||||
can be imported anywhere without causing circular import issues, enabling
|
||||
type-safe method signatures like `str | PeerBase`.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PeerBase(BaseModel):
|
||||
"""Base class for Peer objects (sync and async variants).
|
||||
|
||||
This class contains only the essential data fields shared by both
|
||||
Peer and AsyncPeer. Use this type in method signatures to accept
|
||||
either a peer ID string or any Peer object.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this peer")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
|
||||
|
||||
class SessionBase(BaseModel):
|
||||
"""Base class for Session objects (sync and async variants).
|
||||
|
||||
This class contains only the essential data fields shared by both
|
||||
Session and AsyncSession. Use this type in method signatures to accept
|
||||
either a session ID string or any Session object.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this session")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ from honcho_core.types.workspaces.session import Session as SessionCore
|
|||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .peer import Peer
|
||||
from .session import Session
|
||||
|
|
@ -31,9 +32,11 @@ class Honcho(BaseModel):
|
|||
`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
|
||||
metadata: Cached metadata for this workspace. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this workspace. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
core: Access to the underlying honcho_core client for advanced usage
|
||||
"""
|
||||
|
||||
|
|
@ -44,8 +47,20 @@ class Honcho(BaseModel):
|
|||
min_length=1,
|
||||
description="Workspace ID for scoping operations",
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this workspace. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this workspace. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@property
|
||||
def core(self) -> HonchoCore:
|
||||
"""
|
||||
|
|
@ -183,6 +198,7 @@ class Honcho(BaseModel):
|
|||
Raises:
|
||||
ValidationError: If the peer ID is empty or invalid
|
||||
"""
|
||||
# Peer constructor handles API call and caching when metadata/config provided
|
||||
return Peer(
|
||||
id, self.workspace_id, self._client, config=config, metadata=metadata
|
||||
)
|
||||
|
|
@ -204,7 +220,14 @@ class Honcho(BaseModel):
|
|||
workspace_id=self.workspace_id, filters=filters
|
||||
)
|
||||
return SyncPage(
|
||||
peers_page, lambda peer: Peer(peer.id, self.workspace_id, self._client)
|
||||
peers_page,
|
||||
lambda peer: Peer(
|
||||
peer.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=peer.metadata,
|
||||
config=peer.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
@validate_call
|
||||
|
|
@ -267,7 +290,13 @@ class Honcho(BaseModel):
|
|||
)
|
||||
return SyncPage(
|
||||
sessions_page,
|
||||
lambda session: Session(session.id, self.workspace_id, self._client),
|
||||
lambda session: Session(
|
||||
session.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=session.metadata,
|
||||
config=session.configuration,
|
||||
),
|
||||
)
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
|
|
@ -276,14 +305,16 @@ class Honcho(BaseModel):
|
|||
|
||||
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.
|
||||
key-value data associated with the workspace. This method also updates the
|
||||
cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's metadata. Returns an empty
|
||||
dictionary if no metadata is set
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
return workspace.metadata or {}
|
||||
self._metadata = workspace.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
|
|
@ -295,12 +326,62 @@ class Honcho(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with the current workspace.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.update(self.workspace_id, metadata=metadata)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for the current workspace.
|
||||
|
||||
Makes an API call to retrieve configuration associated with the current workspace.
|
||||
Configuration includes settings that control workspace behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the workspace's configuration. Returns an empty
|
||||
dictionary if no configuration is set
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._configuration = workspace.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for the current workspace.
|
||||
|
||||
Makes an API call to update the configuration associated with the current workspace.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with the workspace.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.update(self.workspace_id, configuration=configuration)
|
||||
self._configuration = configuration
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for the current workspace.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with the current workspace and updates the cached attributes.
|
||||
"""
|
||||
workspace = self._client.workspaces.get_or_create(id=self.workspace_id)
|
||||
self._metadata = workspace.metadata or {}
|
||||
self._configuration = workspace.configuration or {}
|
||||
|
||||
def get_workspaces(self, filters: dict[str, object] | None = None) -> list[str]:
|
||||
"""
|
||||
|
|
@ -365,29 +446,50 @@ class Honcho(BaseModel):
|
|||
self.workspace_id, query=query, filters=filters, limit=limit
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
"""
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
session_id=session_id,
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -402,9 +504,9 @@ class Honcho(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
session_id: Optional session ID to scope the status check
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
session: Optional session (ID string or Session object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -418,7 +520,7 @@ class Honcho(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = self.get_deriver_status(observer_id, sender_id, session_id)
|
||||
status = self.get_deriver_status(observer, sender, session)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
@ -471,10 +573,9 @@ class Honcho(BaseModel):
|
|||
metadata: dict[str, object] = Field(
|
||||
..., description="The metadata to update for the message"
|
||||
),
|
||||
session_id: str | None = Field(
|
||||
session: str | SessionBase | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
description="The ID of the session (required if message is a string ID)",
|
||||
description="The session (ID string or Session object) - required if message is a string ID",
|
||||
),
|
||||
) -> Message:
|
||||
"""
|
||||
|
|
@ -485,7 +586,7 @@ class Honcho(BaseModel):
|
|||
Args:
|
||||
message: Either a Message object or a message ID string
|
||||
metadata: The metadata to update for the message
|
||||
session_id: The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
session: The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
|
||||
Returns:
|
||||
The updated Message object
|
||||
|
|
@ -498,9 +599,9 @@ class Honcho(BaseModel):
|
|||
resolved_session_id = message.session_id
|
||||
else:
|
||||
message_id = message
|
||||
if not session_id:
|
||||
raise ValueError("session_id is required when message is a string ID")
|
||||
resolved_session_id = session_id
|
||||
if not session:
|
||||
raise ValueError("session is required when message is a string ID")
|
||||
resolved_session_id = session if isinstance(session, str) else session.id
|
||||
|
||||
return self._client.workspaces.sessions.messages.update(
|
||||
message_id=message_id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,589 @@
|
|||
"""Observation types and scoped access for the Honcho SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from .base import SessionBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import ObservationCreateParam, Representation
|
||||
|
||||
|
||||
def _convert_observation(item: Any) -> dict[str, Any]:
|
||||
"""Convert a core SDK Observations model to a dict for our Observation class."""
|
||||
if hasattr(item, "model_dump"):
|
||||
# Pydantic model - use model_dump()
|
||||
return item.model_dump() # type: ignore[no-any-return]
|
||||
elif isinstance(item, dict):
|
||||
return cast(dict[str, Any], item)
|
||||
else:
|
||||
# Fallback: access as object attributes
|
||||
return {
|
||||
"id": getattr(item, "id", ""),
|
||||
"content": getattr(item, "content", ""),
|
||||
"observer_id": getattr(item, "observer_id", ""),
|
||||
"observed_id": getattr(item, "observed_id", ""),
|
||||
"session_id": getattr(item, "session_id", ""),
|
||||
"created_at": str(getattr(item, "created_at", "")),
|
||||
}
|
||||
|
||||
|
||||
class Observation:
|
||||
"""
|
||||
An observation from the theory-of-mind system.
|
||||
|
||||
Observations are facts derived from messages that help build a representation
|
||||
of a peer.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this observation
|
||||
content: The observation content/text
|
||||
observer_id: The peer who made the observation
|
||||
observed_id: The peer being observed
|
||||
session_id: The session where this observation was made
|
||||
created_at: When the observation was created
|
||||
"""
|
||||
|
||||
id: str
|
||||
content: str
|
||||
observer_id: str
|
||||
observed_id: str
|
||||
session_id: str
|
||||
created_at: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
observer_id: str,
|
||||
observed_id: str,
|
||||
session_id: str,
|
||||
created_at: str,
|
||||
):
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.observer_id = observer_id
|
||||
self.observed_id = observed_id
|
||||
self.session_id = session_id
|
||||
self.created_at = created_at
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, data: dict[str, Any]) -> "Observation":
|
||||
"""Create an Observation from an API response dict."""
|
||||
return cls(
|
||||
id=data.get("id", ""),
|
||||
content=data.get("content", ""),
|
||||
observer_id=data.get("observer_id", ""),
|
||||
observed_id=data.get("observed_id", ""),
|
||||
session_id=data.get("session_id", ""),
|
||||
created_at=str(data.get("created_at", "")),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
truncated = (
|
||||
f"{self.content[:50]}..." if len(self.content) > 50 else self.content
|
||||
)
|
||||
return f"Observation(id={self.id!r}, content={truncated!r})"
|
||||
|
||||
|
||||
class ObservationScope:
|
||||
"""
|
||||
Scoped access to observations for a specific observer/observed relationship.
|
||||
|
||||
This class provides convenient methods to list, query, create, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
`peer.observations_of(target)` (for observations about another peer).
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get self-observations
|
||||
observations = peer.observations
|
||||
obs_list = observations.list()
|
||||
search_results = observations.query("preferences")
|
||||
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
bob_list = bob_observations.list()
|
||||
```
|
||||
"""
|
||||
|
||||
_client: Any
|
||||
workspace_id: str
|
||||
observer: str
|
||||
observed: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Any,
|
||||
workspace_id: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
):
|
||||
"""
|
||||
Initialize an ObservationScope.
|
||||
|
||||
Args:
|
||||
client: The Honcho client instance
|
||||
workspace_id: The workspace ID
|
||||
observer: The observer peer ID
|
||||
observed: The observed peer ID
|
||||
"""
|
||||
self._client = client
|
||||
self.workspace_id = workspace_id
|
||||
self.observer = observer
|
||||
self.observed = observed
|
||||
|
||||
def list(
|
||||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
||||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session: Optional session (ID string or Session object) to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_session_id
|
||||
|
||||
response = self._client.workspaces.observations.list(
|
||||
workspace_id=self.workspace_id,
|
||||
filters=filters,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
# response.items is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response.items
|
||||
]
|
||||
|
||||
def query(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
distance: float | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Semantic search for observations in this scope.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
top_k: Maximum number of results to return
|
||||
distance: Maximum cosine distance threshold (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
List of matching Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
|
||||
response = self._client.workspaces.observations.query(
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
top_k=top_k,
|
||||
distance=distance,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
Delete an observation by ID.
|
||||
|
||||
Args:
|
||||
observation_id: The ID of the observation to delete
|
||||
"""
|
||||
self._client.workspaces.observations.delete(
|
||||
workspace_id=self.workspace_id,
|
||||
observation_id=observation_id,
|
||||
)
|
||||
|
||||
def create(
|
||||
self,
|
||||
observations: "ObservationCreateParam | list[ObservationCreateParam]",
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Create observations in this scope.
|
||||
|
||||
Args:
|
||||
observations: Single observation or list of observations to create.
|
||||
Each observation must have 'content' and 'session_id' keys.
|
||||
|
||||
Returns:
|
||||
List of created Observation objects
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Create a single observation
|
||||
observations = peer.observations.create(
|
||||
{"content": "User prefers dark mode", "session_id": "session1"}
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
observations = peer.observations.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session1"},
|
||||
{"content": "User is interested in AI", "session_id": "session1"},
|
||||
])
|
||||
```
|
||||
"""
|
||||
# Normalize to list
|
||||
if not isinstance(observations, list):
|
||||
observations = [observations]
|
||||
|
||||
# Build the request body with observer/observed from scope
|
||||
request_observations = [
|
||||
{
|
||||
"content": obs["content"],
|
||||
"session_id": obs["session_id"]
|
||||
if isinstance(obs["session_id"], str)
|
||||
else obs["session_id"].id,
|
||||
"observer_id": self.observer,
|
||||
"observed_id": self.observed,
|
||||
}
|
||||
for obs in observations
|
||||
]
|
||||
|
||||
response = self._client.workspaces.observations.create( # type: ignore[attr-defined]
|
||||
workspace_id=self.workspace_id,
|
||||
observations=request_observations,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the computed representation for this scope.
|
||||
|
||||
This returns the working representation (narrative) built from the
|
||||
observations in this scope.
|
||||
|
||||
Args:
|
||||
search_query: Optional semantic search query to curate the representation
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
"""
|
||||
from honcho_core._types import omit
|
||||
|
||||
from .types import Representation
|
||||
|
||||
response = self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.observer,
|
||||
workspace_id=self.workspace_id,
|
||||
target=self.observed,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
representation = response.get("representation")
|
||||
if representation is not None:
|
||||
return Representation.from_dict(cast(dict[str, Any], representation))
|
||||
else:
|
||||
return Representation.from_dict(response)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"ObservationScope(workspace_id={self.workspace_id!r}, "
|
||||
f"observer={self.observer!r}, observed={self.observed!r})"
|
||||
)
|
||||
|
||||
|
||||
class AsyncObservationScope:
|
||||
"""
|
||||
Async scoped access to observations for a specific observer/observed relationship.
|
||||
|
||||
This class provides convenient async methods to list, query, create, and delete observations
|
||||
that are automatically scoped to a specific observer/observed pair.
|
||||
|
||||
Typically accessed via `peer.observations` (for self-observations) or
|
||||
`peer.observations_of(target)` (for observations about another peer).
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get self-observations
|
||||
observations = peer.observations
|
||||
obs_list = await observations.list()
|
||||
search_results = await observations.query("preferences")
|
||||
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
bob_list = await bob_observations.list()
|
||||
```
|
||||
"""
|
||||
|
||||
_client: Any
|
||||
workspace_id: str
|
||||
observer: str
|
||||
observed: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Any,
|
||||
workspace_id: str,
|
||||
observer: str,
|
||||
observed: str,
|
||||
):
|
||||
"""
|
||||
Initialize an AsyncObservationScope.
|
||||
|
||||
Args:
|
||||
client: The AsyncHoncho client instance
|
||||
workspace_id: The workspace ID
|
||||
observer: The observer peer ID
|
||||
observed: The observed peer ID
|
||||
"""
|
||||
self._client = client
|
||||
self.workspace_id = workspace_id
|
||||
self.observer = observer
|
||||
self.observed = observed
|
||||
|
||||
async def list(
|
||||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
List observations in this scope.
|
||||
|
||||
Args:
|
||||
page: Page number (1-indexed)
|
||||
size: Number of results per page
|
||||
session: Optional session (ID string or AsyncSession object) to filter by
|
||||
|
||||
Returns:
|
||||
List of Observation objects
|
||||
"""
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
if resolved_session_id:
|
||||
filters["session_id"] = resolved_session_id
|
||||
|
||||
response = await self._client.workspaces.observations.list(
|
||||
workspace_id=self.workspace_id,
|
||||
filters=filters,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
# response.items is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response.items
|
||||
]
|
||||
|
||||
async def query(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
distance: float | None = None,
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Semantic search for observations in this scope.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
top_k: Maximum number of results to return
|
||||
distance: Maximum cosine distance threshold (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
List of matching Observation objects
|
||||
"""
|
||||
filters: dict[str, Any] = {
|
||||
"observer": self.observer,
|
||||
"observed": self.observed,
|
||||
}
|
||||
|
||||
response = await self._client.workspaces.observations.query(
|
||||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
top_k=top_k,
|
||||
distance=distance,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
async def delete(self, observation_id: str) -> None:
|
||||
"""
|
||||
Delete an observation by ID.
|
||||
|
||||
Args:
|
||||
observation_id: The ID of the observation to delete
|
||||
"""
|
||||
await self._client.workspaces.observations.delete(
|
||||
workspace_id=self.workspace_id,
|
||||
observation_id=observation_id,
|
||||
)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
observations: "ObservationCreateParam | list[ObservationCreateParam]",
|
||||
) -> list[Observation]:
|
||||
"""
|
||||
Create observations in this scope.
|
||||
|
||||
Args:
|
||||
observations: Single observation or list of observations to create.
|
||||
Each observation must have 'content' and 'session_id' keys.
|
||||
|
||||
Returns:
|
||||
List of created Observation objects
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Create a single observation
|
||||
observations = await peer.observations.create(
|
||||
{"content": "User prefers dark mode", "session_id": "session1"}
|
||||
)
|
||||
|
||||
# Create multiple observations
|
||||
observations = await peer.observations.create([
|
||||
{"content": "User prefers dark mode", "session_id": "session1"},
|
||||
{"content": "User is interested in AI", "session_id": "session1"},
|
||||
])
|
||||
```
|
||||
"""
|
||||
# Normalize to list
|
||||
if not isinstance(observations, list):
|
||||
observations = [observations]
|
||||
|
||||
# Build the request body with observer/observed from scope
|
||||
request_observations = [
|
||||
{
|
||||
"content": obs["content"],
|
||||
"session_id": obs["session_id"]
|
||||
if isinstance(obs["session_id"], str)
|
||||
else obs["session_id"].id,
|
||||
"observer_id": self.observer,
|
||||
"observed_id": self.observed,
|
||||
}
|
||||
for obs in observations
|
||||
]
|
||||
|
||||
response = await self._client.workspaces.observations.create( # type: ignore[attr-defined]
|
||||
workspace_id=self.workspace_id,
|
||||
observations=request_observations,
|
||||
)
|
||||
|
||||
# response is List[Observations] (Pydantic models)
|
||||
return [
|
||||
Observation.from_api_response(_convert_observation(item))
|
||||
for item in response
|
||||
]
|
||||
|
||||
async def get_representation(
|
||||
self,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the computed representation for this scope.
|
||||
|
||||
This returns the working representation (narrative) built from the
|
||||
observations in this scope.
|
||||
|
||||
Args:
|
||||
search_query: Optional semantic search query to curate the representation
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
"""
|
||||
from honcho_core._types import omit
|
||||
|
||||
from .types import Representation
|
||||
|
||||
response = await self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.observer,
|
||||
workspace_id=self.workspace_id,
|
||||
target=self.observed,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
representation = response.get("representation")
|
||||
if representation is not None:
|
||||
return Representation.from_dict(cast(dict[str, Any], representation))
|
||||
else:
|
||||
return Representation.from_dict(response)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"AsyncObservationScope(workspace_id={self.workspace_id!r}, "
|
||||
f"observer={self.observer!r}, observed={self.observed!r})"
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Generator
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import omit
|
||||
|
|
@ -10,16 +10,21 @@ from honcho_core.types.workspaces import PeerCardResponse
|
|||
from honcho_core.types.workspaces.session import Session as SessionCore
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .types import DialecticStreamResponse
|
||||
from .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .types import DialecticStreamResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .session import Session
|
||||
from .observations import ObservationScope
|
||||
from .types import PeerContext, Representation
|
||||
|
||||
from .session import Session
|
||||
|
||||
|
||||
class Peer(BaseModel):
|
||||
class Peer(PeerBase):
|
||||
"""
|
||||
Represents a peer in the Honcho system.
|
||||
|
||||
|
|
@ -29,15 +34,27 @@ class Peer(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this peer
|
||||
_client: Reference to the parent Honcho client instance
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this peer. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this peer. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this peer")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this peer. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this peer. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -77,24 +94,32 @@ class Peer(BaseModel):
|
|||
config: Optional configuration to set for this peer.
|
||||
If set, will get/create peer immediately with flags.
|
||||
"""
|
||||
super().__init__(id=peer_id, workspace_id=workspace_id)
|
||||
super().__init__(
|
||||
id=peer_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.peers.get_or_create(
|
||||
peer_data = self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=peer_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
# Update cached values with API response
|
||||
self._metadata = peer_data.metadata
|
||||
self._configuration = peer_data.configuration
|
||||
|
||||
def chat(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
stream: bool = False,
|
||||
target: str | Peer | None = None,
|
||||
session_id: str | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
session: str | SessionBase | None = None,
|
||||
) -> str | DialecticStreamResponse | None:
|
||||
"""
|
||||
Query the peer's representation with a natural language question.
|
||||
|
|
@ -108,14 +133,28 @@ class Peer(BaseModel):
|
|||
stream: Whether to stream the response
|
||||
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
|
||||
session_id: Optional session ID to scope the query to a specific session.
|
||||
If provided, only information from that session is considered
|
||||
querying the peer's global representation. Can be a peer ID string
|
||||
or a Peer object.
|
||||
session: Optional session to scope the query to. If provided, only
|
||||
information from that session is considered. Can be a session
|
||||
ID string or a Session object.
|
||||
|
||||
Returns:
|
||||
For non-streaming: Response string containing the answer, or None if no relevant information
|
||||
For streaming: DialecticStreamResponse object that can be iterated over and provides final response
|
||||
"""
|
||||
# Extract IDs from objects if needed
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
resolved_session_id = (
|
||||
None
|
||||
if session is None
|
||||
else (session if isinstance(session, str) else session.id)
|
||||
)
|
||||
|
||||
if stream:
|
||||
|
||||
def stream_response() -> Generator[str, None, None]:
|
||||
|
|
@ -127,8 +166,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=True,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
) as response:
|
||||
response.http_response.raise_for_status()
|
||||
for line in response.iter_lines():
|
||||
|
|
@ -152,8 +191,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
query=query,
|
||||
stream=stream,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
session_id=resolved_session_id,
|
||||
)
|
||||
if response.content in ("", None, "None"):
|
||||
return None
|
||||
|
|
@ -194,6 +233,10 @@ class Peer(BaseModel):
|
|||
metadata: dict[str, object] | None = Field(
|
||||
None, description="Optional metadata dictionary"
|
||||
),
|
||||
config: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the message",
|
||||
),
|
||||
created_at: datetime.datetime | str | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the message. Accepts a datetime which will be converted to an ISO 8601 string, or a preformatted string.",
|
||||
|
|
@ -221,6 +264,7 @@ class Peer(BaseModel):
|
|||
return MessageCreateParam(
|
||||
peer_id=self.id,
|
||||
content=content,
|
||||
configuration=config,
|
||||
metadata=metadata,
|
||||
created_at=created_at_str,
|
||||
)
|
||||
|
|
@ -231,7 +275,7 @@ class Peer(BaseModel):
|
|||
|
||||
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.
|
||||
associated with the peer. This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's metadata. Returns an empty dictionary
|
||||
|
|
@ -241,7 +285,8 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.metadata or {}
|
||||
self._metadata = peer.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
|
|
@ -255,6 +300,7 @@ class Peer(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with this peer.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this peer.
|
||||
|
|
@ -265,13 +311,15 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_peer_config(self) -> dict[str, object]:
|
||||
def get_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`.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
|
|
@ -280,10 +328,11 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
return peer.configuration or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_peer_config(
|
||||
def set_config(
|
||||
self,
|
||||
config: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this peer"
|
||||
|
|
@ -296,6 +345,7 @@ class Peer(BaseModel):
|
|||
|
||||
Makes an API call to update the configuration associated with this peer.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer.
|
||||
|
|
@ -306,6 +356,51 @@ class Peer(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
configuration=config,
|
||||
)
|
||||
self._configuration = config
|
||||
|
||||
def get_peer_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get the current workspace-level configuration for this peer.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`get_config` instead.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the peer's configuration
|
||||
"""
|
||||
return self.get_config()
|
||||
|
||||
@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.
|
||||
|
||||
.. deprecated::
|
||||
Use :meth:`set_config` instead.
|
||||
|
||||
Args:
|
||||
config: A dictionary of configuration to associate with this peer
|
||||
"""
|
||||
return self.set_config(config)
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this peer.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with this peer and updates the cached attributes.
|
||||
"""
|
||||
peer = self._client.workspaces.peers.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = peer.metadata or {}
|
||||
self._configuration = peer.configuration or {}
|
||||
|
||||
@validate_call
|
||||
def search(
|
||||
|
|
@ -342,7 +437,7 @@ class Peer(BaseModel):
|
|||
|
||||
def card(
|
||||
self,
|
||||
target: str | Peer | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the peer card for this peer.
|
||||
|
|
@ -362,10 +457,15 @@ class Peer(BaseModel):
|
|||
if isinstance(target, str) and len(target.strip()) == 0:
|
||||
raise ValueError("target string cannot be empty")
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
response: PeerCardResponse = self._client.workspaces.peers.card(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
target=target_id,
|
||||
)
|
||||
if response.peer_card is None:
|
||||
return ""
|
||||
|
|
@ -374,6 +474,215 @@ class Peer(BaseModel):
|
|||
|
||||
return "\n".join(items)
|
||||
|
||||
def working_rep(
|
||||
self,
|
||||
session: str | SessionBase | None = None,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get a working representation for this peer.
|
||||
|
||||
Args:
|
||||
session: Optional session to scope the representation to.
|
||||
target: Optional target peer to get the representation of. If provided,
|
||||
returns the representation of the target from the perspective of this peer.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get global representation
|
||||
rep = peer.working_rep()
|
||||
print(rep)
|
||||
|
||||
# Get representation scoped to a session
|
||||
session_rep = peer.working_rep(session='session-123')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = peer.working_rep(
|
||||
search_query='preferences',
|
||||
search_top_k=10,
|
||||
max_observations=50
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .types import Representation as _Representation
|
||||
|
||||
session_id = (
|
||||
None
|
||||
if session is None
|
||||
else session
|
||||
if isinstance(session, str)
|
||||
else session.id
|
||||
)
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
data = self._client.workspaces.peers.working_representation(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=session_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
representation = data.get("representation")
|
||||
if representation is not None:
|
||||
return _Representation.from_dict(cast(dict[str, object], representation))
|
||||
else:
|
||||
return _Representation.from_dict(data)
|
||||
|
||||
def get_context(
|
||||
self,
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "PeerContext":
|
||||
"""
|
||||
Get context for this peer, including representation and peer card.
|
||||
|
||||
This is a convenience method that retrieves both the working representation
|
||||
and peer card in a single API call.
|
||||
|
||||
Args:
|
||||
target: Optional target peer to get context for. If provided, returns
|
||||
the context for the target from this peer's perspective.
|
||||
Can be a Peer object or peer ID string.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A PeerContext object containing the representation and peer card
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get own context
|
||||
context = peer.get_context()
|
||||
print(context.representation)
|
||||
print(context.peer_card)
|
||||
|
||||
# Get context for another peer
|
||||
context = peer.get_context(target='other-peer-id')
|
||||
|
||||
# Get context with semantic search
|
||||
context = peer.get_context(
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .types import PeerContext as _PeerContext
|
||||
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
response = self._client.workspaces.peers.get_context(
|
||||
peer_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
return _PeerContext.from_api_response(response)
|
||||
|
||||
@property
|
||||
def observations(self) -> "ObservationScope":
|
||||
"""
|
||||
Access this peer's self-observations (where observer == observed == self).
|
||||
|
||||
This property provides a convenient way to access observations that this peer
|
||||
has made about themselves. Use this for self-observation scenarios.
|
||||
|
||||
Returns:
|
||||
An ObservationScope scoped to this peer's self-observations
|
||||
|
||||
Example:
|
||||
```python
|
||||
# List self-observations
|
||||
obs_list = peer.observations.list()
|
||||
|
||||
# Search self-observations
|
||||
results = peer.observations.query("preferences")
|
||||
|
||||
# Delete a self-observation
|
||||
peer.observations.delete("obs-123")
|
||||
```
|
||||
"""
|
||||
from .observations import ObservationScope as _ObservationScope
|
||||
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, self.id)
|
||||
|
||||
def observations_of(self, target: str | PeerBase) -> "ObservationScope":
|
||||
"""
|
||||
Access observations this peer has made about another peer.
|
||||
|
||||
This method provides scoped access to observations where this peer is the
|
||||
observer and the target is the observed peer.
|
||||
|
||||
Args:
|
||||
target: The target peer (either a Peer object or peer ID string)
|
||||
|
||||
Returns:
|
||||
An ObservationScope scoped to this peer's observations of the target
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get observations about another peer
|
||||
bob_observations = peer.observations_of("bob")
|
||||
|
||||
# List observations
|
||||
obs_list = bob_observations.list()
|
||||
|
||||
# Search observations
|
||||
results = bob_observations.query("work history")
|
||||
|
||||
# Get the representation from these observations
|
||||
rep = bob_observations.get_representation()
|
||||
```
|
||||
"""
|
||||
from .observations import ObservationScope as _ObservationScope
|
||||
|
||||
target_id = target.id if isinstance(target, PeerBase) else target
|
||||
return _ObservationScope(self._client, self.workspace_id, self.id, target_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Peer.
|
||||
|
|
|
|||
|
|
@ -3,19 +3,24 @@ from __future__ import annotations
|
|||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from honcho_core import Honcho as HonchoCore
|
||||
from honcho_core._types import omit
|
||||
from honcho_core.types import DeriverStatus
|
||||
from honcho_core.types.workspaces.sessions import MessageCreateParam
|
||||
from honcho_core.types.workspaces.sessions.message import Message
|
||||
from honcho_core.types.workspaces.sessions.message_create_param import Configuration
|
||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, validate_call
|
||||
|
||||
from .base import PeerBase, SessionBase
|
||||
from .pagination import SyncPage
|
||||
from .session_context import SessionContext, SessionSummaries, Summary
|
||||
from .utils import prepare_file_for_upload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import Representation
|
||||
from .peer import Peer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -32,7 +37,7 @@ class SessionPeerConfig(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
class Session(SessionBase):
|
||||
"""
|
||||
Represents a session in Honcho.
|
||||
|
||||
|
|
@ -42,17 +47,27 @@ class Session(BaseModel):
|
|||
|
||||
Attributes:
|
||||
id: Unique identifier for this session
|
||||
_honcho: Reference to the parent Honcho client instance
|
||||
anonymous: Whether this is an anonymous session
|
||||
summarize: Whether automatic summarization is enabled
|
||||
workspace_id: Workspace ID for scoping operations
|
||||
metadata: Cached metadata for this session. May be stale if not recently
|
||||
fetched. Call get_metadata() for fresh data.
|
||||
configuration: Cached configuration for this session. May be stale if not
|
||||
recently fetched. Call get_config() for fresh data.
|
||||
"""
|
||||
|
||||
id: str = Field(..., min_length=1, description="Unique identifier for this session")
|
||||
workspace_id: str = Field(
|
||||
..., min_length=1, description="Workspace ID for scoping operations"
|
||||
)
|
||||
_metadata: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_configuration: dict[str, object] | None = PrivateAttr(default=None)
|
||||
_client: HonchoCore = PrivateAttr()
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, object] | None:
|
||||
"""Cached metadata for this session. May be stale. Use get_metadata() for fresh data."""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def configuration(self) -> dict[str, object] | None:
|
||||
"""Cached configuration for this session. May be stale. Use get_config() for fresh data."""
|
||||
return self._configuration
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -95,24 +110,29 @@ class Session(BaseModel):
|
|||
workspace_id=workspace_id,
|
||||
)
|
||||
self._client = client
|
||||
self._metadata = metadata
|
||||
self._configuration = config
|
||||
|
||||
if config is not None or metadata is not None:
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=workspace_id,
|
||||
id=session_id,
|
||||
configuration=config if config is not None else omit,
|
||||
metadata=metadata if metadata is not None else omit,
|
||||
)
|
||||
# Update cached values with API response
|
||||
self._metadata = session_data.metadata
|
||||
self._configuration = session_data.configuration
|
||||
|
||||
def add_peers(
|
||||
self,
|
||||
peers: str
|
||||
| Peer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[Peer, SessionPeerConfig]
|
||||
| list[Peer | str]
|
||||
| list[tuple[Peer | str, SessionPeerConfig]]
|
||||
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -157,12 +177,12 @@ class Session(BaseModel):
|
|||
def set_peers(
|
||||
self,
|
||||
peers: str
|
||||
| Peer
|
||||
| PeerBase
|
||||
| tuple[str, SessionPeerConfig]
|
||||
| tuple[Peer, SessionPeerConfig]
|
||||
| list[Peer | str]
|
||||
| list[tuple[Peer | str, SessionPeerConfig]]
|
||||
| list[Peer | str | tuple[Peer | str, SessionPeerConfig]] = Field(
|
||||
| tuple[PeerBase, SessionPeerConfig]
|
||||
| list[PeerBase | str]
|
||||
| list[tuple[PeerBase | str, SessionPeerConfig]]
|
||||
| list[PeerBase | str | tuple[PeerBase | str, SessionPeerConfig]] = Field(
|
||||
..., description="Peers to set for the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -205,7 +225,7 @@ class Session(BaseModel):
|
|||
|
||||
def remove_peers(
|
||||
self,
|
||||
peers: str | Peer | list[Peer | str] = Field(
|
||||
peers: str | PeerBase | list[PeerBase | str] = Field(
|
||||
..., description="Peers to remove from the session"
|
||||
),
|
||||
) -> None:
|
||||
|
|
@ -254,14 +274,13 @@ class Session(BaseModel):
|
|||
Peer(peer.id, self.workspace_id, self._client) for peer in peers_page.items
|
||||
]
|
||||
|
||||
def get_peer_config(self, peer: str | Peer) -> SessionPeerConfig:
|
||||
def get_peer_config(self, peer: str | PeerBase) -> SessionPeerConfig:
|
||||
"""
|
||||
Get the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import Peer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
peer_get_config_response = self._client.workspaces.sessions.peers.get_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
|
@ -270,14 +289,13 @@ class Session(BaseModel):
|
|||
observe_me=peer_get_config_response.observe_me,
|
||||
)
|
||||
|
||||
def set_peer_config(self, peer: str | Peer, config: SessionPeerConfig) -> None:
|
||||
def set_peer_config(self, peer: str | PeerBase, config: SessionPeerConfig) -> None:
|
||||
"""
|
||||
Set the configuration for a peer in this session.
|
||||
"""
|
||||
from .peer import Peer
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
self._client.workspaces.sessions.peers.set_config(
|
||||
peer_id=str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
peer_id=peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
observe_others=omit
|
||||
|
|
@ -292,7 +310,7 @@ class Session(BaseModel):
|
|||
messages: MessageCreateParam | list[MessageCreateParam] = Field(
|
||||
..., description="Messages to add to the session"
|
||||
),
|
||||
) -> None:
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Add one or more messages to this session.
|
||||
|
||||
|
|
@ -308,7 +326,7 @@ class Session(BaseModel):
|
|||
if not isinstance(messages, list):
|
||||
messages = [messages]
|
||||
|
||||
self._client.workspaces.sessions.messages.create(
|
||||
return self._client.workspaces.sessions.messages.create(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
messages=[MessageCreateParam(**message) for message in messages],
|
||||
|
|
@ -352,30 +370,83 @@ class Session(BaseModel):
|
|||
|
||||
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.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's metadata. Returns an empty dictionary
|
||||
if no metadata is set
|
||||
"""
|
||||
return (
|
||||
self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
).metadata
|
||||
or {}
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session_data.metadata or {}
|
||||
return self._metadata
|
||||
|
||||
def delete(self) -> None:
|
||||
"""
|
||||
Delete this session
|
||||
Delete this session and all associated data.
|
||||
|
||||
Makes an API call to mark this session as inactive.
|
||||
Makes an API call to permanently delete this session and all related data including:
|
||||
- Messages
|
||||
- Message embeddings
|
||||
- Observations
|
||||
- Session-Peer associations
|
||||
- Background processing queue items
|
||||
|
||||
This action cannot be undone.
|
||||
"""
|
||||
self._client.workspaces.sessions.delete(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
)
|
||||
|
||||
def clone(
|
||||
self,
|
||||
*,
|
||||
message_id: str | None = None,
|
||||
) -> "Session":
|
||||
"""
|
||||
Clone this session, optionally up to a specific message.
|
||||
|
||||
Makes an API call to create a copy of this session with a new ID.
|
||||
All messages and peers from the original session are copied to the new session.
|
||||
If a message_id is provided, only messages up to and including that message
|
||||
are copied.
|
||||
|
||||
Args:
|
||||
message_id: Optional message ID to cut off the clone at. If provided,
|
||||
the cloned session will only contain messages up to and
|
||||
including this message.
|
||||
|
||||
Returns:
|
||||
A new Session object representing the cloned session
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Clone entire session
|
||||
cloned = session.clone()
|
||||
|
||||
# Clone session up to a specific message
|
||||
cloned = session.clone(message_id="msg_abc123")
|
||||
```
|
||||
"""
|
||||
# Make the API call using the core SDK's clone method
|
||||
cloned_session_data = self._client.workspaces.sessions.clone(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
message_id=message_id if message_id is not None else omit,
|
||||
)
|
||||
|
||||
# Return a new Session object with the cloned session's data
|
||||
return Session(
|
||||
cloned_session_data.id,
|
||||
self.workspace_id,
|
||||
self._client,
|
||||
metadata=cloned_session_data.metadata,
|
||||
config=cloned_session_data.configuration,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
def set_metadata(
|
||||
self,
|
||||
|
|
@ -388,6 +459,7 @@ class Session(BaseModel):
|
|||
|
||||
Makes an API call to update the metadata associated with this session.
|
||||
This will overwrite any existing metadata with the provided values.
|
||||
This method also updates the cached metadata attribute.
|
||||
|
||||
Args:
|
||||
metadata: A dictionary of metadata to associate with this session.
|
||||
|
|
@ -398,6 +470,65 @@ class Session(BaseModel):
|
|||
workspace_id=self.workspace_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
self._metadata = metadata
|
||||
|
||||
def get_config(self) -> dict[str, object]:
|
||||
"""
|
||||
Get configuration for this session.
|
||||
|
||||
Makes an API call to retrieve the current configuration associated with this session.
|
||||
Configuration includes settings that control session behavior.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the session's configuration. Returns an empty dictionary
|
||||
if no configuration is set
|
||||
"""
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._configuration = session_data.configuration or {}
|
||||
return self._configuration
|
||||
|
||||
@validate_call
|
||||
def set_config(
|
||||
self,
|
||||
configuration: dict[str, object] = Field(
|
||||
..., description="Configuration dictionary to associate with this session"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Set configuration for this session.
|
||||
|
||||
Makes an API call to update the configuration associated with this session.
|
||||
This will overwrite any existing configuration with the provided values.
|
||||
This method also updates the cached configuration attribute.
|
||||
|
||||
Args:
|
||||
configuration: A dictionary of configuration to associate with this session.
|
||||
Keys must be strings, values can be any JSON-serializable type
|
||||
"""
|
||||
self._client.workspaces.sessions.update(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
configuration=configuration,
|
||||
)
|
||||
self._configuration = configuration
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""
|
||||
Refresh cached metadata and configuration for this session.
|
||||
|
||||
Makes a single API call to retrieve the latest metadata and configuration
|
||||
associated with this session and updates the cached attributes.
|
||||
"""
|
||||
session_data = self._client.workspaces.sessions.get_or_create(
|
||||
workspace_id=self.workspace_id,
|
||||
id=self.id,
|
||||
)
|
||||
self._metadata = session_data.metadata or {}
|
||||
self._configuration = session_data.configuration or {}
|
||||
|
||||
@validate_call
|
||||
def get_context(
|
||||
|
|
@ -419,6 +550,32 @@ class Session(BaseModel):
|
|||
None,
|
||||
description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.",
|
||||
),
|
||||
limit_to_session: bool = Field(
|
||||
False,
|
||||
description="Whether to limit the representation to this session only. If True, only observations from this session will be included.",
|
||||
),
|
||||
search_top_k: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of semantically relevant facts to return when searching with `last_user_message`.",
|
||||
),
|
||||
search_max_distance: float | None = Field(
|
||||
None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.",
|
||||
),
|
||||
include_most_derived: bool | None = Field(
|
||||
None,
|
||||
description="Whether to include the most derived observations in the representation.",
|
||||
),
|
||||
max_observations: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of observations to include in the representation.",
|
||||
),
|
||||
) -> SessionContext:
|
||||
"""
|
||||
Get optimized context for this session within a token limit.
|
||||
|
|
@ -435,6 +592,11 @@ class Session(BaseModel):
|
|||
peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.
|
||||
last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.
|
||||
peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.
|
||||
limit_to_session: Whether to limit the representation to this session only. If True, only observations from this session will be included.
|
||||
search_top_k: Number of semantically relevant facts to return when searching with `last_user_message`.
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0) when searching with `last_user_message`.
|
||||
include_most_derived: Whether to include the most derived observations in the representation.
|
||||
max_observations: Maximum number of observations to include in the representation.
|
||||
|
||||
Returns:
|
||||
A SessionContext object containing the optimized message history and
|
||||
|
|
@ -471,6 +633,15 @@ class Session(BaseModel):
|
|||
else omit,
|
||||
peer_target=peer_target if peer_target is not None else omit,
|
||||
peer_perspective=peer_perspective if peer_perspective is not None else omit,
|
||||
limit_to_session=limit_to_session,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
|
||||
# Convert the honcho_core summary to our Summary if it exists
|
||||
|
|
@ -580,14 +751,28 @@ class Session(BaseModel):
|
|||
limit=limit,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def upload_file(
|
||||
self,
|
||||
file: tuple[str, bytes, str] | tuple[str, Any, str] | Any = Field(
|
||||
...,
|
||||
description="File to upload. Can be a file object, (filename, bytes, content_type) tuple, or (filename, fileobj, content_type) tuple.",
|
||||
),
|
||||
peer_id: str = Field(..., description="ID of the peer creating the messages"),
|
||||
peer: str | PeerBase = Field(
|
||||
..., description="The peer creating the messages (ID string or Peer object)"
|
||||
),
|
||||
metadata: dict[str, object] | None = Field(
|
||||
None,
|
||||
description="Optional metadata dictionary to associate with the messages",
|
||||
),
|
||||
configuration: Configuration | None = Field(
|
||||
None,
|
||||
description="Optional configuration dictionary to associate with the messages",
|
||||
),
|
||||
created_at: str | datetime | None = Field(
|
||||
None,
|
||||
description="Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.",
|
||||
),
|
||||
) -> list[Message]:
|
||||
"""
|
||||
Upload file to create message(s) in this session.
|
||||
|
|
@ -604,7 +789,11 @@ class Session(BaseModel):
|
|||
- a file object (must have .name and .read())
|
||||
- a tuple (filename, bytes, content_type)
|
||||
- a tuple (filename, fileobj, content_type)
|
||||
peer_id: ID of the peer who will be attributed as the creator of the messages
|
||||
peer: The peer who will be attributed as the creator of the messages.
|
||||
Can be a peer ID string or a Peer object.
|
||||
metadata: Optional metadata dictionary to associate with the messages
|
||||
configuration: Optional configuration dictionary to associate with the messages
|
||||
created_at: Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
|
||||
Returns:
|
||||
A list of Message objects representing the created messages
|
||||
|
|
@ -618,22 +807,44 @@ class Session(BaseModel):
|
|||
# Prepare file for upload using shared utility
|
||||
filename, content_bytes, content_type = prepare_file_for_upload(file)
|
||||
|
||||
# Call the upload endpoint
|
||||
# Extract peer ID from Peer object if needed
|
||||
resolved_peer_id = peer if isinstance(peer, str) else peer.id
|
||||
|
||||
# Build extra_body dict with optional fields as JSON strings (backend expects Form fields)
|
||||
extra_body_data: dict[str, str] = {}
|
||||
if metadata is not None:
|
||||
extra_body_data["metadata"] = json.dumps(metadata)
|
||||
if configuration is not None:
|
||||
extra_body_data["configuration"] = json.dumps(configuration)
|
||||
if created_at is not None:
|
||||
# Ensure created_at is a string (ISO format)
|
||||
if isinstance(created_at, datetime):
|
||||
extra_body_data["created_at"] = created_at.isoformat()
|
||||
else:
|
||||
extra_body_data["created_at"] = created_at
|
||||
|
||||
# Call the upload endpoint with extra_body for the additional form fields
|
||||
response = self._client.workspaces.sessions.messages.upload(
|
||||
session_id=self.id,
|
||||
workspace_id=self.workspace_id,
|
||||
file=(filename, content_bytes, content_type),
|
||||
peer_id=peer_id,
|
||||
peer_id=resolved_peer_id,
|
||||
extra_body=extra_body_data if extra_body_data else None,
|
||||
)
|
||||
|
||||
return [Message.model_validate(msg) for msg in response]
|
||||
|
||||
def working_rep(
|
||||
self,
|
||||
peer: str | Peer,
|
||||
peer: str | PeerBase,
|
||||
*,
|
||||
target: str | Peer | None = None,
|
||||
) -> dict[str, object]:
|
||||
target: str | PeerBase | None = None,
|
||||
search_query: str | None = None,
|
||||
search_top_k: int | None = None,
|
||||
search_max_distance: float | None = None,
|
||||
include_most_derived: bool | None = None,
|
||||
max_observations: int | None = None,
|
||||
) -> "Representation":
|
||||
"""
|
||||
Get the current working representation of the peer in this session.
|
||||
|
||||
|
|
@ -641,40 +852,94 @@ class Session(BaseModel):
|
|||
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`.
|
||||
search_query: Semantic search query to filter relevant observations
|
||||
search_top_k: Number of semantically relevant facts to return
|
||||
search_max_distance: Maximum semantic distance for search results (0.0-1.0)
|
||||
include_most_derived: Whether to include the most derived observations
|
||||
max_observations: Maximum number of observations to include
|
||||
|
||||
Returns:
|
||||
A dictionary containing information about the peer.
|
||||
"""
|
||||
from .peer import Peer
|
||||
A Representation object containing explicit and deductive observations
|
||||
|
||||
return self._client.workspaces.peers.working_representation(
|
||||
str(peer.id) if isinstance(peer, Peer) else peer,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=str(target.id) if isinstance(target, Peer) else target,
|
||||
Example:
|
||||
```python
|
||||
# Get peer's representation in this session
|
||||
rep = session.working_rep('user123')
|
||||
print(rep)
|
||||
|
||||
# Get what user123 knows about assistant in this session
|
||||
local_rep = session.working_rep('user123', target='assistant')
|
||||
|
||||
# Get representation with semantic search
|
||||
searched_rep = session.working_rep(
|
||||
'user123',
|
||||
search_query='preferences',
|
||||
search_top_k=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
from .types import Representation as _Representation
|
||||
|
||||
peer_id = peer if isinstance(peer, str) else peer.id
|
||||
target_id = (
|
||||
None
|
||||
if target is None
|
||||
else (target if isinstance(target, str) else target.id)
|
||||
)
|
||||
|
||||
@validate_call
|
||||
data = self._client.workspaces.peers.working_representation(
|
||||
peer_id,
|
||||
workspace_id=self.workspace_id,
|
||||
session_id=self.id,
|
||||
target=target_id,
|
||||
search_query=search_query if search_query is not None else omit,
|
||||
search_top_k=search_top_k if search_top_k is not None else omit,
|
||||
search_max_distance=search_max_distance
|
||||
if search_max_distance is not None
|
||||
else omit,
|
||||
include_most_derived=include_most_derived
|
||||
if include_most_derived is not None
|
||||
else omit,
|
||||
max_observations=max_observations if max_observations is not None else omit,
|
||||
)
|
||||
return _Representation.from_dict(data) # type: ignore
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def get_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
) -> DeriverStatus:
|
||||
"""
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session
|
||||
Get the deriver processing status, optionally scoped to an observer, sender, and/or session.
|
||||
|
||||
Args:
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
"""
|
||||
resolved_observer_id = (
|
||||
None
|
||||
if observer is None
|
||||
else (observer if isinstance(observer, str) else observer.id)
|
||||
)
|
||||
resolved_sender_id = (
|
||||
None
|
||||
if sender is None
|
||||
else (sender if isinstance(sender, str) else sender.id)
|
||||
)
|
||||
|
||||
return self._client.workspaces.deriver_status(
|
||||
workspace_id=self.workspace_id,
|
||||
observer_id=observer_id,
|
||||
sender_id=sender_id,
|
||||
observer_id=resolved_observer_id,
|
||||
sender_id=resolved_sender_id,
|
||||
session_id=self.id,
|
||||
)
|
||||
|
||||
@validate_call
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def poll_deriver_status(
|
||||
self,
|
||||
observer_id: str | None = None,
|
||||
sender_id: str | None = None,
|
||||
observer: str | PeerBase | None = None,
|
||||
sender: str | PeerBase | None = None,
|
||||
timeout: float = Field(
|
||||
300.0,
|
||||
gt=0,
|
||||
|
|
@ -689,8 +954,8 @@ class Session(BaseModel):
|
|||
The polling estimates sleep time by assuming each work unit takes 1 second.
|
||||
|
||||
Args:
|
||||
observer_id: Optional observer ID to scope the status check
|
||||
sender_id: Optional sender ID to scope the status check
|
||||
observer: Optional observer (ID string or Peer object) to scope the status check
|
||||
sender: Optional sender (ID string or Peer object) to scope the status check
|
||||
timeout: Maximum time to poll in seconds. Defaults to 5 minutes (300 seconds).
|
||||
|
||||
Returns:
|
||||
|
|
@ -704,7 +969,7 @@ class Session(BaseModel):
|
|||
|
||||
while True:
|
||||
try:
|
||||
status = self.get_deriver_status(observer_id, sender_id)
|
||||
status = self.get_deriver_status(observer, sender)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get deriver status: {e}")
|
||||
# Sleep briefly before retrying
|
||||
|
|
|
|||
|
|
@ -2,7 +2,339 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, AsyncIterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Re-export observation types from dedicated module
|
||||
from .observations import AsyncObservationScope, Observation, ObservationScope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import SessionBase
|
||||
|
||||
__all__ = [
|
||||
"AsyncObservationScope",
|
||||
"DeductiveObservation",
|
||||
"DeductiveObservationBase",
|
||||
"DialecticStreamResponse",
|
||||
"ExplicitObservation",
|
||||
"ExplicitObservationBase",
|
||||
"Observation",
|
||||
"ObservationCreateParam",
|
||||
"ObservationMetadata",
|
||||
"ObservationScope",
|
||||
"PeerContext",
|
||||
"Representation",
|
||||
]
|
||||
|
||||
|
||||
class ObservationCreateParam(TypedDict, total=False):
|
||||
"""Parameters for creating an observation.
|
||||
|
||||
Attributes:
|
||||
content: The observation content/text (required)
|
||||
session_id: The session this observation relates to (ID string or Session object) (required)
|
||||
"""
|
||||
|
||||
content: Required[str]
|
||||
session_id: "Required[str | SessionBase]"
|
||||
|
||||
|
||||
class ObservationMetadata(BaseModel):
|
||||
"""Metadata associated with an observation."""
|
||||
|
||||
created_at: datetime
|
||||
message_ids: list[int]
|
||||
session_name: str
|
||||
|
||||
|
||||
class ExplicitObservationBase(BaseModel):
|
||||
"""Base model for explicit observations - facts literally stated."""
|
||||
|
||||
content: str = Field(description="The explicit observation")
|
||||
|
||||
|
||||
class DeductiveObservationBase(BaseModel):
|
||||
"""Base model for deductive observations - logical conclusions."""
|
||||
|
||||
premises: list[str] = Field(
|
||||
description="Supporting premises or evidence for this conclusion",
|
||||
default_factory=list,
|
||||
)
|
||||
conclusion: str = Field(description="The deductive conclusion")
|
||||
|
||||
|
||||
class ExplicitObservation(ExplicitObservationBase, ObservationMetadata):
|
||||
"""
|
||||
Explicit observation with content and metadata.
|
||||
Represents facts LITERALLY stated - direct quotes or clear paraphrases only.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format observation with timestamp and content."""
|
||||
return f"[{self.created_at.replace(microsecond=0)}] {self.content}"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
Make ExplicitObservation hashable for use in sets.
|
||||
"""
|
||||
return hash((self.content, self.created_at, self.session_name))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""
|
||||
Define equality for ExplicitObservation objects.
|
||||
Two observations are equal if content, created_at, and session_name match.
|
||||
NOTE: message_ids are not included in the equality check.
|
||||
"""
|
||||
if not isinstance(other, ExplicitObservation):
|
||||
return False
|
||||
return (
|
||||
self.content == other.content
|
||||
and self.created_at == other.created_at
|
||||
and self.session_name == other.session_name
|
||||
)
|
||||
|
||||
|
||||
class DeductiveObservation(DeductiveObservationBase, ObservationMetadata):
|
||||
"""
|
||||
Deductive observation with multiple premises and one conclusion, plus metadata.
|
||||
Represents conclusions that MUST be true given explicit facts and premises.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format observation with timestamp, conclusion, and premises."""
|
||||
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
|
||||
return f"[{self.created_at.replace(microsecond=0)}] {self.conclusion}\n{premises_text}"
|
||||
|
||||
def str_no_timestamps(self) -> str:
|
||||
"""Format observation without timestamps."""
|
||||
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
|
||||
return f"{self.conclusion}\n{premises_text}"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
Make DeductiveObservation hashable for use in sets.
|
||||
NOTE: premises are not included in the hash.
|
||||
"""
|
||||
return hash((self.conclusion, self.created_at, self.session_name))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""
|
||||
Define equality for DeductiveObservation objects.
|
||||
Two observations are equal if all their fields match.
|
||||
NOTE: premises are not included in the equality check.
|
||||
"""
|
||||
if not isinstance(other, DeductiveObservation):
|
||||
return False
|
||||
return (
|
||||
self.conclusion == other.conclusion
|
||||
and self.created_at == other.created_at
|
||||
and self.session_name == other.session_name
|
||||
)
|
||||
|
||||
|
||||
class Representation(BaseModel):
|
||||
"""
|
||||
A Representation is a traversable and diffable map of observations.
|
||||
|
||||
At the base, we have a list of explicit observations, derived from a peer's messages.
|
||||
From there, deductive observations can be made by establishing logical relationships
|
||||
between explicit observations.
|
||||
|
||||
All of a peer's observations are stored as documents in a collection. These documents
|
||||
can be queried in various ways to produce this Representation object.
|
||||
|
||||
A "working representation" is a version of this data structure representing the most
|
||||
recent observations within a single session.
|
||||
|
||||
A representation can have a maximum number of observations, which is applied
|
||||
individually to each level of reasoning. If a maximum is set, observations are
|
||||
added and removed in FIFO order.
|
||||
"""
|
||||
|
||||
explicit: list[ExplicitObservation] = Field(
|
||||
description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference",
|
||||
default_factory=list,
|
||||
)
|
||||
deductive: list[DeductiveObservation] = Field(
|
||||
description="Conclusions that MUST be true given explicit facts and premises - strict logical necessities",
|
||||
default_factory=list,
|
||||
)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
Check if the representation is empty.
|
||||
"""
|
||||
return len(self.explicit) == 0 and len(self.deductive) == 0
|
||||
|
||||
def diff_representation(self, other: "Representation") -> "Representation":
|
||||
"""
|
||||
Given this and another representation, return a new representation with only
|
||||
observations that are unique to the other.
|
||||
|
||||
Note: This only removes literal duplicates, not semantically equivalent ones.
|
||||
|
||||
Args:
|
||||
other: The representation to compare against
|
||||
|
||||
Returns:
|
||||
A new Representation containing only observations unique to other
|
||||
"""
|
||||
diff = Representation()
|
||||
diff.explicit = [o for o in other.explicit if o not in self.explicit]
|
||||
diff.deductive = [o for o in other.deductive if o not in self.deductive]
|
||||
return diff
|
||||
|
||||
def merge_representation(
|
||||
self, other: "Representation", max_observations: int | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Merge another representation object into this one.
|
||||
|
||||
This will automatically deduplicate explicit and deductive observations.
|
||||
This *preserves order* of observations so that they retain FIFO order.
|
||||
|
||||
NOTE: observations with the *same* timestamp will not have order preserved.
|
||||
That's fine though, because they are from the same timestamp...
|
||||
|
||||
Args:
|
||||
other: The representation to merge into this one
|
||||
max_observations: Optional maximum number of observations to keep per type
|
||||
"""
|
||||
# removing duplicates by going list->set->list
|
||||
self.explicit = list(set(self.explicit + other.explicit))
|
||||
self.deductive = list(set(self.deductive + other.deductive))
|
||||
# sort by created_at
|
||||
self.explicit.sort(key=lambda x: x.created_at)
|
||||
self.deductive.sort(key=lambda x: x.created_at)
|
||||
|
||||
if max_observations:
|
||||
self.explicit = self.explicit[-max_observations:]
|
||||
self.deductive = self.deductive[-max_observations:]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format representation into a clean, readable string for LLM prompts.
|
||||
NOTE: we always strip subsecond precision from the timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted string with clear sections and bullet points including temporal metadata
|
||||
|
||||
Example:
|
||||
EXPLICIT:
|
||||
1. [2025-01-01 12:00:00] The user has a dog named Rover
|
||||
2. [2025-01-01 12:01:00] The user's dog is 5 years old
|
||||
|
||||
DEDUCTIVE:
|
||||
1. [2025-01-01 12:01:00] Rover is 5 years old
|
||||
- The user has a dog named Rover
|
||||
- The user's dog is 5 years old
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def str_no_timestamps(self) -> str:
|
||||
"""
|
||||
Format representation into a clean, readable string for LLM prompts... but without timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted string with clear sections and bullet points without temporal metadata
|
||||
|
||||
Example:
|
||||
EXPLICIT:
|
||||
1. The user has a dog named Rover
|
||||
2. The user's dog is 5 years old
|
||||
|
||||
DEDUCTIVE:
|
||||
1. Rover is 5 years old
|
||||
- The user has a dog named Rover
|
||||
- The user's dog is 5 years old
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append("EXPLICIT:\n")
|
||||
for i, observation in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {observation.content}")
|
||||
parts.append("")
|
||||
|
||||
parts.append("DEDUCTIVE:\n")
|
||||
for i, observation in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. {observation.str_no_timestamps()}")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
def format_as_markdown(self) -> str:
|
||||
"""
|
||||
Format a Representation object as markdown.
|
||||
NOTE: we always strip subsecond precision from the timestamps.
|
||||
|
||||
Returns:
|
||||
Formatted markdown string with headers and lists
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
# Add explicit observations
|
||||
parts.append("## Explicit Observations\n")
|
||||
for i, obs in enumerate(self.explicit, 1):
|
||||
parts.append(f"{i}. {obs}")
|
||||
parts.append("")
|
||||
|
||||
# Add deductive observations
|
||||
parts.append("## Deductive Observations\n")
|
||||
for i, obs in enumerate(self.deductive, 1):
|
||||
parts.append(f"{i}. **Conclusion**: {obs.conclusion}")
|
||||
if obs.premises:
|
||||
parts.append(" **Premises**:")
|
||||
for premise in obs.premises:
|
||||
parts.append(f" - {premise}")
|
||||
parts.append("")
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Representation":
|
||||
"""
|
||||
Create a Representation from a dictionary (typically from API response).
|
||||
|
||||
Args:
|
||||
data: Dictionary containing 'explicit' and 'deductive' observation lists
|
||||
|
||||
Returns:
|
||||
A new Representation instance
|
||||
|
||||
Raises:
|
||||
ValidationError: If observation data is missing required fields
|
||||
"""
|
||||
explicit_data: Any = data.get("explicit", [])
|
||||
deductive_data: Any = data.get("deductive", [])
|
||||
|
||||
explicit_list = cast(
|
||||
list[Any], explicit_data if isinstance(explicit_data, list) else []
|
||||
)
|
||||
deductive_list = cast(
|
||||
list[Any], deductive_data if isinstance(deductive_data, list) else []
|
||||
)
|
||||
|
||||
return cls(
|
||||
explicit=[ExplicitObservation(**obs) for obs in explicit_list],
|
||||
deductive=[DeductiveObservation(**obs) for obs in deductive_list],
|
||||
)
|
||||
|
||||
|
||||
class DialecticStreamResponse:
|
||||
|
|
@ -62,7 +394,7 @@ class DialecticStreamResponse:
|
|||
try:
|
||||
if not isinstance(self._iterator, Iterator):
|
||||
raise TypeError("iterator must be an sync iterator, got async iterator")
|
||||
chunk = next(self._iterator) # type: ignore
|
||||
chunk = next(self._iterator)
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopIteration:
|
||||
|
|
@ -80,7 +412,7 @@ class DialecticStreamResponse:
|
|||
try:
|
||||
if not isinstance(self._iterator, AsyncIterator):
|
||||
raise TypeError("iterator must be an async iterator, got sync iterator")
|
||||
chunk = await self._iterator.__anext__() # type: ignore
|
||||
chunk = await self._iterator.__anext__()
|
||||
self._accumulated_content.append(chunk)
|
||||
return chunk
|
||||
except StopAsyncIteration:
|
||||
|
|
@ -104,3 +436,81 @@ class DialecticStreamResponse:
|
|||
def is_complete(self) -> bool:
|
||||
"""Check if the stream has finished."""
|
||||
return self._is_complete
|
||||
|
||||
|
||||
class PeerContext:
|
||||
"""
|
||||
Context for a peer, including representation and peer card.
|
||||
|
||||
This class holds both the working representation and peer card for a peer,
|
||||
typically returned from the get_context API call.
|
||||
|
||||
Attributes:
|
||||
peer_id: The ID of the observer peer
|
||||
target_id: The ID of the target peer being observed
|
||||
representation: The working representation (may be None if no observations exist)
|
||||
peer_card: List of peer card strings (may be None if no card exists)
|
||||
"""
|
||||
|
||||
peer_id: str
|
||||
target_id: str
|
||||
representation: Representation | None
|
||||
peer_card: list[str] | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
peer_id: str,
|
||||
target_id: str,
|
||||
representation: Representation | None = None,
|
||||
peer_card: list[str] | None = None,
|
||||
):
|
||||
self.peer_id = peer_id
|
||||
self.target_id = target_id
|
||||
self.representation = representation
|
||||
self.peer_card = peer_card
|
||||
|
||||
@classmethod
|
||||
def from_api_response(cls, response: Any) -> "PeerContext":
|
||||
"""
|
||||
Create a PeerContext from an API response.
|
||||
|
||||
Args:
|
||||
response: API response object with peer_id, target_id, representation, and peer_card
|
||||
|
||||
Returns:
|
||||
A new PeerContext instance
|
||||
"""
|
||||
peer_id = getattr(response, "peer_id", "") or ""
|
||||
target_id = getattr(response, "target_id", "") or ""
|
||||
|
||||
representation = None
|
||||
rep_data = getattr(response, "representation", None)
|
||||
if rep_data is not None:
|
||||
if isinstance(rep_data, dict):
|
||||
representation = Representation.from_dict(
|
||||
cast(dict[str, Any], rep_data)
|
||||
)
|
||||
elif hasattr(rep_data, "explicit") and hasattr(rep_data, "deductive"):
|
||||
representation = Representation.from_dict(
|
||||
{
|
||||
"explicit": rep_data.explicit,
|
||||
"deductive": rep_data.deductive,
|
||||
}
|
||||
)
|
||||
|
||||
peer_card = getattr(response, "peer_card", None)
|
||||
|
||||
return cls(
|
||||
peer_id=peer_id,
|
||||
target_id=target_id,
|
||||
representation=representation,
|
||||
peer_card=peer_card,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
has_rep = self.representation is not None
|
||||
has_card = self.peer_card is not None and len(self.peer_card) > 0
|
||||
return (
|
||||
f"PeerContext(peer_id={self.peer_id!r}, target_id={self.target_id!r}, "
|
||||
f"has_representation={has_rep}, has_peer_card={has_card})"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,509 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.8"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.9'" },
|
||||
{ name = "idna", marker = "python_full_version < '3.9'" },
|
||||
{ name = "sniffio", marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293, upload-time = "2024-10-13T22:18:03.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766, upload-time = "2024-10-13T22:18:01.524Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
||||
{ name = "idna", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "sniffio", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
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/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]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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/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]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "honcho-ai"
|
||||
version = "1.4.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "honcho-core" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "honcho-core", specifier = ">=1.4.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.0,<1" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0,<3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.11.13" }]
|
||||
|
||||
[[package]]
|
||||
name = "honcho-core"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic", version = "2.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "sniffio" },
|
||||
{ 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.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/c2/4d3737d5c0a75e2f59f324bed7428a9593c8edfcb7500cf402c46ad7378a/honcho_core-1.4.0.tar.gz", hash = "sha256:b30de9247763c01c3c37ed22b5d04d3c611440dd1983168f3d7d93f2cc0ecf7d", size = 126164, upload-time = "2025-08-12T19:06:54.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ac/6eb2e38376736bb1dfd26bbdfd96490bfbb6f0ddcb00b39913e93799e7d8/honcho_core-1.4.0-py3-none-any.whl", hash = "sha256:a84397fd9daf546a04f5458d7233ebd5eeafcb97af75ec60a9327db89abdaf03", size = 117602, upload-time = "2025-08-12T19:06:53.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "anyio", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "annotated-types", marker = "python_full_version < '3.9'" },
|
||||
{ name = "pydantic-core", version = "2.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "annotated-types", marker = "python_full_version >= '3.9'" },
|
||||
{ name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "typing-inspection", marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938, upload-time = "2024-12-18T11:27:14.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684, upload-time = "2024-12-18T11:27:16.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169, upload-time = "2024-12-18T11:27:22.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227, upload-time = "2024-12-18T11:27:25.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695, upload-time = "2024-12-18T11:27:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662, upload-time = "2024-12-18T11:27:30.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370, upload-time = "2024-12-18T11:27:33.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813, upload-time = "2024-12-18T11:27:37.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287, upload-time = "2024-12-18T11:27:40.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414, upload-time = "2024-12-18T11:27:43.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301, upload-time = "2024-12-18T11:27:47.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685, upload-time = "2024-12-18T11:27:50.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876, upload-time = "2024-12-18T11:27:53.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/53/13e9917fc69c0a4aea06fd63ed6a8d6cda9cf140ca9584d49c1650b0ef5e/pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", size = 1899595, upload-time = "2024-12-18T11:29:40.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/20/26c549249769ed84877f862f7bb93f89a6ee08b4bee1ed8781616b7fbb5e/pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", size = 1775010, upload-time = "2024-12-18T11:29:44.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/eb/8234e05452d92d2b102ffa1b56d801c3567e628fdc63f02080fdfc68fd5e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", size = 1830727, upload-time = "2024-12-18T11:29:46.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/df/59f915c8b929d5f61e5a46accf748a87110ba145156f9326d1a7d28912b2/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", size = 1868393, upload-time = "2024-12-18T11:29:49.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/52/81cf4071dca654d485c277c581db368b0c95b2b883f4d7b736ab54f72ddf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", size = 2040300, upload-time = "2024-12-18T11:29:51.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/00/05197ce1614f5c08d7a06e1d39d5d8e704dc81971b2719af134b844e2eaf/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", size = 2738785, upload-time = "2024-12-18T11:29:55.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a3/5f19bc495793546825ab160e530330c2afcee2281c02b5ffafd0b32ac05e/pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", size = 1996493, upload-time = "2024-12-18T11:29:57.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e8/e0102c2ec153dc3eed88aea03990e1b06cfbca532916b8a48173245afe60/pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", size = 1998544, upload-time = "2024-12-18T11:30:00.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a3/4be70845b555bd80aaee9f9812a7cf3df81550bce6dadb3cfee9c5d8421d/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", size = 2007449, upload-time = "2024-12-18T11:30:02.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/b779ed2480ba355c054e6d7ea77792467631d674b13d8257085a4bc7dcda/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", size = 2129460, upload-time = "2024-12-18T11:30:06.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f0/a6ab0681f6e95260c7fbf552874af7302f2ea37b459f9b7f00698f875492/pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", size = 2159609, upload-time = "2024-12-18T11:30:09.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/e1059506795104349712fbca647b18b3f4a7fd541c099e6259717441e1e0/pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", size = 1819886, upload-time = "2024-12-18T11:30:11.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6d/df49c17f024dfc58db0bacc7b03610058018dd2ea2eaf748ccbada4c3d06/pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad", size = 1980773, upload-time = "2024-12-18T11:30:14.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/97/3aef1ddb65c5ccd6eda9050036c956ff6ecbfe66cb7eb40f280f121a5bb0/pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", size = 1896475, upload-time = "2024-12-18T11:30:18.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/d3/5668da70e373c9904ed2f372cb52c0b996426f302e0dee2e65634c92007d/pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", size = 1772279, upload-time = "2024-12-18T11:30:20.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9e/e44b8cb0edf04a2f0a1f6425a65ee089c1d6f9c4c2dcab0209127b6fdfc2/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", size = 1829112, upload-time = "2024-12-18T11:30:23.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/90/1160d7ac700102effe11616e8119e268770f2a2aa5afb935f3ee6832987d/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", size = 1866780, upload-time = "2024-12-18T11:30:25.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/33/13983426df09a36d22c15980008f8d9c77674fc319351813b5a2739b70f3/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", size = 2037943, upload-time = "2024-12-18T11:30:28.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d7/ced164e376f6747e9158c89988c293cd524ab8d215ae4e185e9929655d5c/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", size = 2740492, upload-time = "2024-12-18T11:30:30.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/1f/3dc6e769d5b7461040778816aab2b00422427bcaa4b56cc89e9c653b2605/pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", size = 1995714, upload-time = "2024-12-18T11:30:34.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d7/a0bd09bc39283530b3f7c27033a814ef254ba3bd0b5cfd040b7abf1fe5da/pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", size = 1997163, upload-time = "2024-12-18T11:30:37.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/bb/2db4ad1762e1c5699d9b857eeb41959191980de6feb054e70f93085e1bcd/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", size = 2005217, upload-time = "2024-12-18T11:30:40.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5f/23a5a3e7b8403f8dd8fc8a6f8b49f6b55c7d715b77dcf1f8ae919eeb5628/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", size = 2127899, upload-time = "2024-12-18T11:30:42.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ae/aa38bb8dd3d89c2f1d8362dd890ee8f3b967330821d03bbe08fa01ce3766/pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", size = 2155726, upload-time = "2024-12-18T11:30:45.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/61/4f784608cc9e98f70839187117ce840480f768fed5d386f924074bf6213c/pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", size = 1817219, upload-time = "2024-12-18T11:30:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/82/bb16a68e4a1a858bb3768c2c8f1ff8d8978014e16598f001ea29a25bf1d1/pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", size = 1985382, upload-time = "2024-12-18T11:30:51.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159, upload-time = "2024-12-18T11:30:54.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331, upload-time = "2024-12-18T11:30:58.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467, upload-time = "2024-12-18T11:31:00.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797, upload-time = "2024-12-18T11:31:07.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839, upload-time = "2024-12-18T11:31:09.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861, upload-time = "2024-12-18T11:31:13.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582, upload-time = "2024-12-18T11:31:17.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985, upload-time = "2024-12-18T11:31:19.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715, upload-time = "2024-12-18T11:31:22.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/0e/dcaea00c9dbd0348b723cae82b0e0c122e0fa2b43fa933e1622fd237a3ee/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", size = 1891733, upload-time = "2024-12-18T11:31:26.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/d3/e797bba8860ce650272bda6383a9d8cad1d1c9a75a640c9d0e848076f85e/pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", size = 1768375, upload-time = "2024-12-18T11:31:29.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f7/f847b15fb14978ca2b30262548f5fc4872b2724e90f116393eb69008299d/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", size = 1822307, upload-time = "2024-12-18T11:31:33.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/63/ed80ec8255b587b2f108e514dc03eed1546cd00f0af281e699797f373f38/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", size = 1979971, upload-time = "2024-12-18T11:31:35.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/6d18308a45454a0de0e975d70171cadaf454bc7a0bf86b9c7688e313f0bb/pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", size = 1987616, upload-time = "2024-12-18T11:31:38.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/8a/05f8780f2c1081b800a7ca54c1971e291c2d07d1a50fb23c7e4aef4ed403/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", size = 1998943, upload-time = "2024-12-18T11:31:41.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/3e/fe5b6613d9e4c0038434396b46c5303f5ade871166900b357ada4766c5b7/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", size = 2116654, upload-time = "2024-12-18T11:31:44.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ad/28869f58938fad8cc84739c4e592989730bfb69b7c90a8fff138dff18e1e/pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", size = 2152292, upload-time = "2024-12-18T11:31:48.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961, upload-time = "2024-12-18T11:31:52.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.9'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
|
@ -5,6 +5,20 @@ 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.6.0] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- metadata and configuration fields to Workspace, Peer, Session, and Message objects
|
||||
- Session Clone methods
|
||||
- Peer level get_context method
|
||||
- `ObservationScope` object to perform CRUD operations on observations
|
||||
- Representation object for WorkingRepresentations
|
||||
|
||||
### Changed
|
||||
|
||||
- methods that take IDs, can all optionally take an object of the same type
|
||||
|
||||
## [1.5.0] - 2025-10-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -104,6 +104,12 @@ describe('Honcho Client', () => {
|
|||
const metadata = { name: 'Test Peer' };
|
||||
const config = { observe_me: false };
|
||||
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
await honcho.peer('test-peer', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
|
|
@ -176,6 +182,12 @@ describe('Honcho Client', () => {
|
|||
const metadata = { name: 'Test Session' };
|
||||
const config = { anonymous: true };
|
||||
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'test-session',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
await honcho.session('test-session', { metadata, config });
|
||||
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
|
||||
|
|
@ -429,9 +441,9 @@ describe('Honcho Client', () => {
|
|||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus);
|
||||
|
||||
const status = await honcho.getDeriverStatus({
|
||||
observerId: 'observer1',
|
||||
senderId: 'sender1',
|
||||
sessionId: 'session1',
|
||||
observer: 'observer1',
|
||||
sender: 'sender1',
|
||||
session: 'session1',
|
||||
});
|
||||
|
||||
expect(status).toEqual({
|
||||
|
|
@ -548,7 +560,7 @@ describe('Honcho Client', () => {
|
|||
const metadata = { updated: true };
|
||||
|
||||
await expect(honcho.updateMessage(messageId, metadata)).rejects.toThrow(
|
||||
'sessionId is required when message is a string ID'
|
||||
'session is required when message is a string ID'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Peer } from '../src/peer'
|
|||
import { Session } from '../src/session'
|
||||
import { SessionContext } from '../src/session_context'
|
||||
import { Page } from '../src/pagination'
|
||||
import { Representation } from '../src/representation'
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
let mockWorkspacesApi: any
|
||||
|
|
@ -369,16 +370,35 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
})
|
||||
|
||||
it('should handle working representation queries', async () => {
|
||||
const mockWorkingRep = {
|
||||
peer_id: 'alice',
|
||||
knowledge: 'Alice likes coffee and works as a developer',
|
||||
relationships: ['bob', 'charlie'],
|
||||
context: 'session-specific context',
|
||||
const mockWorkingRepData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Alice likes coffee',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
{
|
||||
content: 'Alice works as a developer',
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Alice is a coffee-drinking developer',
|
||||
premises: ['Alice likes coffee', 'Alice works as a developer'],
|
||||
created_at: '2024-01-01T00:02:00Z',
|
||||
message_ids: [[5, 6]],
|
||||
session_name: 'working-rep-session',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockWorkingRep
|
||||
)
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockWorkingRepData,
|
||||
})
|
||||
|
||||
const session = await honcho.session('working-rep-session')
|
||||
const alice = await honcho.peer('alice')
|
||||
|
|
@ -386,7 +406,12 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
|
||||
// Test working representation without target
|
||||
const globalRep = await session.workingRep('alice')
|
||||
expect(globalRep).toEqual(mockWorkingRep)
|
||||
expect(globalRep).toBeInstanceOf(Representation)
|
||||
expect(globalRep.explicit).toHaveLength(2)
|
||||
expect(globalRep.explicit[0].content).toBe('Alice likes coffee')
|
||||
expect(globalRep.explicit[1].content).toBe('Alice works as a developer')
|
||||
expect(globalRep.deductive).toHaveLength(1)
|
||||
expect(globalRep.deductive[0].conclusion).toBe('Alice is a coffee-drinking developer')
|
||||
expect(
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
|
||||
|
|
@ -395,7 +420,8 @@ describe('Honcho SDK Integration Tests', () => {
|
|||
})
|
||||
|
||||
// Test working representation with target
|
||||
await session.workingRep(alice, bob)
|
||||
const targetRep = await session.workingRep(alice, bob)
|
||||
expect(targetRep).toBeInstanceOf(Representation)
|
||||
expect(
|
||||
mockWorkspacesApi.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('integration-test-workspace', 'alice', {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,565 @@
|
|||
import { Honcho } from '../src/client';
|
||||
import { Peer } from '../src/peer';
|
||||
import { Session } from '../src/session';
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
workspaces: {
|
||||
peers: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
sessions: {
|
||||
list: jest.fn(),
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Metadata and Configuration Caching', () => {
|
||||
let honcho: Honcho;
|
||||
let mockClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
honcho = new Honcho({
|
||||
workspaceId: 'test-workspace',
|
||||
apiKey: 'test-key',
|
||||
environment: 'local',
|
||||
});
|
||||
|
||||
mockClient = (honcho as any)._client;
|
||||
});
|
||||
|
||||
describe('Workspace Metadata Caching', () => {
|
||||
it('should initialize with undefined metadata', () => {
|
||||
expect(honcho.metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: { theme: 'dark', version: '1.0' },
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const metadata = await honcho.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ theme: 'dark', version: '1.0' });
|
||||
expect(honcho.metadata).toEqual({ theme: 'dark', version: '1.0' });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
const metadata = await honcho.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(honcho.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { theme: 'light', version: '2.0' };
|
||||
await honcho.setMetadata(newMetadata);
|
||||
|
||||
expect(honcho.metadata).toEqual(newMetadata);
|
||||
});
|
||||
|
||||
it('should maintain cached value across multiple calls', async () => {
|
||||
const mockWorkspace = {
|
||||
id: 'test-workspace',
|
||||
metadata: { count: 1 },
|
||||
};
|
||||
mockClient.workspaces.getOrCreate.mockResolvedValue(mockWorkspace);
|
||||
|
||||
await honcho.getMetadata();
|
||||
expect(honcho.metadata).toEqual({ count: 1 });
|
||||
|
||||
// Update cache
|
||||
mockClient.workspaces.update.mockResolvedValue({});
|
||||
await honcho.setMetadata({ count: 2 });
|
||||
expect(honcho.metadata).toEqual({ count: 2 });
|
||||
|
||||
// Verify cache persists
|
||||
expect(honcho.metadata).toEqual({ count: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Metadata and Configuration Caching', () => {
|
||||
describe('Peer Constructor with metadata/config', () => {
|
||||
it('should initialize peer with provided metadata and config', async () => {
|
||||
const metadata = { name: 'Test Peer', role: 'assistant' };
|
||||
const config = { observe_me: false };
|
||||
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'peer1',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
const peer = await honcho.peer('peer1', { metadata, config });
|
||||
|
||||
expect(peer.metadata).toEqual(metadata);
|
||||
expect(peer.configuration).toEqual(config);
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'peer1', metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize peer without metadata/config', async () => {
|
||||
const peer = await honcho.peer('peer1');
|
||||
|
||||
expect(peer.metadata).toBeUndefined();
|
||||
expect(peer.configuration).toBeUndefined();
|
||||
expect(mockClient.workspaces.peers.getOrCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Metadata Caching', () => {
|
||||
let peer: Peer;
|
||||
|
||||
beforeEach(() => {
|
||||
peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Alice', role: 'user' },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const metadata = await peer.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ name: 'Alice', role: 'user' });
|
||||
expect(peer.metadata).toEqual({ name: 'Alice', role: 'user' });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const metadata = await peer.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(peer.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { name: 'Bob', role: 'admin' };
|
||||
await peer.setMetadata(newMetadata);
|
||||
|
||||
expect(peer.metadata).toEqual(newMetadata);
|
||||
});
|
||||
|
||||
it('should maintain cached value across operations', async () => {
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { score: 100 },
|
||||
});
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
// Get initial metadata
|
||||
await peer.getMetadata();
|
||||
expect(peer.metadata).toEqual({ score: 100 });
|
||||
|
||||
// Update metadata
|
||||
await peer.setMetadata({ score: 200 });
|
||||
expect(peer.metadata).toEqual({ score: 200 });
|
||||
|
||||
// Verify cache persists
|
||||
expect(peer.metadata).toEqual({ score: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer Configuration Caching', () => {
|
||||
let peer: Peer;
|
||||
|
||||
beforeEach(() => {
|
||||
peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache configuration after getConfig call', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: { observe_me: true, observe_others: false },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getConfig();
|
||||
|
||||
expect(config).toEqual({ observe_me: true, observe_others: false });
|
||||
expect(peer.configuration).toEqual({ observe_me: true, observe_others: false });
|
||||
});
|
||||
|
||||
it('should cache empty object when configuration is null', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: null,
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
expect(peer.configuration).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached configuration after setConfig call', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { observe_me: false, observe_others: true };
|
||||
await peer.setConfig(newConfig);
|
||||
|
||||
expect(peer.configuration).toEqual(newConfig);
|
||||
});
|
||||
|
||||
it('should support deprecated getPeerConfig method', async () => {
|
||||
const mockPeer = {
|
||||
id: 'test-peer',
|
||||
configuration: { observe_me: true },
|
||||
};
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue(mockPeer);
|
||||
|
||||
const config = await peer.getPeerConfig();
|
||||
|
||||
expect(config).toEqual({ observe_me: true });
|
||||
expect(peer.configuration).toEqual({ observe_me: true });
|
||||
});
|
||||
|
||||
it('should support deprecated setPeerConfig method', async () => {
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { observe_me: false };
|
||||
await peer.setPeerConfig(newConfig);
|
||||
|
||||
expect(peer.configuration).toEqual(newConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Peer List with Cached Data', () => {
|
||||
it('should populate metadata and config when listing peers', async () => {
|
||||
const mockPeersData = {
|
||||
items: [
|
||||
{
|
||||
id: 'peer1',
|
||||
metadata: { name: 'Alice' },
|
||||
configuration: { observe_me: true },
|
||||
},
|
||||
{
|
||||
id: 'peer2',
|
||||
metadata: { name: 'Bob' },
|
||||
configuration: { observe_me: false },
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
|
||||
|
||||
const peersPage = await honcho.getPeers();
|
||||
const peers = peersPage.items;
|
||||
|
||||
expect(peers[0].metadata).toEqual({ name: 'Alice' });
|
||||
expect(peers[0].configuration).toEqual({ observe_me: true });
|
||||
expect(peers[1].metadata).toEqual({ name: 'Bob' });
|
||||
expect(peers[1].configuration).toEqual({ observe_me: false });
|
||||
});
|
||||
|
||||
it('should handle null metadata and config in peer list', async () => {
|
||||
const mockPeersData = {
|
||||
items: [
|
||||
{
|
||||
id: 'peer1',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.peers.list.mockResolvedValue(mockPeersData);
|
||||
|
||||
const peersPage = await honcho.getPeers();
|
||||
const peers = peersPage.items;
|
||||
|
||||
expect(peers[0].metadata).toBeUndefined();
|
||||
expect(peers[0].configuration).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Metadata and Configuration Caching', () => {
|
||||
describe('Session Constructor with metadata/config', () => {
|
||||
it('should initialize session with provided metadata and config', async () => {
|
||||
const metadata = { title: 'Test Session', tags: ['important'] };
|
||||
const config = { anonymous: false };
|
||||
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'session1',
|
||||
metadata: metadata,
|
||||
configuration: config,
|
||||
});
|
||||
|
||||
const session = await honcho.session('session1', { metadata, config });
|
||||
|
||||
expect(session.metadata).toEqual(metadata);
|
||||
expect(session.configuration).toEqual(config);
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
{ id: 'session1', metadata, configuration: config }
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize session without metadata/config', async () => {
|
||||
const session = await honcho.session('session1');
|
||||
|
||||
expect(session.metadata).toBeUndefined();
|
||||
expect(session.configuration).toBeUndefined();
|
||||
expect(mockClient.workspaces.sessions.getOrCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Metadata Caching', () => {
|
||||
let session: Session;
|
||||
|
||||
beforeEach(() => {
|
||||
session = new Session('test-session', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache metadata after getMetadata call', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
metadata: { title: 'Chat Session', active: true },
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const metadata = await session.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({ title: 'Chat Session', active: true });
|
||||
expect(session.metadata).toEqual({ title: 'Chat Session', active: true });
|
||||
});
|
||||
|
||||
it('should cache empty object when metadata is null', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
metadata: null,
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const metadata = await session.getMetadata();
|
||||
|
||||
expect(metadata).toEqual({});
|
||||
expect(session.metadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached metadata after setMetadata call', async () => {
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
const newMetadata = { title: 'Updated Session', active: false };
|
||||
await session.setMetadata(newMetadata);
|
||||
|
||||
expect(session.metadata).toEqual(newMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Configuration Caching', () => {
|
||||
let session: Session;
|
||||
|
||||
beforeEach(() => {
|
||||
session = new Session('test-session', 'test-workspace', mockClient);
|
||||
});
|
||||
|
||||
it('should cache configuration after getConfig call', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
configuration: { anonymous: true, summarize: false },
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const config = await session.getConfig();
|
||||
|
||||
expect(config).toEqual({ anonymous: true, summarize: false });
|
||||
expect(session.configuration).toEqual({ anonymous: true, summarize: false });
|
||||
});
|
||||
|
||||
it('should cache empty object when configuration is null', async () => {
|
||||
const mockSession = {
|
||||
id: 'test-session',
|
||||
configuration: null,
|
||||
};
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const config = await session.getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
expect(session.configuration).toEqual({});
|
||||
});
|
||||
|
||||
it('should update cached configuration after setConfig call', async () => {
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
const newConfig = { anonymous: false, summarize: true };
|
||||
await session.setConfig(newConfig);
|
||||
|
||||
expect(session.configuration).toEqual(newConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session List with Cached Data', () => {
|
||||
it('should populate metadata and config when listing sessions', async () => {
|
||||
const mockSessionsData = {
|
||||
items: [
|
||||
{
|
||||
id: 'session1',
|
||||
metadata: { title: 'Session 1' },
|
||||
configuration: { anonymous: true },
|
||||
},
|
||||
{
|
||||
id: 'session2',
|
||||
metadata: { title: 'Session 2' },
|
||||
configuration: { anonymous: false },
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
size: 2,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
|
||||
|
||||
const sessionsPage = await honcho.getSessions();
|
||||
const sessions = sessionsPage.items;
|
||||
|
||||
expect(sessions[0].metadata).toEqual({ title: 'Session 1' });
|
||||
expect(sessions[0].configuration).toEqual({ anonymous: true });
|
||||
expect(sessions[1].metadata).toEqual({ title: 'Session 2' });
|
||||
expect(sessions[1].configuration).toEqual({ anonymous: false });
|
||||
});
|
||||
|
||||
it('should handle null metadata and config in session list', async () => {
|
||||
const mockSessionsData = {
|
||||
items: [
|
||||
{
|
||||
id: 'session1',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
size: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
mockClient.workspaces.sessions.list.mockResolvedValue(mockSessionsData);
|
||||
|
||||
const sessionsPage = await honcho.getSessions();
|
||||
const sessions = sessionsPage.items;
|
||||
|
||||
expect(sessions[0].metadata).toBeUndefined();
|
||||
expect(sessions[0].configuration).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration: Combined Metadata and Configuration Operations', () => {
|
||||
it('should cache both metadata and config for peers independently', async () => {
|
||||
const peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
|
||||
// Set up mocks
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Test' },
|
||||
configuration: { observe_me: true },
|
||||
});
|
||||
mockClient.workspaces.peers.update.mockResolvedValue({});
|
||||
|
||||
// Get both metadata and config
|
||||
await peer.getMetadata();
|
||||
await peer.getConfig();
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Test' });
|
||||
expect(peer.configuration).toEqual({ observe_me: true });
|
||||
|
||||
// Update metadata only
|
||||
await peer.setMetadata({ name: 'Updated' });
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Updated' });
|
||||
expect(peer.configuration).toEqual({ observe_me: true }); // Should remain unchanged
|
||||
|
||||
// Update config only
|
||||
await peer.setConfig({ observe_me: false });
|
||||
|
||||
expect(peer.metadata).toEqual({ name: 'Updated' }); // Should remain unchanged
|
||||
expect(peer.configuration).toEqual({ observe_me: false });
|
||||
});
|
||||
|
||||
it('should cache both metadata and config for sessions independently', async () => {
|
||||
const session = new Session('test-session', 'test-workspace', mockClient);
|
||||
|
||||
// Set up mocks
|
||||
mockClient.workspaces.sessions.getOrCreate.mockResolvedValue({
|
||||
id: 'test-session',
|
||||
metadata: { title: 'Test' },
|
||||
configuration: { anonymous: true },
|
||||
});
|
||||
mockClient.workspaces.sessions.update.mockResolvedValue({});
|
||||
|
||||
// Get both metadata and config
|
||||
await session.getMetadata();
|
||||
await session.getConfig();
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Test' });
|
||||
expect(session.configuration).toEqual({ anonymous: true });
|
||||
|
||||
// Update metadata only
|
||||
await session.setMetadata({ title: 'Updated' });
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Updated' });
|
||||
expect(session.configuration).toEqual({ anonymous: true }); // Should remain unchanged
|
||||
|
||||
// Update config only
|
||||
await session.setConfig({ anonymous: false });
|
||||
|
||||
expect(session.metadata).toEqual({ title: 'Updated' }); // Should remain unchanged
|
||||
expect(session.configuration).toEqual({ anonymous: false });
|
||||
});
|
||||
|
||||
it('should reduce API calls by using cached values', async () => {
|
||||
const peer = new Peer('test-peer', 'test-workspace', mockClient);
|
||||
|
||||
// Initial fetch
|
||||
mockClient.workspaces.peers.getOrCreate.mockResolvedValue({
|
||||
id: 'test-peer',
|
||||
metadata: { name: 'Test' },
|
||||
});
|
||||
|
||||
await peer.getMetadata();
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Access cached value directly (without API call)
|
||||
const cachedMetadata = peer.metadata;
|
||||
expect(cachedMetadata).toEqual({ name: 'Test' });
|
||||
expect(mockClient.workspaces.peers.getOrCreate).toHaveBeenCalledTimes(1); // Still only 1 call
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import { Peer } from '../src/peer';
|
|||
import { Session } from '../src/session';
|
||||
import { Page } from '../src/pagination';
|
||||
import { Honcho } from '../src/client';
|
||||
import { Representation } from '../src/representation';
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
|
|
@ -125,7 +126,7 @@ describe('Peer', () => {
|
|||
const mockResponse = { content: 'Session-specific response' };
|
||||
mockClient.workspaces.peers.chat.mockResolvedValue(mockResponse);
|
||||
|
||||
await peer.chat('Hello', { sessionId: 'session-123' });
|
||||
await peer.chat('Hello', { session: 'session-123' });
|
||||
|
||||
expect(mockClient.workspaces.peers.chat).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
|
|
@ -208,6 +209,8 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: 'Test content',
|
||||
metadata: undefined,
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -219,6 +222,39 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: 'Hello there',
|
||||
metadata: { importance: 'high', category: 'greeting' },
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create message object with configuration', () => {
|
||||
const configuration = { deriver: { enabled: false } };
|
||||
const message = peer.message('Test content', { configuration });
|
||||
|
||||
expect(message).toEqual({
|
||||
peer_id: 'test-peer',
|
||||
content: 'Test content',
|
||||
metadata: undefined,
|
||||
configuration: { deriver: { enabled: false } },
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create message object with metadata, configuration, and timestamp', () => {
|
||||
const metadata = { importance: 'high' };
|
||||
const configuration = { deriver: { enabled: false }, peer_card: { create: false } };
|
||||
const message = peer.message('Full options test', {
|
||||
metadata,
|
||||
configuration,
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
|
||||
expect(message).toEqual({
|
||||
peer_id: 'test-peer',
|
||||
content: 'Full options test',
|
||||
metadata: { importance: 'high' },
|
||||
configuration: { deriver: { enabled: false }, peer_card: { create: false } },
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -229,6 +265,8 @@ describe('Peer', () => {
|
|||
peer_id: 'test-peer',
|
||||
content: '',
|
||||
metadata: undefined,
|
||||
configuration: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -514,4 +552,423 @@ describe('Peer', () => {
|
|||
await expect(peer.card()).rejects.toThrow('Card fetch failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('workingRep', () => {
|
||||
beforeEach(() => {
|
||||
mockClient.workspaces.peers.workingRepresentation = jest.fn();
|
||||
});
|
||||
|
||||
it('should get working representation with no parameters', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Observation 1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
{
|
||||
content: 'Observation 2',
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Conclusion 1',
|
||||
premises: ['Observation 1', 'Observation 2'],
|
||||
created_at: '2024-01-01T00:02:00Z',
|
||||
message_ids: [[5, 6]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep();
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(2);
|
||||
expect(result.explicit[0].content).toBe('Observation 1');
|
||||
expect(result.explicit[1].content).toBe('Observation 2');
|
||||
expect(result.deductive).toHaveLength(1);
|
||||
expect(result.deductive[0].conclusion).toBe('Conclusion 1');
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with session as string', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Session-scoped observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep('session-123');
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Session-scoped observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with session as Session object', async () => {
|
||||
const session = new Session('session-123', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Session object observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(session);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Session object observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with target as string', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: "Observer's view of target",
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, 'target-peer');
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe("Observer's view of target");
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: 'target-peer',
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with target as Peer object', async () => {
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: "Observer's view of target peer object",
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, targetPeer);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe("Observer's view of target peer object");
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: 'target-peer',
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with search query', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Query-curated observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
undefined,
|
||||
undefined,
|
||||
{ searchQuery: 'programming' }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Query-curated observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: 'programming',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with custom size', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Limited observations',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(undefined, undefined, { maxObservations: 10 });
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Limited observations');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with all parameters', async () => {
|
||||
const session = new Session('session-123', 'test-workspace', mockClient);
|
||||
const targetPeer = new Peer('target-peer', 'test-workspace', mockClient);
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Fully parameterized observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
deductive: [
|
||||
{
|
||||
conclusion: 'Conclusion with all params',
|
||||
premises: ['Fully parameterized observation'],
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
message_ids: [[3, 4]],
|
||||
session_name: 'session-123',
|
||||
},
|
||||
],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
session,
|
||||
targetPeer,
|
||||
{ searchQuery: 'Python programming', maxObservations: 25 }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('Fully parameterized observation');
|
||||
expect(result.deductive).toHaveLength(1);
|
||||
expect(result.deductive[0].conclusion).toBe('Conclusion with all params');
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-123',
|
||||
target: 'target-peer',
|
||||
search_query: 'Python programming',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 25,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get working representation with string session and string target', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'String params observation',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'session-456',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
const result = await peer.workingRep(
|
||||
'session-456',
|
||||
'target-peer-123',
|
||||
{ searchQuery: 'machine learning', maxObservations: 50 }
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Representation);
|
||||
expect(result.explicit).toHaveLength(1);
|
||||
expect(result.explicit[0].content).toBe('String params observation');
|
||||
expect(result.deductive).toHaveLength(0);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: 'session-456',
|
||||
target: 'target-peer-123',
|
||||
search_query: 'machine learning',
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle boundary size values', async () => {
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Boundary test',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
};
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
});
|
||||
|
||||
// Test size = 1
|
||||
const result1 = await peer.workingRep(undefined, undefined, { maxObservations: 1 });
|
||||
expect(result1).toBeInstanceOf(Representation);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenLastCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 1,
|
||||
});
|
||||
|
||||
// Test size = 100
|
||||
const result2 = await peer.workingRep(undefined, undefined, { maxObservations: 100 });
|
||||
expect(result2).toBeInstanceOf(Representation);
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenLastCalledWith('test-workspace', 'test-peer', {
|
||||
session_id: undefined,
|
||||
target: undefined,
|
||||
search_query: undefined,
|
||||
search_top_k: undefined,
|
||||
search_max_distance: undefined,
|
||||
include_most_derived: undefined,
|
||||
max_observations: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.peers.workingRepresentation.mockRejectedValue(
|
||||
new Error('Working representation fetch failed')
|
||||
);
|
||||
|
||||
await expect(peer.workingRep()).rejects.toThrow(
|
||||
'Working representation fetch failed'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Peer } from '../src/peer'
|
|||
import { Page } from '../src/pagination'
|
||||
import { SessionContext } from '../src/session_context'
|
||||
import { Honcho } from '../src/client'
|
||||
import { Representation } from '../src/representation'
|
||||
|
||||
// Mock the @honcho-ai/core module
|
||||
jest.mock('@honcho-ai/core', () => {
|
||||
|
|
@ -25,6 +26,7 @@ jest.mock('@honcho-ai/core', () => {
|
|||
getOrCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
clone: jest.fn(),
|
||||
getContext: jest.fn(),
|
||||
search: jest.fn(),
|
||||
},
|
||||
|
|
@ -568,6 +570,62 @@ describe('Session', () => {
|
|||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should return list of messages when created', async () => {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: 'msg1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Hello',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: 'msg2',
|
||||
peer_id: 'peer2',
|
||||
content: 'Hi there',
|
||||
created_at: '2023-01-01T00:00:01Z',
|
||||
metadata: {},
|
||||
},
|
||||
]
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue(
|
||||
mockMessages
|
||||
)
|
||||
|
||||
const messages = [
|
||||
{ peer_id: 'peer1', content: 'Hello' },
|
||||
{ peer_id: 'peer2', content: 'Hi there' },
|
||||
]
|
||||
const result = await session.addMessages(messages)
|
||||
|
||||
expect(result).toEqual(mockMessages)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe('msg1')
|
||||
expect(result[0].content).toBe('Hello')
|
||||
expect(result[1].id).toBe('msg2')
|
||||
expect(result[1].content).toBe('Hi there')
|
||||
})
|
||||
|
||||
it('should return single message when adding single message', async () => {
|
||||
const mockMessage = {
|
||||
id: 'msg1',
|
||||
peer_id: 'peer1',
|
||||
content: 'Hello',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
metadata: {},
|
||||
}
|
||||
mockClient.workspaces.sessions.messages.create.mockResolvedValue([
|
||||
mockMessage,
|
||||
])
|
||||
|
||||
const message = { peer_id: 'peer1', content: 'Hello' }
|
||||
const result = await session.addMessages(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual(mockMessage)
|
||||
expect(result[0].id).toBe('msg1')
|
||||
expect(result[0].content).toBe('Hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessages', () => {
|
||||
|
|
@ -847,18 +905,27 @@ describe('Session', () => {
|
|||
|
||||
describe('workingRep', () => {
|
||||
it('should get working representation with peer string', async () => {
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
knowledge: 'Some knowledge about the peer',
|
||||
relationships: ['peer2', 'peer3'],
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Some knowledge about the peer',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep('peer1')
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('Some knowledge about the peer')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -869,17 +936,27 @@ describe('Session', () => {
|
|||
|
||||
it('should get working representation with Peer object', async () => {
|
||||
const peer = new Peer('peer1', 'test-workspace', mockClient)
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
knowledge: 'Some knowledge',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'Some knowledge',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep(peer)
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('Some knowledge')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -889,17 +966,27 @@ describe('Session', () => {
|
|||
})
|
||||
|
||||
it('should get working representation with target peer string', async () => {
|
||||
const mockRepresentation = {
|
||||
peer_id: 'peer1',
|
||||
target_knowledge: 'What peer1 knows about target',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'What peer1 knows about target',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep('peer1', 'target-peer')
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('What peer1 knows about target')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -911,17 +998,27 @@ describe('Session', () => {
|
|||
it('should get working representation with target Peer object', async () => {
|
||||
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',
|
||||
const mockRepresentationData = {
|
||||
explicit: [
|
||||
{
|
||||
content: 'What peer1 knows about target',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
message_ids: [[1, 2]],
|
||||
session_name: 'test-session',
|
||||
},
|
||||
],
|
||||
deductive: [],
|
||||
}
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue(
|
||||
mockRepresentation
|
||||
)
|
||||
mockClient.workspaces.peers.workingRepresentation.mockResolvedValue({
|
||||
representation: mockRepresentationData,
|
||||
})
|
||||
|
||||
const result = await session.workingRep(peer, target)
|
||||
|
||||
expect(result).toEqual(mockRepresentation)
|
||||
expect(result).toBeInstanceOf(Representation)
|
||||
expect(result.explicit).toHaveLength(1)
|
||||
expect(result.explicit[0].content).toBe('What peer1 knows about target')
|
||||
expect(result.deductive).toHaveLength(0)
|
||||
expect(
|
||||
mockClient.workspaces.peers.workingRepresentation
|
||||
).toHaveBeenCalledWith('test-workspace', 'peer1', {
|
||||
|
|
@ -959,6 +1056,74 @@ describe('Session', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('clone', () => {
|
||||
it('should clone the session without messageId', async () => {
|
||||
const mockClonedSession = {
|
||||
id: 'cloned-session-id',
|
||||
workspace_id: 'test-workspace',
|
||||
metadata: { cloned: true },
|
||||
configuration: { test: 'config' },
|
||||
}
|
||||
mockClient.workspaces.sessions.clone.mockResolvedValue(mockClonedSession)
|
||||
|
||||
const clonedSession = await session.clone()
|
||||
|
||||
expect(clonedSession).toBeInstanceOf(Session)
|
||||
expect(clonedSession.id).toBe('cloned-session-id')
|
||||
expect(clonedSession.workspaceId).toBe('test-workspace')
|
||||
expect(clonedSession.metadata).toEqual({ cloned: true })
|
||||
expect(clonedSession.configuration).toEqual({ test: 'config' })
|
||||
expect(mockClient.workspaces.sessions.clone).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should clone the session with messageId cutoff', async () => {
|
||||
const mockClonedSession = {
|
||||
id: 'cloned-session-cutoff',
|
||||
workspace_id: 'test-workspace',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
}
|
||||
mockClient.workspaces.sessions.clone.mockResolvedValue(mockClonedSession)
|
||||
|
||||
const clonedSession = await session.clone('msg-123')
|
||||
|
||||
expect(clonedSession).toBeInstanceOf(Session)
|
||||
expect(clonedSession.id).toBe('cloned-session-cutoff')
|
||||
expect(mockClient.workspaces.sessions.clone).toHaveBeenCalledWith(
|
||||
'test-workspace',
|
||||
'test-session',
|
||||
{ message_id: 'msg-123' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle null metadata and configuration', async () => {
|
||||
const mockClonedSession = {
|
||||
id: 'cloned-session-null',
|
||||
workspace_id: 'test-workspace',
|
||||
metadata: null,
|
||||
configuration: null,
|
||||
}
|
||||
mockClient.workspaces.sessions.clone.mockResolvedValue(mockClonedSession)
|
||||
|
||||
const clonedSession = await session.clone()
|
||||
|
||||
expect(clonedSession.metadata).toBeUndefined()
|
||||
expect(clonedSession.configuration).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
mockClient.workspaces.sessions.clone.mockRejectedValue(
|
||||
new Error('Failed to clone session')
|
||||
)
|
||||
|
||||
await expect(session.clone()).rejects.toThrow('Failed to clone session')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDeriverStatus', () => {
|
||||
it('should return deriver status without options', async () => {
|
||||
const mockStatus = {
|
||||
|
|
@ -995,8 +1160,8 @@ describe('Session', () => {
|
|||
mockClient.workspaces.deriverStatus.mockResolvedValue(mockStatus)
|
||||
|
||||
const status = await session.getDeriverStatus({
|
||||
observerId: 'observer1',
|
||||
senderId: 'sender1',
|
||||
observer: 'observer1',
|
||||
sender: 'sender1',
|
||||
})
|
||||
|
||||
expect(status).toEqual({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"": {
|
||||
"name": "@honcho-ai/sdk",
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.5.1",
|
||||
"@honcho-ai/core": "^1.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0",
|
||||
},
|
||||
|
|
@ -20,11 +20,11 @@
|
|||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
|
|
@ -38,13 +38,13 @@
|
|||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="],
|
||||
|
||||
|
|
@ -82,33 +82,31 @@
|
|||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.5.1", "", { "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-lbYtMTcL2AxdcIl5ZKenogeTlVMnE7buJWvAFOCLp0yQxcezyA/R9FPvLz2UGRApLsiplLNWhftML+3QBjIIJA=="],
|
||||
"@honcho-ai/core": ["@honcho-ai/core@1.6.1", "", { "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-sfKIqAIybP/yj6iXGQLFgrqlX1dA7OuAK86p8sy8XiT64ZpEYpEz6viifAsm65LRZMgb0HPTFeBGodseUSoqVQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
|
|
@ -158,40 +156,6 @@
|
|||
|
||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
|
||||
|
||||
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
|
||||
|
||||
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
|
||||
|
||||
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
|
||||
|
||||
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
|
@ -210,22 +174,18 @@
|
|||
|
||||
"@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.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
|
||||
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
|
||||
|
||||
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
|
@ -236,8 +196,6 @@
|
|||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
|
@ -254,13 +212,13 @@
|
|||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.26.2", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
|
||||
"browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="],
|
||||
|
||||
"bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="],
|
||||
|
||||
|
|
@ -274,7 +232,7 @@
|
|||
|
||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001746", "", {}, "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
|
|
@ -288,7 +246,7 @@
|
|||
|
||||
"co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="],
|
||||
|
||||
"collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="],
|
||||
"collect-v8-coverage": ["collect-v8-coverage@1.0.3", "", {}, "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
|
|
@ -302,8 +260,6 @@
|
|||
|
||||
"create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="],
|
||||
|
||||
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
|
@ -316,13 +272,11 @@
|
|||
|
||||
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
|
||||
|
||||
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||
|
||||
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.228", "", {}, "sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.263", "", {}, "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg=="],
|
||||
|
||||
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
|
||||
|
||||
|
|
@ -360,7 +314,7 @@
|
|||
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
|
|
@ -492,7 +446,7 @@
|
|||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
|
|
@ -546,7 +500,7 @@
|
|||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
|
|
@ -590,7 +544,7 @@
|
|||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
|
||||
|
||||
|
|
@ -598,7 +552,7 @@
|
|||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
|
|
@ -642,9 +596,7 @@
|
|||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-jest": ["ts-jest@29.4.4", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw=="],
|
||||
|
||||
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
|
||||
"ts-jest": ["ts-jest@29.4.6", "", { "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.3", "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-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
|
|
@ -654,11 +606,9 @@
|
|||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
|
||||
|
||||
"v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
|
||||
|
||||
|
|
@ -688,8 +638,6 @@
|
|||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.0.0", "", {}, "sha512-9diLdTPc/L7w/5jI4C3gHYNiGHDV9IZYxo1e5LSD8cabi65WVTWWb+g2BGPEpUUCOxR4D+6O5B0AzyMdUAXwrw=="],
|
||||
|
|
@ -698,9 +646,7 @@
|
|||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.129", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A=="],
|
||||
"@honcho-ai/core/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@honcho-ai/sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "Official DX Optimized TypeScript SDK for Honcho",
|
||||
"author": "Plastic Labs <hello@plasticlabs.ai>",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@honcho-ai/core": "^1.5.1",
|
||||
"@honcho-ai/core": "^1.6.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"zod": "4.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { Peer } from './peer'
|
|||
import { Session } from './session'
|
||||
import {
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
type HonchoConfig,
|
||||
|
|
@ -28,6 +27,8 @@ import {
|
|||
SessionIdSchema,
|
||||
type SessionMetadata,
|
||||
SessionMetadataSchema,
|
||||
type WorkspaceConfig,
|
||||
WorkspaceConfigSchema,
|
||||
type WorkspaceMetadata,
|
||||
WorkspaceMetadataSchema,
|
||||
} from './validation'
|
||||
|
|
@ -62,6 +63,36 @@ export class Honcho {
|
|||
* Reference to the core Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this workspace.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this workspace.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this workspace. May be stale if the workspace
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this workspace. May be stale if the workspace
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the underlying @honcho-ai/core client. The @honcho-ai/core client is the raw Stainless-generated client,
|
||||
|
|
@ -155,17 +186,26 @@ export class Honcho {
|
|||
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,
|
||||
})
|
||||
const peerData = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{
|
||||
id: validatedId,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
)
|
||||
return new Peer(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
peerData.metadata ?? undefined,
|
||||
peerData.configuration ?? undefined
|
||||
)
|
||||
}
|
||||
|
||||
return peer
|
||||
return new Peer(validatedId, this.workspaceId, this._client)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,7 +225,14 @@ export class Honcho {
|
|||
)
|
||||
return new Page(
|
||||
peersPage,
|
||||
(peer) => new Peer(peer.id, this.workspaceId, this._client)
|
||||
(peer) =>
|
||||
new Peer(
|
||||
peer.id,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
peer.metadata ?? undefined,
|
||||
peer.configuration ?? undefined
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -224,17 +271,26 @@ export class Honcho {
|
|||
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,
|
||||
})
|
||||
const sessionData = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{
|
||||
id: validatedId,
|
||||
configuration: validatedConfig,
|
||||
metadata: validatedMetadata,
|
||||
}
|
||||
)
|
||||
return new Session(
|
||||
validatedId,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
sessionData.metadata ?? undefined,
|
||||
sessionData.configuration ?? undefined
|
||||
)
|
||||
}
|
||||
|
||||
return session
|
||||
return new Session(validatedId, this.workspaceId, this._client)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -255,7 +311,14 @@ export class Honcho {
|
|||
)
|
||||
return new Page(
|
||||
sessionsPage,
|
||||
(session) => new Session(session.id, this.workspaceId, this._client)
|
||||
(session) =>
|
||||
new Session(
|
||||
session.id,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
session.metadata ?? undefined,
|
||||
session.configuration ?? undefined
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +327,8 @@ export class Honcho {
|
|||
*
|
||||
* 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.
|
||||
* key-value data associated with the workspace. This method also updates the
|
||||
* cached metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the workspace's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -273,7 +337,8 @@ export class Honcho {
|
|||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
return workspace.metadata || {}
|
||||
this._metadata = workspace.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,6 +346,7 @@ export class Honcho {
|
|||
*
|
||||
* Makes an API call to update the metadata associated with the current workspace.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* This method also updates the cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with the workspace.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -290,6 +356,57 @@ export class Honcho {
|
|||
await this._client.workspaces.update(this.workspaceId, {
|
||||
metadata: validatedMetadata,
|
||||
})
|
||||
this._metadata = validatedMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for the current workspace.
|
||||
*
|
||||
* Makes an API call to retrieve configuration associated with the current workspace.
|
||||
* Configuration includes settings that control workspace behavior.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the workspace's configuration.
|
||||
* Returns an empty dictionary if no configuration is set
|
||||
*/
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
this._configuration = workspace.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration for the current workspace.
|
||||
*
|
||||
* Makes an API call to update the configuration associated with the current workspace.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @param configuration - A dictionary of configuration to associate with the workspace.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setConfig(configuration: WorkspaceConfig): Promise<void> {
|
||||
const validatedConfig = WorkspaceConfigSchema.parse(configuration)
|
||||
await this._client.workspaces.update(this.workspaceId, {
|
||||
configuration: validatedConfig,
|
||||
})
|
||||
this._configuration = validatedConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for the current workspace.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with the current workspace and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const workspace = await this._client.workspaces.getOrCreate({
|
||||
id: this.workspaceId,
|
||||
})
|
||||
this._metadata = workspace.metadata || {}
|
||||
this._configuration = workspace.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -368,28 +485,47 @@ export class Honcho {
|
|||
* 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
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.session - Optional session (ID string or Session object) to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(options?: DeriverStatusOptions): Promise<{
|
||||
async getDeriverStatus(
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'observerId' | 'senderId' | 'sessionId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
const resolvedObserverId = options?.observer
|
||||
? typeof options.observer === 'string'
|
||||
? options.observer
|
||||
: options.observer.id
|
||||
: undefined
|
||||
const resolvedSenderId = options?.sender
|
||||
? typeof options.sender === 'string'
|
||||
? options.sender
|
||||
: options.sender.id
|
||||
: undefined
|
||||
const resolvedSessionId = options?.session
|
||||
? typeof options.session === 'string'
|
||||
? options.session
|
||||
: options.session.id
|
||||
: 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
|
||||
if (resolvedObserverId) queryParams.observer_id = resolvedObserverId
|
||||
if (resolvedSenderId) queryParams.sender_id = resolvedSenderId
|
||||
if (resolvedSessionId) queryParams.session_id = resolvedSessionId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
|
|
@ -413,28 +549,34 @@ 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.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.session - Optional session (ID string or Session object) 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?: DeriverStatusOptions): Promise<{
|
||||
async pollDeriverStatus(
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'observerId' | 'senderId' | 'sessionId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
inProgressWorkUnits: number
|
||||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
const status = await this.getDeriverStatus(options)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
|
@ -471,14 +613,14 @@ export class Honcho {
|
|||
*
|
||||
* @param message - Either a Message object or a message ID string
|
||||
* @param metadata - The metadata to update for the message
|
||||
* @param sessionId - The ID of the session (required if message is a string ID, ignored if message is a Message object)
|
||||
* @param session - The session (ID string or Session object) - required if message is a string ID, ignored if message is a Message object
|
||||
* @returns Promise resolving to the updated Message object
|
||||
* @throws Error if message is a string ID but sessionId is not provided
|
||||
* @throws Error if message is a string ID but session is not provided
|
||||
*/
|
||||
async updateMessage(
|
||||
message: Message | string,
|
||||
metadata: Record<string, unknown>,
|
||||
sessionId?: string
|
||||
session?: string | Session
|
||||
): Promise<Message> {
|
||||
const validatedMetadata = MessageMetadataSchema.parse(metadata)
|
||||
let messageId: string
|
||||
|
|
@ -486,10 +628,10 @@ export class Honcho {
|
|||
|
||||
if (typeof message === 'string') {
|
||||
messageId = message
|
||||
if (!sessionId) {
|
||||
throw new Error('sessionId is required when message is a string ID')
|
||||
if (!session) {
|
||||
throw new Error('session is required when message is a string ID')
|
||||
}
|
||||
resolvedSessionId = sessionId
|
||||
resolvedSessionId = typeof session === 'string' ? session : session.id
|
||||
} else {
|
||||
messageId = message.id
|
||||
resolvedSessionId = message.session_id
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@
|
|||
|
||||
export type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
export { Honcho } from './client'
|
||||
export { Observation, ObservationScope } from './observations'
|
||||
export { Page } from './pagination'
|
||||
export { Peer } from './peer'
|
||||
export { Peer, PeerContext } from './peer'
|
||||
export { Session, SessionPeerConfig } from './session'
|
||||
export {
|
||||
SessionContext,
|
||||
SessionSummaries,
|
||||
Summary,
|
||||
SummaryData,
|
||||
type SummaryData,
|
||||
} from './session_context'
|
||||
export {
|
||||
type DialecticStreamChunk,
|
||||
type DialecticStreamDelta,
|
||||
DialecticStreamResponse,
|
||||
type Observation as ObservationData,
|
||||
type ObservationQueryParams,
|
||||
} from './types'
|
||||
|
||||
// Export validation types for advanced usage
|
||||
|
|
@ -35,5 +38,6 @@ export type {
|
|||
SessionConfig,
|
||||
SessionMetadata,
|
||||
WorkingRepParams,
|
||||
WorkspaceConfig,
|
||||
WorkspaceMetadata,
|
||||
} from './validation'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,338 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import type { Session } from './session'
|
||||
import type { ObservationCreateParam } from './types'
|
||||
|
||||
// Re-export for consumers who import from this module
|
||||
export type { RepresentationOptions, ObservationCreateParam }
|
||||
|
||||
/**
|
||||
* An observation from the theory-of-mind system.
|
||||
*
|
||||
* Observations are facts derived from messages that help build a representation
|
||||
* of a peer.
|
||||
*/
|
||||
export class Observation {
|
||||
/**
|
||||
* Unique identifier for this observation.
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* The observation content/text.
|
||||
*/
|
||||
readonly content: string
|
||||
|
||||
/**
|
||||
* The peer who made the observation.
|
||||
*/
|
||||
readonly observerId: string
|
||||
|
||||
/**
|
||||
* The peer being observed.
|
||||
*/
|
||||
readonly observedId: string
|
||||
|
||||
/**
|
||||
* The session where this observation was made.
|
||||
*/
|
||||
readonly sessionId: string
|
||||
|
||||
/**
|
||||
* When the observation was created.
|
||||
*/
|
||||
readonly createdAt: string
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
content: string,
|
||||
observerId: string,
|
||||
observedId: string,
|
||||
sessionId: string,
|
||||
createdAt: string
|
||||
) {
|
||||
this.id = id
|
||||
this.content = content
|
||||
this.observerId = observerId
|
||||
this.observedId = observedId
|
||||
this.sessionId = sessionId
|
||||
this.createdAt = createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Observation from an API response object.
|
||||
*
|
||||
* @param data - API response data
|
||||
* @returns A new Observation instance
|
||||
*/
|
||||
static fromApiResponse(data: Record<string, unknown>): Observation {
|
||||
return new Observation(
|
||||
(data.id as string) ?? '',
|
||||
(data.content as string) ?? '',
|
||||
(data.observer_id as string) ?? '',
|
||||
(data.observed_id as string) ?? '',
|
||||
(data.session_id as string) ?? '',
|
||||
(data.created_at as string) ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Observation.
|
||||
*/
|
||||
toString(): string {
|
||||
const truncatedContent =
|
||||
this.content.length > 50
|
||||
? `${this.content.slice(0, 50)}...`
|
||||
: this.content
|
||||
return `Observation(id='${this.id}', content='${truncatedContent}')`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoped access to observations for a specific observer/observed relationship.
|
||||
*
|
||||
* This class provides convenient methods to list, query, and delete observations
|
||||
* that are automatically scoped to a specific observer/observed pair.
|
||||
*
|
||||
* Typically accessed via `peer.observations` (for self-observations) or
|
||||
* `peer.observationsOf(target)` (for observations about another peer).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get self-observations
|
||||
* const observations = peer.observations
|
||||
* const obsList = await observations.list()
|
||||
* const searchResults = await observations.query('preferences')
|
||||
*
|
||||
* // Get observations about another peer
|
||||
* const bobObservations = peer.observationsOf('bob')
|
||||
* const bobList = await bobObservations.list()
|
||||
* ```
|
||||
*
|
||||
* @note
|
||||
* This class requires the core Honcho SDK to support observation endpoints.
|
||||
* The observation endpoints are:
|
||||
* - POST /workspaces/{workspace_id}/observations/list
|
||||
* - POST /workspaces/{workspace_id}/observations/query
|
||||
* - DELETE /workspaces/{workspace_id}/observations/{observation_id}
|
||||
*/
|
||||
export class ObservationScope {
|
||||
private _client: HonchoCore
|
||||
|
||||
/**
|
||||
* The workspace ID.
|
||||
*/
|
||||
readonly workspaceId: string
|
||||
|
||||
/**
|
||||
* The observer peer ID.
|
||||
*/
|
||||
readonly observer: string
|
||||
|
||||
/**
|
||||
* The observed peer ID.
|
||||
*/
|
||||
readonly observed: string
|
||||
|
||||
/**
|
||||
* Initialize an ObservationScope.
|
||||
*
|
||||
* @param client - The Honcho client instance
|
||||
* @param workspaceId - The workspace ID
|
||||
* @param observer - The observer peer ID
|
||||
* @param observed - The observed peer ID
|
||||
*/
|
||||
constructor(
|
||||
client: HonchoCore,
|
||||
workspaceId: string,
|
||||
observer: string,
|
||||
observed: string
|
||||
) {
|
||||
this._client = client
|
||||
this.workspaceId = workspaceId
|
||||
this.observer = observer
|
||||
this.observed = observed
|
||||
}
|
||||
|
||||
/**
|
||||
* List observations in this scope.
|
||||
*
|
||||
* @param page - Page number (1-indexed)
|
||||
* @param size - Number of results per page
|
||||
* @param session - Optional session (ID string or Session object) to filter by
|
||||
* @returns Promise resolving to list of Observation objects
|
||||
*/
|
||||
async list(
|
||||
page: number = 1,
|
||||
size: number = 50,
|
||||
session?: string | Session
|
||||
): Promise<Observation[]> {
|
||||
const resolvedSessionId = session
|
||||
? typeof session === 'string'
|
||||
? session
|
||||
: session.id
|
||||
: undefined
|
||||
const filters: Record<string, unknown> = {
|
||||
observer: this.observer,
|
||||
observed: this.observed,
|
||||
}
|
||||
if (resolvedSessionId) {
|
||||
filters.session_id = resolvedSessionId
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.list(
|
||||
this.workspaceId,
|
||||
{
|
||||
filters,
|
||||
page,
|
||||
size,
|
||||
}
|
||||
)
|
||||
|
||||
return (response.items ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic search for observations in this scope.
|
||||
*
|
||||
* @param query - The search query string
|
||||
* @param topK - Maximum number of results to return
|
||||
* @param distance - Maximum cosine distance threshold (0.0-1.0)
|
||||
* @returns Promise resolving to list of matching Observation objects
|
||||
*/
|
||||
async query(
|
||||
query: string,
|
||||
topK: number = 10,
|
||||
distance?: number
|
||||
): Promise<Observation[]> {
|
||||
const filters: Record<string, unknown> = {
|
||||
observer: this.observer,
|
||||
observed: this.observed,
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.query(
|
||||
this.workspaceId,
|
||||
{
|
||||
query,
|
||||
top_k: topK,
|
||||
distance,
|
||||
filters,
|
||||
}
|
||||
)
|
||||
|
||||
return (response ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an observation by ID.
|
||||
*
|
||||
* @param observationId - The ID of the observation to delete
|
||||
*/
|
||||
async delete(observationId: string): Promise<void> {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
await (this._client.workspaces as any).observations.delete(
|
||||
this.workspaceId,
|
||||
observationId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create observations in this scope.
|
||||
*
|
||||
* @param observations - Single observation or array of observations with content and sessionId
|
||||
* @returns Promise resolving to list of created Observation objects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create a single observation
|
||||
* const observations = await peer.observations.create(
|
||||
* { content: 'User prefers dark mode', sessionId: 'session1' }
|
||||
* )
|
||||
*
|
||||
* // Create multiple observations
|
||||
* const observations = await peer.observations.create([
|
||||
* { content: 'User prefers dark mode', sessionId: 'session1' },
|
||||
* { content: 'User is interested in AI', sessionId: 'session1' },
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
async create(
|
||||
observations: ObservationCreateParam | ObservationCreateParam[]
|
||||
): Promise<Observation[]> {
|
||||
// Normalize to array
|
||||
const observationArray = Array.isArray(observations)
|
||||
? observations
|
||||
: [observations]
|
||||
|
||||
// Build the request body with observer/observed from scope
|
||||
const requestObservations = observationArray.map((obs) => ({
|
||||
content: obs.content,
|
||||
session_id:
|
||||
typeof obs.sessionId === 'string' ? obs.sessionId : obs.sessionId.id,
|
||||
observer_id: this.observer,
|
||||
observed_id: this.observed,
|
||||
}))
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: SDK workspaces type doesn't include observations
|
||||
const response = await (this._client.workspaces as any).observations.create(
|
||||
this.workspaceId,
|
||||
{ observations: requestObservations }
|
||||
)
|
||||
|
||||
return (response ?? []).map((item: unknown) =>
|
||||
Observation.fromApiResponse(item as Record<string, unknown>)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed representation for this scope.
|
||||
*
|
||||
* This returns the working representation (narrative) built from the
|
||||
* observations in this scope.
|
||||
*
|
||||
* @param options - Optional options to configure the representation
|
||||
* @returns Promise resolving to a Representation object
|
||||
*/
|
||||
async getRepresentation(
|
||||
options?: RepresentationOptions
|
||||
): Promise<Representation> {
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
this.observer,
|
||||
{
|
||||
target: this.observed,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_derived: options?.includeMostDerived,
|
||||
max_observations: options?.maxObservations,
|
||||
}
|
||||
)
|
||||
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the ObservationScope.
|
||||
*/
|
||||
toString(): string {
|
||||
return `ObservationScope(workspaceId='${this.workspaceId}', observer='${this.observer}', observed='${this.observed}')`
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import type { Page as CorePage } from '@honcho-ai/core/pagination'
|
|||
* Provides async iteration and transformation capabilities while preserving
|
||||
* pagination functionality from the underlying core Page.
|
||||
*/
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Generic type parameter with reasonable default for internal transform
|
||||
export class Page<T, TOriginal = any> implements AsyncIterable<T> {
|
||||
private _originalPage: CorePage<TOriginal>
|
||||
private _transformFunc?: (item: TOriginal) => T
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import type HonchoCore from '@honcho-ai/core'
|
||||
import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages'
|
||||
import { ObservationScope } from './observations'
|
||||
import { Page } from './pagination'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import { Session } from './session'
|
||||
import { type DialecticStreamChunk, DialecticStreamResponse } from './types'
|
||||
import {
|
||||
|
|
@ -10,6 +16,7 @@ import {
|
|||
LimitSchema,
|
||||
MessageContentSchema,
|
||||
MessageMetadataSchema,
|
||||
PeerWorkingRepParamsSchema,
|
||||
SearchQuerySchema,
|
||||
type MessageCreate as ValidatedMessageCreate,
|
||||
} from './validation'
|
||||
|
|
@ -34,6 +41,36 @@ export class Peer {
|
|||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this peer.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this peer.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this peer. May be stale if the peer
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this peer. May be stale if the peer
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Peer. **Do not call this directly, use the client.peer() method instead.**
|
||||
|
|
@ -41,11 +78,21 @@ export class Peer {
|
|||
* @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
|
||||
* @param metadata - Optional metadata to initialize the cached value
|
||||
* @param configuration - Optional configuration to initialize the cached value
|
||||
*/
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
constructor(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
client: HonchoCore,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,9 +106,11 @@ export class Peer {
|
|||
* @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
|
||||
* querying the peer's global representation. Can be a peer ID string
|
||||
* or a Peer object.
|
||||
* @param session - Optional session to scope the query to. If provided, only
|
||||
* information from that session is considered. Can be a session
|
||||
* ID string or a Session object.
|
||||
* @returns Promise resolving to:
|
||||
* - For non-streaming: response string or null if no relevant information
|
||||
* - For streaming: DialecticStreamResponse that can be iterated over
|
||||
|
|
@ -71,26 +120,33 @@ export class Peer {
|
|||
options?: {
|
||||
stream?: boolean
|
||||
target?: string | Peer
|
||||
sessionId?: string
|
||||
session?: string | Session
|
||||
}
|
||||
): Promise<string | DialecticStreamResponse | null> {
|
||||
const targetId = options?.target
|
||||
? typeof options.target === 'string'
|
||||
? options.target
|
||||
: options.target.id
|
||||
: undefined
|
||||
const resolvedSessionId = options?.session
|
||||
? typeof options.session === 'string'
|
||||
? options.session
|
||||
: options.session.id
|
||||
: undefined
|
||||
|
||||
const chatParams = ChatQuerySchema.parse({
|
||||
query,
|
||||
stream: options?.stream,
|
||||
target: options?.target,
|
||||
sessionId: options?.sessionId,
|
||||
target: targetId,
|
||||
session: resolvedSessionId,
|
||||
})
|
||||
|
||||
if (chatParams.stream) {
|
||||
const body = {
|
||||
query: chatParams.query,
|
||||
stream: true,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: chatParams.sessionId,
|
||||
target: chatParams.target,
|
||||
session_id: chatParams.session,
|
||||
}
|
||||
|
||||
const url = `${this._client.baseURL}/v2/workspaces/${this.workspaceId}/peers/${this.id}/chat`
|
||||
|
|
@ -165,12 +221,8 @@ export class Peer {
|
|||
{
|
||||
query: chatParams.query,
|
||||
stream: false,
|
||||
target: chatParams.target
|
||||
? typeof chatParams.target === 'string'
|
||||
? chatParams.target
|
||||
: chatParams.target.id
|
||||
: undefined,
|
||||
session_id: chatParams.sessionId,
|
||||
target: chatParams.target,
|
||||
session_id: chatParams.session,
|
||||
}
|
||||
)
|
||||
if (!response.content || response.content === 'None') {
|
||||
|
|
@ -211,14 +263,16 @@ export class Peer {
|
|||
* 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
|
||||
* @param created_at - Optional ISO 8601 timestamp for the message
|
||||
* @param options.metadata - Optional metadata to associate with the message
|
||||
* @param options.configuration - Optional message-level configuration (e.g., deriver settings)
|
||||
* @param options.created_at - Optional ISO 8601 timestamp for the message
|
||||
* @returns A new message object with this peer's ID and the provided content
|
||||
*/
|
||||
message(
|
||||
content: string,
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
configuration?: Record<string, unknown>
|
||||
created_at?: string | Date
|
||||
}
|
||||
): ValidatedMessageCreate {
|
||||
|
|
@ -236,6 +290,7 @@ export class Peer {
|
|||
peer_id: this.id,
|
||||
content: validatedContent,
|
||||
metadata: validatedMetadata,
|
||||
configuration: options?.configuration,
|
||||
created_at: createdAt,
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +300,7 @@ export class 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.
|
||||
* associated with the peer. This method also updates the cached metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -255,7 +310,8 @@ export class Peer {
|
|||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.metadata || {}
|
||||
this._metadata = peer.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -263,6 +319,7 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to update the metadata associated with this peer.
|
||||
* This will overwrite any existing metadata with the provided values.
|
||||
* This method also updates the cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this peer.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -271,6 +328,7 @@ export class Peer {
|
|||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
this._metadata = metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -278,15 +336,17 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to retrieve configuration associated with this peer.
|
||||
* Configuration currently includes one optional flag, `observe_me`.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the peer's configuration
|
||||
*/
|
||||
async getPeerConfig(): Promise<Record<string, unknown>> {
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return peer.configuration || {}
|
||||
this._configuration = peer.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,14 +356,51 @@ export class Peer {
|
|||
*
|
||||
* Makes an API call to update the configuration associated with this peer.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @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> {
|
||||
async setConfig(config: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.peers.update(this.workspaceId, this.id, {
|
||||
configuration: config,
|
||||
})
|
||||
this._configuration = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current workspace-level configuration for this peer.
|
||||
*
|
||||
* @deprecated Use getConfig() instead
|
||||
* @returns Promise resolving to a dictionary containing the peer's configuration
|
||||
*/
|
||||
async getPeerConfig(): Promise<Record<string, unknown>> {
|
||||
return this.getConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the configuration for this peer.
|
||||
*
|
||||
* @deprecated Use setConfig() instead
|
||||
* @param config - A dictionary of configuration to associate with this peer
|
||||
*/
|
||||
async setPeerConfig(config: Record<string, unknown>): Promise<void> {
|
||||
return this.setConfig(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for this peer.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with this peer and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const peer = await this._client.workspaces.peers.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._metadata = peer.metadata || {}
|
||||
this._configuration = peer.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -383,6 +480,197 @@ export class Peer {
|
|||
return items.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a working representation for this peer.
|
||||
*
|
||||
* Makes an API call to retrieve the working representation for this peer.
|
||||
*
|
||||
* @param session - Optional session to scope the representation to.
|
||||
* @param target - Optional target peer to get the representation of. If provided,
|
||||
* returns the representation of the target from the perspective of this peer.
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a Representation object containing explicit and deductive observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get global representation
|
||||
* const globalRep = await peer.workingRep()
|
||||
* console.log(globalRep.toString())
|
||||
*
|
||||
* // Get representation scoped to a session
|
||||
* const sessionRep = await peer.workingRep('session-123')
|
||||
*
|
||||
* // Get representation with semantic search
|
||||
* const searchedRep = await peer.workingRep(undefined, undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10,
|
||||
* maxObservations: 50
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async workingRep(
|
||||
session?: string | Session,
|
||||
target?: string | Peer,
|
||||
options?: RepresentationOptions
|
||||
): Promise<Representation> {
|
||||
const workingRepParams = PeerWorkingRepParamsSchema.parse({
|
||||
session,
|
||||
target,
|
||||
options,
|
||||
})
|
||||
const sessionId = workingRepParams.session
|
||||
? typeof workingRepParams.session === 'string'
|
||||
? workingRepParams.session
|
||||
: workingRepParams.session.id
|
||||
: undefined
|
||||
const targetId = workingRepParams.target
|
||||
? typeof workingRepParams.target === 'string'
|
||||
? workingRepParams.target
|
||||
: workingRepParams.target.id
|
||||
: undefined
|
||||
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
session_id: sessionId,
|
||||
target: targetId,
|
||||
search_query: workingRepParams.options?.searchQuery,
|
||||
search_top_k: workingRepParams.options?.searchTopK,
|
||||
search_max_distance: workingRepParams.options?.searchMaxDistance,
|
||||
include_most_derived: workingRepParams.options?.includeMostDerived,
|
||||
max_observations: workingRepParams.options?.maxObservations,
|
||||
}
|
||||
)
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for this peer, including representation and peer card.
|
||||
*
|
||||
* This is a convenience method that retrieves both the working representation
|
||||
* and peer card in a single API call.
|
||||
*
|
||||
* @param target - Optional target peer to get context for. If provided, returns
|
||||
* the context for the target from this peer's perspective.
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a PeerContext object containing representation and peer card
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get own context
|
||||
* const context = await peer.getContext()
|
||||
* console.log(context.representation?.toString())
|
||||
* console.log(context.peerCard)
|
||||
*
|
||||
* // Get context for another peer
|
||||
* const context = await peer.getContext('other-peer-id')
|
||||
*
|
||||
* // Get context with semantic search
|
||||
* const context = await peer.getContext(undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async getContext(
|
||||
target?: string | Peer,
|
||||
options?: RepresentationOptions
|
||||
): Promise<PeerContext> {
|
||||
const targetId = target
|
||||
? typeof target === 'string'
|
||||
? target
|
||||
: target.id
|
||||
: undefined
|
||||
|
||||
const response = await this._client.workspaces.peers.getContext(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
target: targetId,
|
||||
search_query: options?.searchQuery,
|
||||
search_top_k: options?.searchTopK,
|
||||
search_max_distance: options?.searchMaxDistance,
|
||||
include_most_derived: options?.includeMostDerived,
|
||||
max_observations: options?.maxObservations,
|
||||
}
|
||||
)
|
||||
|
||||
return PeerContext.fromApiResponse(
|
||||
response as unknown as Record<string, unknown>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access this peer's self-observations (where observer == observed == self).
|
||||
*
|
||||
* This property provides a convenient way to access observations that this peer
|
||||
* has made about themselves. Use this for self-observation scenarios.
|
||||
*
|
||||
* @returns An ObservationScope scoped to this peer's self-observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // List self-observations
|
||||
* const obsList = await peer.observations.list()
|
||||
*
|
||||
* // Search self-observations
|
||||
* const results = await peer.observations.query('preferences')
|
||||
*
|
||||
* // Delete a self-observation
|
||||
* await peer.observations.delete('obs-123')
|
||||
* ```
|
||||
*/
|
||||
get observations(): ObservationScope {
|
||||
return new ObservationScope(
|
||||
this._client,
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
this.id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access observations this peer has made about another peer.
|
||||
*
|
||||
* This method provides scoped access to observations where this peer is the
|
||||
* observer and the target is the observed peer.
|
||||
*
|
||||
* @param target - The target peer (either a Peer object or peer ID string)
|
||||
* @returns An ObservationScope scoped to this peer's observations of the target
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get observations about another peer
|
||||
* const bobObservations = peer.observationsOf('bob')
|
||||
*
|
||||
* // List observations
|
||||
* const obsList = await bobObservations.list()
|
||||
*
|
||||
* // Search observations
|
||||
* const results = await bobObservations.query('work history')
|
||||
*
|
||||
* // Get the representation from these observations
|
||||
* const rep = await bobObservations.getRepresentation()
|
||||
* ```
|
||||
*/
|
||||
observationsOf(target: string | Peer): ObservationScope {
|
||||
const targetId = typeof target === 'string' ? target : target.id
|
||||
return new ObservationScope(
|
||||
this._client,
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
targetId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the Peer.
|
||||
*
|
||||
|
|
@ -392,3 +680,76 @@ export class Peer {
|
|||
return `Peer(id='${this.id}')`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for a peer, including representation and peer card.
|
||||
*
|
||||
* This class holds both the working representation and peer card for a peer,
|
||||
* typically returned from the getContext API call.
|
||||
*/
|
||||
export class PeerContext {
|
||||
/**
|
||||
* The ID of the observer peer.
|
||||
*/
|
||||
readonly peerId: string
|
||||
|
||||
/**
|
||||
* The ID of the target peer being observed.
|
||||
*/
|
||||
readonly targetId: string
|
||||
|
||||
/**
|
||||
* The working representation (may be null if no observations exist).
|
||||
*/
|
||||
readonly representation: Representation | null
|
||||
|
||||
/**
|
||||
* List of peer card strings (may be null if no card exists).
|
||||
*/
|
||||
readonly peerCard: string[] | null
|
||||
|
||||
constructor(
|
||||
peerId: string,
|
||||
targetId: string,
|
||||
representation: Representation | null,
|
||||
peerCard: string[] | null
|
||||
) {
|
||||
this.peerId = peerId
|
||||
this.targetId = targetId
|
||||
this.representation = representation
|
||||
this.peerCard = peerCard
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PeerContext from an API response.
|
||||
*
|
||||
* @param response - API response object with peer_id, target_id, representation, and peer_card
|
||||
* @returns A new PeerContext instance
|
||||
*/
|
||||
static fromApiResponse(response: Record<string, unknown>): PeerContext {
|
||||
const peerId = (response.peer_id as string | undefined) ?? ''
|
||||
const targetId = (response.target_id as string | undefined) ?? ''
|
||||
|
||||
let representation: Representation | null = null
|
||||
if (response.representation) {
|
||||
representation = Representation.fromData(
|
||||
response.representation as RepresentationData
|
||||
)
|
||||
}
|
||||
|
||||
const peerCard = (response.peer_card as string[] | undefined) ?? null
|
||||
|
||||
return new PeerContext(peerId, targetId, representation, peerCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of the PeerContext.
|
||||
*
|
||||
* @returns A string representation suitable for debugging
|
||||
*/
|
||||
toString(): string {
|
||||
const hasRep = this.representation !== null
|
||||
const hasCard = this.peerCard !== null && this.peerCard.length > 0
|
||||
return `PeerContext(peerId='${this.peerId}', targetId='${this.targetId}', hasRepresentation=${hasRep}, hasPeerCard=${hasCard})`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* Options for representation retrieval.
|
||||
*/
|
||||
export interface RepresentationOptions {
|
||||
/**
|
||||
* Semantic search query to filter relevant observations.
|
||||
*/
|
||||
searchQuery?: string
|
||||
|
||||
/**
|
||||
* Number of semantically relevant facts to return.
|
||||
*/
|
||||
searchTopK?: number
|
||||
|
||||
/**
|
||||
* Maximum semantic distance for search results (0.0-1.0).
|
||||
*/
|
||||
searchMaxDistance?: number
|
||||
|
||||
/**
|
||||
* Whether to include the most derived observations.
|
||||
*/
|
||||
includeMostDerived?: boolean
|
||||
|
||||
/**
|
||||
* Maximum number of observations to include.
|
||||
*/
|
||||
maxObservations?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata associated with an observation.
|
||||
*/
|
||||
export interface ObservationMetadata {
|
||||
created_at: string
|
||||
message_ids: Array<[number, number]>
|
||||
session_name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* An explicit observation with full metadata.
|
||||
* Represents facts LITERALLY stated - direct quotes or clear paraphrases only.
|
||||
*/
|
||||
export interface ExplicitObservationBase {
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for deductive observations - logical conclusions.
|
||||
*/
|
||||
export interface DeductiveObservationBase {
|
||||
premises: string[]
|
||||
conclusion: string
|
||||
}
|
||||
|
||||
export interface ExplicitObservation
|
||||
extends ExplicitObservationBase,
|
||||
ObservationMetadata {}
|
||||
|
||||
/**
|
||||
* A deductive observation with full metadata.
|
||||
* Represents conclusions that MUST be true given explicit facts and premises.
|
||||
*/
|
||||
export interface DeductiveObservation
|
||||
extends DeductiveObservationBase,
|
||||
ObservationMetadata {}
|
||||
|
||||
/**
|
||||
* Raw representation data structure returned from the API.
|
||||
*/
|
||||
export interface RepresentationData {
|
||||
explicit: ExplicitObservation[]
|
||||
deductive: DeductiveObservation[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A Representation is a traversable and diffable map of observations.
|
||||
*
|
||||
* At the base, we have a list of explicit observations, derived from a peer's messages.
|
||||
* From there, deductive observations can be made by establishing logical relationships
|
||||
* between explicit observations.
|
||||
*
|
||||
* All of a peer's observations are stored as documents in a collection. These documents
|
||||
* can be queried in various ways to produce this Representation object.
|
||||
*
|
||||
* A "working representation" is a version of this data structure representing the most
|
||||
* recent observations within a single session.
|
||||
*/
|
||||
export class Representation {
|
||||
/**
|
||||
* Facts LITERALLY stated - direct quotes or clear paraphrases only, no interpretation or inference.
|
||||
*/
|
||||
explicit: ExplicitObservation[]
|
||||
|
||||
/**
|
||||
* Conclusions that MUST be true given explicit facts and premises - strict logical necessities.
|
||||
*/
|
||||
deductive: DeductiveObservation[]
|
||||
|
||||
/**
|
||||
* Create a new Representation from observation lists.
|
||||
*
|
||||
* @param explicit - List of explicit observations
|
||||
* @param deductive - List of deductive observations
|
||||
*/
|
||||
constructor(
|
||||
explicit: ExplicitObservation[] = [],
|
||||
deductive: DeductiveObservation[] = []
|
||||
) {
|
||||
this.explicit = explicit
|
||||
this.deductive = deductive
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the representation is empty.
|
||||
*
|
||||
* @returns True if both explicit and deductive observation lists are empty
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.explicit.length === 0 && this.deductive.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Given this and another representation, return a new representation with only
|
||||
* observations that are unique to the other.
|
||||
*
|
||||
* Note: This only removes literal duplicates based on stringified comparison,
|
||||
* not semantically equivalent ones.
|
||||
*
|
||||
* @param other - The representation to compare against
|
||||
* @returns A new Representation containing only observations unique to other
|
||||
*/
|
||||
diff(other: Representation): Representation {
|
||||
const thisExplicitSet = new Set(
|
||||
this.explicit.map((obs) => this._hashExplicit(obs))
|
||||
)
|
||||
const thisDeductiveSet = new Set(
|
||||
this.deductive.map((obs) => this._hashDeductive(obs))
|
||||
)
|
||||
|
||||
const uniqueExplicit = other.explicit.filter(
|
||||
(obs) => !thisExplicitSet.has(this._hashExplicit(obs))
|
||||
)
|
||||
const uniqueDeductive = other.deductive.filter(
|
||||
(obs) => !thisDeductiveSet.has(this._hashDeductive(obs))
|
||||
)
|
||||
|
||||
return new Representation(uniqueExplicit, uniqueDeductive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge another representation into this one.
|
||||
*
|
||||
* This automatically deduplicates explicit and deductive observations.
|
||||
* Preserves order of observations to retain FIFO order.
|
||||
*
|
||||
* Note: Observations with the same timestamp may not have order preserved,
|
||||
* but that's acceptable since they're from the same timestamp.
|
||||
*
|
||||
* @param other - The representation to merge into this one
|
||||
* @param maxObservations - Optional maximum number of observations to keep per type
|
||||
*/
|
||||
merge(other: Representation, maxObservations?: number): void {
|
||||
// Deduplicate by converting to Set using hash, then back to array
|
||||
const explicitMap = new Map<string, ExplicitObservation>()
|
||||
const deductiveMap = new Map<string, DeductiveObservation>()
|
||||
|
||||
// Add existing observations
|
||||
for (const obs of this.explicit) {
|
||||
explicitMap.set(this._hashExplicit(obs), obs)
|
||||
}
|
||||
for (const obs of this.deductive) {
|
||||
deductiveMap.set(this._hashDeductive(obs), obs)
|
||||
}
|
||||
|
||||
// Add new observations (overwrites duplicates)
|
||||
for (const obs of other.explicit) {
|
||||
explicitMap.set(this._hashExplicit(obs), obs)
|
||||
}
|
||||
for (const obs of other.deductive) {
|
||||
deductiveMap.set(this._hashDeductive(obs), obs)
|
||||
}
|
||||
|
||||
// Convert back to arrays and sort by created_at
|
||||
this.explicit = Array.from(explicitMap.values()).sort(
|
||||
(a, b) =>
|
||||
this._parseTimestampForSort(a.created_at) -
|
||||
this._parseTimestampForSort(b.created_at)
|
||||
)
|
||||
this.deductive = Array.from(deductiveMap.values()).sort(
|
||||
(a, b) =>
|
||||
this._parseTimestampForSort(a.created_at) -
|
||||
this._parseTimestampForSort(b.created_at)
|
||||
)
|
||||
|
||||
// Apply max observations limit if specified
|
||||
if (maxObservations !== undefined) {
|
||||
this.explicit = this.explicit.slice(-maxObservations)
|
||||
this.deductive = this.deductive.slice(-maxObservations)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format representation into a clean, readable string for LLM prompts.
|
||||
*
|
||||
* Timestamps are stripped of subsecond precision for cleaner display.
|
||||
*
|
||||
* @returns Formatted string with clear sections and numbered items including timestamps
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* EXPLICIT:
|
||||
* 1. [2025-01-01T12:00:00Z] The user has a dog named Rover
|
||||
* 2. [2025-01-01T12:01:00Z] The user's dog is 5 years old
|
||||
*
|
||||
* DEDUCTIVE:
|
||||
* 1. [2025-01-01T12:01:00Z] Rover is 5 years old
|
||||
* - The user has a dog named Rover
|
||||
* - The user's dog is 5 years old
|
||||
* ```
|
||||
*/
|
||||
toString(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('EXPLICIT:\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
const obs = this.explicit[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('DEDUCTIVE:\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.conclusion}`)
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format representation into a clean, readable string without timestamps.
|
||||
*
|
||||
* @returns Formatted string with clear sections and numbered items without temporal metadata
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* EXPLICIT:
|
||||
* 1. The user has a dog named Rover
|
||||
* 2. The user's dog is 5 years old
|
||||
*
|
||||
* DEDUCTIVE:
|
||||
* 1. Rover is 5 years old
|
||||
* - The user has a dog named Rover
|
||||
* - The user's dog is 5 years old
|
||||
* ```
|
||||
*/
|
||||
toStringNoTimestamps(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('EXPLICIT:\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
parts.push(`${i + 1}. ${this.explicit[i].content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('DEDUCTIVE:\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
parts.push(`${i + 1}. ${obs.conclusion}`)
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Representation object as markdown.
|
||||
*
|
||||
* Timestamps are stripped of subsecond precision for cleaner display.
|
||||
*
|
||||
* @returns Formatted markdown string with headers and lists
|
||||
*/
|
||||
toMarkdown(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push('## Explicit Observations\n')
|
||||
for (let i = 0; i < this.explicit.length; i++) {
|
||||
const obs = this.explicit[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. [${timestamp}] ${obs.content}`)
|
||||
}
|
||||
parts.push('')
|
||||
|
||||
parts.push('## Deductive Observations\n')
|
||||
for (let i = 0; i < this.deductive.length; i++) {
|
||||
const obs = this.deductive[i]
|
||||
const timestamp = this._stripMicroseconds(obs.created_at)
|
||||
parts.push(`${i + 1}. **Conclusion**: ${obs.conclusion}`)
|
||||
parts.push(` **Created**: ${timestamp}`)
|
||||
if (obs.premises.length > 0) {
|
||||
parts.push(' **Premises**:')
|
||||
for (const premise of obs.premises) {
|
||||
parts.push(` - ${premise}`)
|
||||
}
|
||||
}
|
||||
parts.push('')
|
||||
}
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Representation from raw API response data.
|
||||
*
|
||||
* @param data - Raw representation data from the API
|
||||
* @returns A new Representation instance
|
||||
*/
|
||||
static fromData(data: RepresentationData): Representation {
|
||||
return new Representation(data.explicit, data.deductive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash string for an explicit observation for deduplication.
|
||||
* Based on content, created_at, and session_name.
|
||||
*/
|
||||
private _hashExplicit(obs: ExplicitObservation): string {
|
||||
return JSON.stringify({
|
||||
content: obs.content,
|
||||
created_at: obs.created_at,
|
||||
session_name: obs.session_name,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash string for a deductive observation for deduplication.
|
||||
* Based on conclusion, created_at, and session_name (premises not included).
|
||||
*/
|
||||
private _hashDeductive(obs: DeductiveObservation): string {
|
||||
return JSON.stringify({
|
||||
conclusion: obs.conclusion,
|
||||
created_at: obs.created_at,
|
||||
session_name: obs.session_name,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip microseconds from ISO timestamp for cleaner display.
|
||||
*/
|
||||
private _stripMicroseconds(timestamp: string): string {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
|
||||
} catch {
|
||||
return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse a timestamp and return milliseconds since epoch for sorting.
|
||||
* Handles microsecond precision by truncating to milliseconds before parsing.
|
||||
*
|
||||
* @param timestamp - ISO 8601 timestamp string (may include microseconds)
|
||||
* @returns Milliseconds since epoch, or 0 if parsing fails
|
||||
*/
|
||||
private _parseTimestampForSort(timestamp: string): number {
|
||||
try {
|
||||
// Normalize fractional seconds to 3 digits (milliseconds)
|
||||
// Match pattern: YYYY-MM-DDTHH:mm:ss.SSSSSS(Z or timezone)
|
||||
const normalized = timestamp.replace(
|
||||
/(\.\d{3})\d+(Z|[+-]\d{2}:\d{2})$/,
|
||||
'$1$2'
|
||||
)
|
||||
const time = new Date(normalized).getTime()
|
||||
// Return 0 if parsing failed (NaN)
|
||||
return Number.isNaN(time) ? 0 : time
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,17 +7,24 @@ import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/mess
|
|||
import type { Uploadable } from '@honcho-ai/core/uploads'
|
||||
import { Page } from './pagination'
|
||||
import { Peer } from './peer'
|
||||
import {
|
||||
Representation,
|
||||
type RepresentationData,
|
||||
type RepresentationOptions,
|
||||
} from './representation'
|
||||
import { SessionContext, SessionSummaries, Summary } from './session_context'
|
||||
// Disabled: observations not ready for release
|
||||
// import type { Observation, ObservationQueryParams } from './types'
|
||||
import {
|
||||
ContextParamsSchema,
|
||||
type DeriverStatusOptions,
|
||||
DeriverStatusOptionsSchema,
|
||||
FileUploadSchema,
|
||||
FilterSchema,
|
||||
type Filters,
|
||||
LimitSchema,
|
||||
type MessageAddition,
|
||||
MessageAdditionSchema,
|
||||
// ObservationQueryParamsSchema, // Disabled: observations not ready for release
|
||||
type PeerAddition,
|
||||
PeerAdditionSchema,
|
||||
type PeerRemoval,
|
||||
|
|
@ -47,7 +54,7 @@ export class SessionPeerConfig {
|
|||
* of other peers in the session. When false, this peer will not build local
|
||||
* representations of other peers within this session.
|
||||
*/
|
||||
observe_others?: boolean
|
||||
observe_others?: boolean | null
|
||||
|
||||
/**
|
||||
* Initialize SessionPeerConfig with observation settings.
|
||||
|
|
@ -55,7 +62,7 @@ export class SessionPeerConfig {
|
|||
* @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) {
|
||||
constructor(observe_me?: boolean | null, observe_others?: boolean | null) {
|
||||
const validatedConfig = SessionPeerConfigSchema.parse({
|
||||
observe_me,
|
||||
observe_others,
|
||||
|
|
@ -112,6 +119,36 @@ export class Session {
|
|||
* Reference to the parent Honcho client instance.
|
||||
*/
|
||||
private _client: HonchoCore
|
||||
/**
|
||||
* Private cached metadata for this session.
|
||||
*/
|
||||
private _metadata?: Record<string, unknown>
|
||||
/**
|
||||
* Private cached configuration for this session.
|
||||
*/
|
||||
private _configuration?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Cached metadata for this session. May be stale if the session
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getMetadata() to get the latest metadata from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get metadata(): Record<string, unknown> | undefined {
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached configuration for this session. May be stale if the session
|
||||
* was not recently fetched from the API.
|
||||
*
|
||||
* Call getConfig() to get the latest configuration from the server,
|
||||
* which will also update this cached value.
|
||||
*/
|
||||
get configuration(): Record<string, unknown> | undefined {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Session. **Do not call this directly, use the client.session() method instead.**
|
||||
|
|
@ -119,11 +156,21 @@ export class Session {
|
|||
* @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
|
||||
* @param metadata - Optional metadata to initialize the cached value
|
||||
* @param configuration - Optional configuration to initialize the cached value
|
||||
*/
|
||||
constructor(id: string, workspaceId: string, client: HonchoCore) {
|
||||
constructor(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
client: HonchoCore,
|
||||
metadata?: Record<string, unknown>,
|
||||
configuration?: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.workspaceId = workspaceId
|
||||
this._client = client
|
||||
this._metadata = metadata
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -364,12 +411,12 @@ export class Session {
|
|||
* })
|
||||
* ```
|
||||
*/
|
||||
async addMessages(messages: MessageAddition): Promise<void> {
|
||||
async addMessages(messages: MessageAddition): Promise<Message[]> {
|
||||
const validatedMessages = MessageAdditionSchema.parse(messages)
|
||||
const messagesList = Array.isArray(validatedMessages)
|
||||
? validatedMessages
|
||||
: [validatedMessages]
|
||||
await this._client.workspaces.sessions.messages.create(
|
||||
return await this._client.workspaces.sessions.messages.create(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
|
|
@ -403,7 +450,8 @@ export class 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.
|
||||
* that provides context about the session. This method also updates the cached
|
||||
* metadata property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the session's metadata.
|
||||
* Returns an empty dictionary if no metadata is set
|
||||
|
|
@ -413,7 +461,8 @@ export class Session {
|
|||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
return session.metadata || {}
|
||||
this._metadata = session.metadata || {}
|
||||
return this._metadata
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -422,7 +471,8 @@ export class 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.
|
||||
* contextual information about the session. This method also updates the
|
||||
* cached metadata property.
|
||||
*
|
||||
* @param metadata - A dictionary of metadata to associate with this session.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
|
|
@ -431,17 +481,114 @@ export class Session {
|
|||
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
|
||||
metadata,
|
||||
})
|
||||
this._metadata = metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this session.
|
||||
* Get configuration for this session.
|
||||
*
|
||||
* Makes an API call to mark this session as inactive.
|
||||
* Makes an API call to retrieve the current configuration associated with this session.
|
||||
* Configuration includes settings that control session behavior. This method also
|
||||
* updates the cached configuration property.
|
||||
*
|
||||
* @returns Promise resolving to a dictionary containing the session's configuration.
|
||||
* Returns an empty dictionary if no configuration is set
|
||||
*/
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
const session = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._configuration = session.configuration || {}
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration for this session.
|
||||
*
|
||||
* Makes an API call to update the configuration associated with this session.
|
||||
* This will overwrite any existing configuration with the provided values.
|
||||
* This method also updates the cached configuration property.
|
||||
*
|
||||
* @param configuration - A dictionary of configuration to associate with this session.
|
||||
* Keys must be strings, values can be any JSON-serializable type
|
||||
*/
|
||||
async setConfig(configuration: Record<string, unknown>): Promise<void> {
|
||||
await this._client.workspaces.sessions.update(this.workspaceId, this.id, {
|
||||
configuration,
|
||||
})
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached metadata and configuration for this session.
|
||||
*
|
||||
* Makes a single API call to retrieve the latest metadata and configuration
|
||||
* associated with this session and updates the cached properties.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
const session = await this._client.workspaces.sessions.getOrCreate(
|
||||
this.workspaceId,
|
||||
{ id: this.id }
|
||||
)
|
||||
this._metadata = session.metadata || {}
|
||||
this._configuration = session.configuration || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this session and all associated data.
|
||||
*
|
||||
* Makes an API call to permanently delete this session and all related data including:
|
||||
* - Messages
|
||||
* - Message embeddings
|
||||
* - Observations
|
||||
* - Session-Peer associations
|
||||
* - Background processing queue items
|
||||
*
|
||||
* This action cannot be undone.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this._client.workspaces.sessions.delete(this.workspaceId, this.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this session, optionally up to a specific message.
|
||||
*
|
||||
* Makes an API call to create a copy of this session with a new ID.
|
||||
* All messages and peers from the original session are copied to the new session.
|
||||
* If a messageId is provided, only messages up to and including that message
|
||||
* are copied.
|
||||
*
|
||||
* @param messageId - Optional message ID to cut off the clone at. If provided,
|
||||
* the cloned session will only contain messages up to and
|
||||
* including this message.
|
||||
* @returns Promise resolving to a new Session object representing the cloned session
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Clone entire session
|
||||
* const cloned = await session.clone()
|
||||
*
|
||||
* // Clone session up to a specific message
|
||||
* const cloned = await session.clone('msg_abc123')
|
||||
* ```
|
||||
*/
|
||||
async clone(messageId?: string): Promise<Session> {
|
||||
const clonedSessionData = await this._client.workspaces.sessions.clone(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
messageId ? { message_id: messageId } : {}
|
||||
)
|
||||
|
||||
return new Session(
|
||||
clonedSessionData.id,
|
||||
this.workspaceId,
|
||||
this._client,
|
||||
clonedSessionData.metadata ?? undefined,
|
||||
clonedSessionData.configuration ?? undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized context for this session within a token limit.
|
||||
*
|
||||
|
|
@ -478,7 +625,8 @@ export class Session {
|
|||
tokens?: number,
|
||||
peerTarget?: string | Peer,
|
||||
lastUserMessage?: string | Message,
|
||||
peerPerspective?: string | Peer
|
||||
peerPerspective?: string | Peer,
|
||||
representationOptions?: RepresentationOptions
|
||||
): Promise<SessionContext>
|
||||
async getContext(options?: {
|
||||
summary?: boolean
|
||||
|
|
@ -486,6 +634,8 @@ export class Session {
|
|||
peerTarget?: string | Peer
|
||||
lastUserMessage?: string | Message
|
||||
peerPerspective?: string | Peer
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
}): Promise<SessionContext>
|
||||
async getContext(
|
||||
summaryOrOptions?:
|
||||
|
|
@ -496,11 +646,14 @@ export class Session {
|
|||
peerTarget?: string | Peer
|
||||
lastUserMessage?: string | Message
|
||||
peerPerspective?: string | Peer
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
},
|
||||
tokens?: number,
|
||||
peerTarget?: string | Peer,
|
||||
lastUserMessage?: string | Message,
|
||||
peerPerspective?: string | Peer
|
||||
peerPerspective?: string | Peer,
|
||||
representationOptions?: RepresentationOptions
|
||||
): Promise<SessionContext> {
|
||||
// Normalize positional arguments into options object
|
||||
let options: {
|
||||
|
|
@ -509,10 +662,13 @@ export class Session {
|
|||
peerTarget?: string
|
||||
lastUserMessage?: string
|
||||
peerPerspective?: string
|
||||
limitToSession?: boolean
|
||||
representationOptions?: RepresentationOptions
|
||||
}
|
||||
|
||||
if (
|
||||
typeof summaryOrOptions === 'boolean' ||
|
||||
// biome-ignore lint/complexity/noArguments: Need to detect which overload pattern is being used
|
||||
(summaryOrOptions === undefined && arguments.length > 1)
|
||||
) {
|
||||
// Positional arguments pattern
|
||||
|
|
@ -528,6 +684,7 @@ export class Session {
|
|||
typeof peerPerspective === 'object'
|
||||
? peerPerspective.id
|
||||
: peerPerspective,
|
||||
representationOptions,
|
||||
}
|
||||
} else {
|
||||
// Options object pattern
|
||||
|
|
@ -540,6 +697,8 @@ export class Session {
|
|||
peerTarget: options.peerTarget,
|
||||
lastUserMessage: options.lastUserMessage,
|
||||
peerPerspective: options.peerPerspective,
|
||||
limitToSession: options.limitToSession,
|
||||
representationOptions: options.representationOptions,
|
||||
})
|
||||
|
||||
// Extract message ID if lastUserMessage is a Message object
|
||||
|
|
@ -557,6 +716,13 @@ export class Session {
|
|||
last_message: lastMessageId,
|
||||
peer_target: contextParams.peerTarget,
|
||||
peer_perspective: contextParams.peerPerspective,
|
||||
limit_to_session: contextParams.limitToSession,
|
||||
search_top_k: contextParams.representationOptions?.searchTopK,
|
||||
search_max_distance:
|
||||
contextParams.representationOptions?.searchMaxDistance,
|
||||
include_most_derived:
|
||||
contextParams.representationOptions?.includeMostDerived,
|
||||
max_observations: contextParams.representationOptions?.maxObservations,
|
||||
}
|
||||
)
|
||||
// Convert the summary response to Summary object if present
|
||||
|
|
@ -636,6 +802,91 @@ export class Session {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all observations for this session.
|
||||
*
|
||||
* Observations are theory-of-mind data (documents) that peers have formed about each other.
|
||||
* Returns paginated results that can be filtered by observer_id and observed_id.
|
||||
*
|
||||
* @param filters - Optional filters to scope the observations: see [filters documentation](https://docs.honcho.dev/v2/guides/using-filters).
|
||||
* @returns A paginated list of Observation objects.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const observations = await session.listObservations()
|
||||
* for await (const observation of observations) {
|
||||
* console.log(`${observation.observer_id} observed: ${observation.content}`)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async listObservations(filters?: Filters): Promise<Page<Observation>> {
|
||||
// const validatedFilters = filters ? FilterSchema.parse(filters) : undefined
|
||||
// const response = await this._client.workspaces.sessions.observations.list(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// { filters: validatedFilters }
|
||||
// )
|
||||
// return new Page(response)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Query observations using semantic search.
|
||||
*
|
||||
* Performs vector similarity search on observations to find semantically relevant results.
|
||||
* Use this to find observations related to a specific topic or concept.
|
||||
*
|
||||
* @param params - Query parameters
|
||||
* @param params.query - The semantic search query
|
||||
* @param params.top_k - Number of results to return (1-100, default: 10)
|
||||
* @param params.distance - Maximum cosine distance threshold for results (0.0-1.0)
|
||||
* @param params.filters - Optional filters to scope the query
|
||||
* @returns A list of Observation objects matching the query
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const observations = await session.queryObservations({
|
||||
* query: "user preferences about music",
|
||||
* top_k: 5,
|
||||
* distance: 0.8
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async queryObservations(
|
||||
// params: ObservationQueryParams
|
||||
// ): Promise<Observation[]> {
|
||||
// const validated = ObservationQueryParamsSchema.parse(params)
|
||||
// return await this._client.workspaces.sessions.observations.query(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// validated
|
||||
// )
|
||||
// }
|
||||
|
||||
/**
|
||||
* Delete a specific observation by ID.
|
||||
*
|
||||
* This permanently deletes the observation (document) from the theory-of-mind system.
|
||||
* This action cannot be undone.
|
||||
*
|
||||
* @param observationId - The ID of the observation to delete
|
||||
* @returns A promise that resolves when the observation is deleted
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await session.deleteObservation('obs_123abc')
|
||||
* ```
|
||||
*/
|
||||
// Disabled: observations not ready for release
|
||||
// async deleteObservation(observationId: string): Promise<void> {
|
||||
// await this._client.workspaces.sessions.observations.delete(
|
||||
// this.workspaceId,
|
||||
// this.id,
|
||||
// observationId
|
||||
// )
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get the deriver processing status for this session, optionally scoped to an observer or sender.
|
||||
*
|
||||
|
|
@ -644,12 +895,18 @@ export class Session {
|
|||
* This method automatically scopes the status to this session.
|
||||
*
|
||||
* @param options - Configuration options for the status request
|
||||
* @param options.observerId - Optional observer ID to scope the status to
|
||||
* @param options.senderId - Optional sender ID to scope the status to
|
||||
* @param options.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @returns Promise resolving to the deriver status information including work unit counts
|
||||
*/
|
||||
async getDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'sessionId' | 'observerId' | 'senderId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
|
|
@ -657,16 +914,22 @@ export class Session {
|
|||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
const resolvedObserverId = options?.observer
|
||||
? typeof options.observer === 'string'
|
||||
? options.observer
|
||||
: options.observer.id
|
||||
: undefined
|
||||
const resolvedSenderId = options?.sender
|
||||
? typeof options.sender === 'string'
|
||||
? options.sender
|
||||
: options.sender.id
|
||||
: undefined
|
||||
|
||||
const queryParams: WorkspaceDeriverStatusParams = {
|
||||
session_id: this.id, // Always use this session's ID
|
||||
}
|
||||
if (validatedOptions?.observerId)
|
||||
queryParams.observer_id = validatedOptions.observerId
|
||||
if (validatedOptions?.senderId)
|
||||
queryParams.sender_id = validatedOptions.senderId
|
||||
if (resolvedObserverId) queryParams.observer_id = resolvedObserverId
|
||||
if (resolvedSenderId) queryParams.sender_id = resolvedSenderId
|
||||
|
||||
const status = await this._client.workspaces.deriverStatus(
|
||||
this.workspaceId,
|
||||
|
|
@ -690,14 +953,20 @@ export class Session {
|
|||
* 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.observer - Optional observer (ID string or Peer object) to scope the status to
|
||||
* @param options.sender - Optional sender (ID string or Peer object) to scope the status to
|
||||
* @param options.timeoutMs - Optional timeout in milliseconds (default: 300000 - 5 minutes)
|
||||
* @returns Promise resolving to the final deriver status when processing is complete
|
||||
* @throws Error if timeout is exceeded before processing completes
|
||||
*/
|
||||
async pollDeriverStatus(
|
||||
options?: Omit<DeriverStatusOptions, 'sessionId'>
|
||||
options?: Omit<
|
||||
DeriverStatusOptions,
|
||||
'sessionId' | 'observerId' | 'senderId'
|
||||
> & {
|
||||
observer?: string | Peer
|
||||
sender?: string | Peer
|
||||
}
|
||||
): Promise<{
|
||||
totalWorkUnits: number
|
||||
completedWorkUnits: number
|
||||
|
|
@ -705,14 +974,11 @@ export class Session {
|
|||
pendingWorkUnits: number
|
||||
sessions?: Record<string, DeriverStatus.Sessions>
|
||||
}> {
|
||||
const validatedOptions = options
|
||||
? DeriverStatusOptionsSchema.parse(options)
|
||||
: undefined
|
||||
const timeoutMs = validatedOptions?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const timeoutMs = options?.timeoutMs ?? 300000 // Default to 5 minutes
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
const status = await this.getDeriverStatus(validatedOptions)
|
||||
const status = await this.getDeriverStatus(options)
|
||||
if (status.pendingWorkUnits === 0 && status.inProgressWorkUnits === 0) {
|
||||
return status
|
||||
}
|
||||
|
|
@ -754,7 +1020,11 @@ export class Session {
|
|||
* - 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
|
||||
* @param peer - The peer (ID string or Peer object) to attribute the created messages to
|
||||
* @param options - Optional parameters for the uploaded messages
|
||||
* @param options.metadata - Optional metadata dictionary to associate with the messages
|
||||
* @param options.configuration - Optional configuration dictionary to associate with the messages
|
||||
* @param options.created_at - Optional created-at timestamp for the messages. Should be an ISO 8601 formatted string.
|
||||
* @returns Promise resolving to a list of Message objects representing the created messages
|
||||
*
|
||||
* @note Supported file types include PDFs, text files, and JSON documents.
|
||||
|
|
@ -766,17 +1036,59 @@ export class Session {
|
|||
* // Upload a file
|
||||
* const messages = await session.uploadFile(fileInput.files[0], 'user123')
|
||||
* console.log(`Created ${messages.length} messages from file`)
|
||||
*
|
||||
* // Upload a file with metadata and timestamp
|
||||
* const messages = await session.uploadFile(fileInput.files[0], 'user123', {
|
||||
* metadata: { source: 'upload' },
|
||||
* created_at: '2021-01-01T00:00:00.000Z'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async uploadFile(file: Uploadable, peerId: string): Promise<Message[]> {
|
||||
const uploadParams = FileUploadSchema.parse({ file, peerId })
|
||||
async uploadFile(
|
||||
file: Uploadable,
|
||||
peer: string | Peer,
|
||||
options?: {
|
||||
metadata?: Record<string, unknown>
|
||||
configuration?: Record<string, unknown>
|
||||
created_at?: string | Date
|
||||
}
|
||||
): Promise<Message[]> {
|
||||
const createdAt =
|
||||
options?.created_at instanceof Date
|
||||
? options.created_at.toISOString()
|
||||
: options?.created_at
|
||||
|
||||
const resolvedPeerId = typeof peer === 'string' ? peer : peer.id
|
||||
|
||||
const uploadParams = FileUploadSchema.parse({
|
||||
file,
|
||||
peer: resolvedPeerId,
|
||||
metadata: options?.metadata,
|
||||
configuration: options?.configuration,
|
||||
created_at: createdAt,
|
||||
})
|
||||
|
||||
// Build body with file and peer_id, plus optional fields as JSON strings
|
||||
const body = {
|
||||
file: uploadParams.file,
|
||||
peer_id: resolvedPeerId,
|
||||
...(uploadParams.metadata !== undefined && uploadParams.metadata !== null
|
||||
? { metadata: JSON.stringify(uploadParams.metadata) }
|
||||
: {}),
|
||||
...(uploadParams.configuration !== undefined &&
|
||||
uploadParams.configuration !== null
|
||||
? { configuration: JSON.stringify(uploadParams.configuration) }
|
||||
: {}),
|
||||
...(uploadParams.created_at !== undefined &&
|
||||
uploadParams.created_at !== null
|
||||
? { created_at: uploadParams.created_at }
|
||||
: {}),
|
||||
}
|
||||
|
||||
const response = await this._client.workspaces.sessions.messages.upload(
|
||||
this.workspaceId,
|
||||
this.id,
|
||||
{
|
||||
file: uploadParams.file,
|
||||
peer_id: uploadParams.peerId,
|
||||
}
|
||||
body
|
||||
)
|
||||
|
||||
return response
|
||||
|
|
@ -792,23 +1104,41 @@ export class Session {
|
|||
* @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
|
||||
* @param options - Optional representation options to filter and configure the results
|
||||
* @returns Promise resolving to a Representation object containing explicit and deductive observations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get peer's global representation in this session
|
||||
* const globalRep = await session.workingRep('user123')
|
||||
* console.log(globalRep.toString())
|
||||
*
|
||||
* // Get what user123 knows about assistant in this session
|
||||
* const localRep = await session.workingRep('user123', 'assistant')
|
||||
*
|
||||
* // Get representation with semantic search
|
||||
* const searchedRep = await session.workingRep('user123', undefined, {
|
||||
* searchQuery: 'preferences',
|
||||
* searchTopK: 10
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async workingRep(
|
||||
peer: string | Peer,
|
||||
target?: string | Peer
|
||||
): Promise<Record<string, unknown>> {
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({ peer, target })
|
||||
target?: string | Peer,
|
||||
options?: {
|
||||
searchQuery?: string
|
||||
searchTopK?: number
|
||||
searchMaxDistance?: number
|
||||
includeMostDerived?: boolean
|
||||
maxObservations?: number
|
||||
}
|
||||
): Promise<Representation> {
|
||||
const workingRepParams = WorkingRepParamsSchema.parse({
|
||||
peer,
|
||||
target,
|
||||
options,
|
||||
})
|
||||
const peerId =
|
||||
typeof workingRepParams.peer === 'string'
|
||||
? workingRepParams.peer
|
||||
|
|
@ -819,14 +1149,27 @@ export class Session {
|
|||
: workingRepParams.target.id
|
||||
: undefined
|
||||
|
||||
return await this._client.workspaces.peers.workingRepresentation(
|
||||
const response = await this._client.workspaces.peers.workingRepresentation(
|
||||
this.workspaceId,
|
||||
peerId,
|
||||
{
|
||||
session_id: this.id,
|
||||
target: targetId,
|
||||
search_query: workingRepParams.options?.searchQuery,
|
||||
search_top_k: workingRepParams.options?.searchTopK,
|
||||
search_max_distance: workingRepParams.options?.searchMaxDistance,
|
||||
include_most_derived: workingRepParams.options?.includeMostDerived,
|
||||
max_observations: workingRepParams.options?.maxObservations,
|
||||
}
|
||||
)
|
||||
const maybe = response as
|
||||
| RepresentationData
|
||||
| { representation?: RepresentationData | null }
|
||||
| null
|
||||
const rep = (maybe && typeof maybe === 'object' && 'representation' in maybe
|
||||
? (maybe as { representation?: RepresentationData | null }).representation
|
||||
: maybe) ?? { explicit: [], deductive: [] }
|
||||
return Representation.fromData(rep as RepresentationData)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,41 @@
|
|||
import type { Session } from './session'
|
||||
|
||||
/**
|
||||
* Shared types for the Honcho TypeScript SDK.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Observation - external view of a document (theory-of-mind data).
|
||||
*/
|
||||
export interface Observation {
|
||||
id: string
|
||||
content: string
|
||||
observer_id: string
|
||||
observed_id: string
|
||||
session_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating an observation.
|
||||
*/
|
||||
export interface ObservationCreateParam {
|
||||
/** The observation content/text */
|
||||
content: string
|
||||
/** The session this observation relates to (ID string or Session object) */
|
||||
sessionId: string | Session
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for semantic search of observations.
|
||||
*/
|
||||
export interface ObservationQueryParams {
|
||||
query: string
|
||||
top_k?: number
|
||||
distance?: number
|
||||
filters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta object for streaming dialectic responses.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const SessionIdSchema = z
|
|||
*/
|
||||
export const SessionPeerConfigSchema = z.object({
|
||||
observe_me: z.boolean().nullable().optional(),
|
||||
observe_others: z.boolean().optional(),
|
||||
observe_others: z.boolean().nullable().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +88,15 @@ export const MessageMetadataSchema = z
|
|||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message configuration.
|
||||
* Configuration can include deriver and peer_card settings.
|
||||
*/
|
||||
export const MessageConfigurationSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.nullable()
|
||||
.optional()
|
||||
|
||||
/**
|
||||
* Schema for message creation.
|
||||
*/
|
||||
|
|
@ -95,6 +104,7 @@ export const MessageCreateSchema = z.object({
|
|||
peer_id: PeerIdSchema,
|
||||
content: MessageContentSchema,
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: MessageConfigurationSchema,
|
||||
created_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
|
|
@ -120,8 +130,18 @@ export const FilterSchema = z.record(z.string(), z.unknown()).optional()
|
|||
export const ChatQuerySchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
stream: z.boolean().optional().default(false),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sessionId: z.string().optional(),
|
||||
target: z
|
||||
.union([z.string(), z.object({ id: z.string() })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
session: z
|
||||
.union([z.string(), z.object({ id: z.string() })])
|
||||
.optional()
|
||||
.transform((val) =>
|
||||
val ? (typeof val === 'string' ? val : val.id) : undefined
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -138,6 +158,30 @@ const MessageSchema: z.ZodType<Message> = z.object({
|
|||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
}) as z.ZodType<Message>
|
||||
|
||||
/**
|
||||
* Schema for representation options.
|
||||
*/
|
||||
export const RepresentationOptionsSchema = z.object({
|
||||
searchTopK: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'searchTopK must be at least 1')
|
||||
.max(100, 'searchTopK must be at most 100')
|
||||
.optional(),
|
||||
searchMaxDistance: z
|
||||
.number()
|
||||
.min(0.0, 'searchMaxDistance must be at least 0.0')
|
||||
.max(1.0, 'searchMaxDistance must be at most 1.0')
|
||||
.optional(),
|
||||
includeMostDerived: z.boolean().optional(),
|
||||
maxObservations: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'maxObservations must be at least 1')
|
||||
.max(100, 'maxObservations must be at most 100')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for context retrieval parameters.
|
||||
*/
|
||||
|
|
@ -156,6 +200,8 @@ export const ContextParamsSchema = z
|
|||
.optional(),
|
||||
peerTarget: PeerIdSchema.optional(),
|
||||
peerPerspective: PeerIdSchema.optional(),
|
||||
limitToSession: z.boolean().optional(),
|
||||
representationOptions: RepresentationOptionsSchema.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.lastUserMessage && !data.peerTarget) {
|
||||
|
|
@ -179,9 +225,9 @@ export const ContextParamsSchema = z
|
|||
* Schema for deriver status options.
|
||||
*/
|
||||
export const DeriverStatusOptionsSchema = z.object({
|
||||
observerId: z.string().optional(),
|
||||
senderId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
observer: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
sender: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
session: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
timeoutMs: z
|
||||
.number()
|
||||
.positive('Timeout must be a positive number')
|
||||
|
|
@ -216,7 +262,10 @@ export const FileUploadSchema = z.object({
|
|||
'File must not be null or undefined'
|
||||
),
|
||||
]),
|
||||
peerId: PeerIdSchema,
|
||||
peer: z.union([PeerIdSchema, z.object({ id: z.string() })]),
|
||||
metadata: MessageMetadataSchema,
|
||||
configuration: z.record(z.string(), z.unknown()).optional(),
|
||||
created_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -225,6 +274,20 @@ export const FileUploadSchema = z.object({
|
|||
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(),
|
||||
options: RepresentationOptionsSchema.extend({
|
||||
searchQuery: SearchQuerySchema.optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for peer working representation parameters.
|
||||
*/
|
||||
export const PeerWorkingRepParamsSchema = z.object({
|
||||
session: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
target: z.union([z.string(), z.object({ id: z.string() })]).optional(),
|
||||
options: RepresentationOptionsSchema.extend({
|
||||
searchQuery: SearchQuerySchema.optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -271,6 +334,11 @@ export const MessageAdditionSchema = z.union([
|
|||
*/
|
||||
export const WorkspaceMetadataSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for workspace configuration.
|
||||
*/
|
||||
export const WorkspaceConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* Schema for limit.
|
||||
*/
|
||||
|
|
@ -280,6 +348,25 @@ export const LimitSchema = z
|
|||
.min(1, 'Limit must be a positive integer')
|
||||
.max(100, 'Limit must be less than or equal to 100')
|
||||
|
||||
/**
|
||||
* Schema for observation query parameters.
|
||||
*/
|
||||
export const ObservationQueryParamsSchema = z.object({
|
||||
query: SearchQuerySchema,
|
||||
top_k: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'top_k must be at least 1')
|
||||
.max(100, 'top_k must be at most 100')
|
||||
.optional(),
|
||||
distance: z
|
||||
.number()
|
||||
.min(0.0, 'distance must be at least 0.0')
|
||||
.max(1.0, 'distance must be at most 1.0')
|
||||
.optional(),
|
||||
filters: FilterSchema,
|
||||
})
|
||||
|
||||
/**
|
||||
* Type exports for use throughout the SDK.
|
||||
*/
|
||||
|
|
@ -296,8 +383,13 @@ 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 PeerWorkingRepParams = z.infer<typeof PeerWorkingRepParamsSchema>
|
||||
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 WorkspaceConfig = z.infer<typeof WorkspaceConfigSchema>
|
||||
export type Limit = z.infer<typeof LimitSchema>
|
||||
export type ObservationQueryParams = z.infer<
|
||||
typeof ObservationQueryParamsSchema
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ class BackupLLMSettingsMixin:
|
|||
both fields are set together or both are None.
|
||||
"""
|
||||
|
||||
BACKUP_PROVIDER: SupportedProviders | None = None
|
||||
BACKUP_MODEL: str | None = None
|
||||
BACKUP_PROVIDER: SupportedProviders | None = "custom"
|
||||
BACKUP_MODEL: str | None = "x-ai/grok-4-fast"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_backup_configuration(self):
|
||||
|
|
@ -236,7 +236,7 @@ class DeriverSettings(BackupLLMSettingsMixin, 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
|
||||
|
||||
# Maximum number of observations to store in working representation
|
||||
# Maximum number of observations to return in working representation
|
||||
# This is applied to both explicit and deductive observations
|
||||
WORKING_REPRESENTATION_MAX_OBSERVATIONS: Annotated[
|
||||
int, Field(default=50, gt=0, le=500)
|
||||
|
|
@ -330,7 +330,7 @@ class CacheSettings(HonchoSettings):
|
|||
model_config = SettingsConfigDict(env_prefix="CACHE_", extra="ignore") # pyright: ignore
|
||||
|
||||
ENABLED: bool = False
|
||||
URL: str = "redis://localhost:6379/0?suppress=false"
|
||||
URL: str = "redis://localhost:6379/0?suppress=true"
|
||||
NAMESPACE: str | None = None
|
||||
DEFAULT_TTL_SECONDS: Annotated[int, Field(default=300, ge=1, le=86_400)] = (
|
||||
300 # how long to keep items in cache
|
||||
|
|
@ -351,8 +351,8 @@ class DreamSettings(BackupLLMSettingsMixin, HonchoSettings):
|
|||
ENABLED_TYPES: list[str] = ["consolidate"]
|
||||
|
||||
# LLM settings for dream processing
|
||||
PROVIDER: SupportedProviders = "openai"
|
||||
MODEL: str = "gpt-4o-mini-2024-07-18"
|
||||
PROVIDER: SupportedProviders = "google"
|
||||
MODEL: str = "gemini-2.5-flash"
|
||||
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2000, gt=0, le=10_000)] = 2000
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ from .collection import get_collection, get_or_create_collection
|
|||
from .deriver import get_deriver_status
|
||||
from .document import (
|
||||
create_documents,
|
||||
create_observations,
|
||||
delete_document,
|
||||
delete_document_by_id,
|
||||
get_all_documents,
|
||||
get_documents_with_filters,
|
||||
query_documents,
|
||||
)
|
||||
from .message import (
|
||||
|
|
@ -47,6 +51,7 @@ from .workspace import (
|
|||
delete_workspace,
|
||||
get_all_workspaces,
|
||||
get_or_create_workspace,
|
||||
get_workspace,
|
||||
update_workspace,
|
||||
)
|
||||
|
||||
|
|
@ -58,8 +63,12 @@ __all__ = [
|
|||
"get_deriver_status",
|
||||
# Document
|
||||
"create_documents",
|
||||
"create_observations",
|
||||
"get_all_documents",
|
||||
"get_documents_with_filters",
|
||||
"query_documents",
|
||||
"delete_document",
|
||||
"delete_document_by_id",
|
||||
# Message
|
||||
"create_messages",
|
||||
"get_messages",
|
||||
|
|
@ -98,6 +107,7 @@ __all__ = [
|
|||
# Workspace
|
||||
"delete_workspace",
|
||||
"get_or_create_workspace",
|
||||
"get_workspace",
|
||||
"get_all_workspaces",
|
||||
"update_workspace",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -98,10 +98,12 @@ def _build_queue_status_query(
|
|||
models.QueueItem.work_unit_key == models.ActiveQueueSession.work_unit_key,
|
||||
)
|
||||
|
||||
stmt = stmt.join(models.Session, models.QueueItem.session_id == models.Session.id)
|
||||
stmt = stmt.where(models.Session.workspace_name == workspace_name)
|
||||
stmt = stmt.where(models.QueueItem.workspace_name == workspace_name)
|
||||
|
||||
if session_name is not None:
|
||||
stmt = stmt.join(
|
||||
models.Session, models.QueueItem.session_id == models.Session.id
|
||||
)
|
||||
stmt = stmt.where(models.Session.name == session_name)
|
||||
|
||||
peer_conditions = []
|
||||
|
|
|
|||
|
|
@ -2,42 +2,104 @@ from collections.abc import Sequence
|
|||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import Select
|
||||
|
||||
from src import models, schemas
|
||||
from src.config import settings
|
||||
from src.crud.collection import get_or_create_collection
|
||||
from src.crud.peer import get_peer
|
||||
from src.crud.session import get_session
|
||||
from src.embedding_client import embedding_client
|
||||
from src.exceptions import ValidationException
|
||||
from src.exceptions import ResourceNotFoundException, ValidationException
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
async def get_all_documents(
|
||||
db: AsyncSession,
|
||||
def get_all_documents(
|
||||
workspace_name: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
limit: int = 1000,
|
||||
) -> Sequence[models.Document]:
|
||||
filters: dict[str, Any] | None = None,
|
||||
reverse: bool = False,
|
||||
limit: int | None = None,
|
||||
) -> Select[tuple[models.Document]]:
|
||||
"""
|
||||
Get all documents in a collection.
|
||||
|
||||
NOTE: Order is nondeterministic. Also this may return a massive amount of documents. Don't use this on large collections.
|
||||
TODO: add pagination and update dreaming logic to deduplicate more effectively
|
||||
Returns a Select query for pagination support via apaginate().
|
||||
Results are ordered by created_at timestamp.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
observer: Name of the observing peer
|
||||
observed: Name of the observed peer
|
||||
filters: Optional filters to apply
|
||||
reverse: Whether to reverse the order (oldest first)
|
||||
|
||||
Returns:
|
||||
Select query for documents
|
||||
"""
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.limit(limit)
|
||||
.where(models.Document.workspace_name == workspace_name)
|
||||
.where(models.Document.observer == observer)
|
||||
.where(models.Document.observed == observed)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
# Apply additional filters if provided
|
||||
stmt = apply_filter(stmt, models.Document, filters)
|
||||
|
||||
# Order by created_at (newest first by default)
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Document.created_at.asc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Document.created_at.desc())
|
||||
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
def get_documents_with_filters(
|
||||
workspace_name: str,
|
||||
*,
|
||||
filters: dict[str, Any] | None = None,
|
||||
reverse: bool = False,
|
||||
) -> Select[tuple[models.Document]]:
|
||||
"""
|
||||
Get all documents using custom filters.
|
||||
|
||||
Returns a Select query for pagination support via apaginate().
|
||||
Results are ordered by created_at timestamp.
|
||||
|
||||
Args:
|
||||
workspace_name: Name of the workspace
|
||||
filters: Optional filters to apply
|
||||
reverse: Whether to reverse the order (oldest first)
|
||||
|
||||
Returns:
|
||||
Select query for documents
|
||||
"""
|
||||
stmt = select(models.Document).where(
|
||||
models.Document.workspace_name == workspace_name
|
||||
)
|
||||
|
||||
# Apply additional filters if provided
|
||||
stmt = apply_filter(stmt, models.Document, filters)
|
||||
|
||||
# Order by created_at (newest first by default)
|
||||
if reverse:
|
||||
stmt = stmt.order_by(models.Document.created_at.asc())
|
||||
else:
|
||||
stmt = stmt.order_by(models.Document.created_at.desc())
|
||||
|
||||
return stmt
|
||||
|
||||
|
||||
async def query_documents(
|
||||
|
|
@ -162,6 +224,174 @@ async def create_documents(
|
|||
return len(honcho_documents)
|
||||
|
||||
|
||||
async def delete_document(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
document_id: str,
|
||||
*,
|
||||
observer: str,
|
||||
observed: str,
|
||||
session_name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a single document by ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace
|
||||
document_id: ID of the document to delete
|
||||
observer: Name of the observing peer (for authorization)
|
||||
observed: Name of the observed peer (for authorization)
|
||||
session_name: Optional session name to verify document belongs to session
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If document not found or doesn't match criteria
|
||||
"""
|
||||
stmt = delete(models.Document).where(
|
||||
models.Document.id == document_id,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
models.Document.observer == observer,
|
||||
models.Document.observed == observed,
|
||||
)
|
||||
|
||||
# If session is specified, ensure document belongs to that session
|
||||
if session_name is not None:
|
||||
stmt = stmt.where(models.Document.session_name == session_name)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount == 0:
|
||||
raise ResourceNotFoundException(
|
||||
f"Document {document_id} not found or does not belong to the specified collection/session"
|
||||
)
|
||||
|
||||
|
||||
async def delete_document_by_id(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
document_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a single document by ID and workspace.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
workspace_name: Name of the workspace
|
||||
document_id: ID of the document to delete
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If document not found or doesn't belong to the workspace
|
||||
"""
|
||||
stmt = delete(models.Document).where(
|
||||
models.Document.id == document_id,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount == 0:
|
||||
raise ResourceNotFoundException(
|
||||
f"Document {document_id} not found or does not belong to workspace {workspace_name}"
|
||||
)
|
||||
|
||||
|
||||
async def create_observations(
|
||||
db: AsyncSession,
|
||||
observations: list[schemas.ObservationCreate],
|
||||
workspace_name: str,
|
||||
) -> list[models.Document]:
|
||||
"""
|
||||
Create multiple observations (documents) from user input.
|
||||
|
||||
This function validates all referenced resources, generates embeddings
|
||||
in batch, and creates the documents.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
observations: List of observation creation schemas
|
||||
workspace_name: Name of the workspace
|
||||
|
||||
Returns:
|
||||
List of created Document objects
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If any session or peer is not found
|
||||
ValidationException: If embedding generation fails or integrity constraint is violated
|
||||
"""
|
||||
if not observations:
|
||||
return []
|
||||
|
||||
# Collect unique sessions and peer pairs to validate
|
||||
sessions_to_validate: set[str] = set()
|
||||
peers_to_validate: set[str] = set()
|
||||
collection_pairs: set[tuple[str, str]] = set()
|
||||
|
||||
for obs in observations:
|
||||
sessions_to_validate.add(obs.session_id)
|
||||
peers_to_validate.add(obs.observer_id)
|
||||
peers_to_validate.add(obs.observed_id)
|
||||
collection_pairs.add((obs.observer_id, obs.observed_id))
|
||||
|
||||
# Validate all sessions exist
|
||||
for session_name in sessions_to_validate:
|
||||
await get_session(db, session_name, workspace_name)
|
||||
|
||||
# Validate all peers exist
|
||||
for peer_name in peers_to_validate:
|
||||
await get_peer(db, workspace_name, schemas.PeerCreate(name=peer_name))
|
||||
|
||||
# Get or create all collections
|
||||
for observer, observed in collection_pairs:
|
||||
await get_or_create_collection(
|
||||
db, workspace_name, observer=observer, observed=observed
|
||||
)
|
||||
|
||||
# Generate embeddings in batch
|
||||
contents = [obs.content for obs in observations]
|
||||
try:
|
||||
embeddings = await embedding_client.simple_batch_embed(contents)
|
||||
except ValueError as e:
|
||||
raise ValidationException(str(e)) from e
|
||||
|
||||
# Create document objects
|
||||
honcho_documents: list[models.Document] = []
|
||||
for obs, embedding in zip(observations, embeddings, strict=True):
|
||||
honcho_documents.append(
|
||||
models.Document(
|
||||
workspace_name=workspace_name,
|
||||
observer=obs.observer_id,
|
||||
observed=obs.observed_id,
|
||||
content=obs.content,
|
||||
level="explicit", # Manually created observations are always explicit
|
||||
times_derived=1,
|
||||
internal_metadata={}, # No message_ids since not derived from messages
|
||||
embedding=embedding,
|
||||
session_name=obs.session_id,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
db.add_all(honcho_documents)
|
||||
await db.commit()
|
||||
# Refresh all documents to get generated IDs and timestamps
|
||||
for doc in honcho_documents:
|
||||
await db.refresh(doc)
|
||||
except IntegrityError as e:
|
||||
await db.rollback()
|
||||
raise ValidationException(
|
||||
"Failed to create observations due to integrity constraint violation"
|
||||
) from e
|
||||
|
||||
logger.debug(
|
||||
"Created %d observations in workspace %s",
|
||||
len(honcho_documents),
|
||||
workspace_name,
|
||||
)
|
||||
return honcho_documents
|
||||
|
||||
|
||||
async def is_rejected_duplicate(
|
||||
db: AsyncSession,
|
||||
doc: schemas.DocumentCreate,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from src.cache.client import cache, get_cache_namespace
|
|||
from src.config import settings
|
||||
from src.crud.workspace import get_or_create_workspace
|
||||
from src.exceptions import ConflictException, ResourceNotFoundException
|
||||
from src.models import Peer
|
||||
from src.utils.filter import apply_filter
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
|
@ -63,21 +64,37 @@ async def get_or_create_peers(
|
|||
.where(models.Peer.name.in_(peer_names))
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing_peers = list(result.scalars().all())
|
||||
existing_peers: list[Peer] = list(result.scalars().all())
|
||||
|
||||
# Create a mapping of peer names to peer schemas for easy lookup
|
||||
peer_schema_map = {p.name: p for p in peers}
|
||||
|
||||
# Track which peers actually changed
|
||||
changed_peers: list[Peer] = []
|
||||
|
||||
# Update existing peers with metadata and configuration if provided
|
||||
for existing_peer in existing_peers:
|
||||
peer_schema = peer_schema_map[existing_peer.name]
|
||||
changed = False
|
||||
|
||||
# Update with metadata and configuration if provided
|
||||
if peer_schema.metadata is not None:
|
||||
# Update with metadata if provided AND different
|
||||
if (
|
||||
peer_schema.metadata is not None
|
||||
and existing_peer.h_metadata != peer_schema.metadata
|
||||
):
|
||||
existing_peer.h_metadata = peer_schema.metadata
|
||||
changed = True
|
||||
|
||||
if peer_schema.configuration is not None:
|
||||
# Update with configuration if provided AND different
|
||||
if (
|
||||
peer_schema.configuration is not None
|
||||
and existing_peer.configuration != peer_schema.configuration
|
||||
):
|
||||
existing_peer.configuration = peer_schema.configuration
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
changed_peers.append(existing_peer)
|
||||
|
||||
# Find which peers need to be created
|
||||
existing_names = {p.name for p in existing_peers}
|
||||
|
|
@ -104,10 +121,15 @@ async def get_or_create_peers(
|
|||
) from None
|
||||
return await get_or_create_peers(db, workspace_name, peers, _retry=True)
|
||||
|
||||
# Invalidate cache for all updated/created peers - read-through pattern
|
||||
for peer_obj in existing_peers + new_peers:
|
||||
# Only invalidate cache for changed/new peers - read-through pattern
|
||||
for peer_obj in changed_peers + new_peers:
|
||||
cache_key = peer_cache_key(workspace_name, peer_obj.name)
|
||||
await cache.delete(cache_key)
|
||||
logger.debug(
|
||||
"Peer %s cache invalidated in workspace %s (changed or new)",
|
||||
peer_obj.name,
|
||||
workspace_name,
|
||||
)
|
||||
|
||||
# Return combined list of existing and new peers
|
||||
return existing_peers + new_peers
|
||||
|
|
@ -150,7 +172,7 @@ async def get_peer(
|
|||
peer: Peer creation schema
|
||||
|
||||
Returns:
|
||||
The peer if found or created
|
||||
The peer if found
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the peer does not exist
|
||||
|
|
@ -204,16 +226,31 @@ async def update_peer(
|
|||
)
|
||||
)[0]
|
||||
|
||||
if peer.metadata is not None:
|
||||
honcho_peer.h_metadata = peer.metadata
|
||||
needs_update = False
|
||||
|
||||
if peer.configuration is not None:
|
||||
if peer.metadata is not None and honcho_peer.h_metadata != peer.metadata:
|
||||
honcho_peer.h_metadata = peer.metadata
|
||||
needs_update = True
|
||||
|
||||
if (
|
||||
peer.configuration is not None
|
||||
and honcho_peer.configuration != peer.configuration
|
||||
):
|
||||
honcho_peer.configuration = peer.configuration
|
||||
needs_update = True
|
||||
|
||||
# Early exit if unchanged
|
||||
if not needs_update:
|
||||
logger.debug(
|
||||
"Peer %s unchanged in workspace %s, skipping update",
|
||||
peer_name,
|
||||
workspace_name,
|
||||
)
|
||||
return honcho_peer
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(honcho_peer)
|
||||
|
||||
# Invalidate cache - read-through pattern
|
||||
cache_key = peer_cache_key(workspace_name, honcho_peer.name)
|
||||
await cache.delete(cache_key)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from src.config import settings
|
|||
from src.dependencies import tracked_db
|
||||
from src.dreamer.dream_scheduler import check_and_schedule_dream
|
||||
from src.embedding_client import embedding_client
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
from src.utils.logging import accumulate_metric
|
||||
from src.utils.representation import (
|
||||
|
|
@ -44,9 +45,10 @@ class RepresentationManager:
|
|||
async def save_representation(
|
||||
self,
|
||||
representation: Representation,
|
||||
message_id_range: tuple[int, int],
|
||||
message_ids: list[int],
|
||||
session_name: str,
|
||||
message_created_at: datetime.datetime,
|
||||
message_level_configuration: ResolvedConfiguration,
|
||||
) -> int:
|
||||
"""
|
||||
Save Representation objects to the collection as a set of documents.
|
||||
|
|
@ -85,7 +87,7 @@ class RepresentationManager:
|
|||
|
||||
batch_embed_duration = (time.perf_counter() - batch_embed_start) * 1000
|
||||
accumulate_metric(
|
||||
f"deriver_{message_id_range[1]}_{self.observer}",
|
||||
f"deriver_{message_ids[-1]}_{self.observer}",
|
||||
"embed_new_observations",
|
||||
batch_embed_duration,
|
||||
"ms",
|
||||
|
|
@ -98,14 +100,15 @@ class RepresentationManager:
|
|||
db,
|
||||
all_observations,
|
||||
embeddings,
|
||||
message_id_range,
|
||||
message_ids,
|
||||
session_name,
|
||||
message_created_at,
|
||||
message_level_configuration,
|
||||
)
|
||||
|
||||
create_document_duration = (time.perf_counter() - create_document_start) * 1000
|
||||
accumulate_metric(
|
||||
f"deriver_{message_id_range[1]}_{self.observer}",
|
||||
f"deriver_{message_ids[-1]}_{self.observer}",
|
||||
"save_new_observations",
|
||||
create_document_duration,
|
||||
"ms",
|
||||
|
|
@ -118,9 +121,10 @@ class RepresentationManager:
|
|||
db: AsyncSession,
|
||||
all_observations: list[ExplicitObservation | DeductiveObservation],
|
||||
embeddings: list[list[float]],
|
||||
message_id_range: tuple[int, int],
|
||||
message_ids: list[int],
|
||||
session_name: str,
|
||||
message_created_at: datetime.datetime,
|
||||
message_level_configuration: ResolvedConfiguration,
|
||||
) -> int:
|
||||
# get_or_create_collection already handles IntegrityError with rollback and a retry
|
||||
collection = await crud.get_or_create_collection(
|
||||
|
|
@ -144,7 +148,7 @@ class RepresentationManager:
|
|||
obs_premises = None
|
||||
|
||||
metadata: schemas.DocumentMetadata = schemas.DocumentMetadata(
|
||||
message_ids=[message_id_range],
|
||||
message_ids=message_ids,
|
||||
premises=obs_premises,
|
||||
message_created_at=format_datetime_utc(message_created_at),
|
||||
)
|
||||
|
|
@ -169,10 +173,11 @@ class RepresentationManager:
|
|||
deduplicate=settings.DERIVER.DEDUPLICATE,
|
||||
)
|
||||
|
||||
try:
|
||||
await check_and_schedule_dream(db, collection)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check dream scheduling: {e}")
|
||||
if message_level_configuration.dream.enabled:
|
||||
try:
|
||||
await check_and_schedule_dream(db, collection)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check dream scheduling: {e}")
|
||||
|
||||
return new_documents
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any
|
|||
|
||||
from cashews import NOT_NONE
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import Select, case, cast, func, insert, select, update
|
||||
from sqlalchemy import Select, and_, case, cast, delete, func, insert, select, update
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -83,9 +83,13 @@ async def get_sessions(
|
|||
filters: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Session]]:
|
||||
"""
|
||||
Get all sessions in a workspace.
|
||||
Get all active sessions in a workspace.
|
||||
"""
|
||||
stmt = select(models.Session).where(models.Session.workspace_name == workspace_name)
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.where(models.Session.workspace_name == workspace_name)
|
||||
.where(models.Session.is_active == True) # noqa: E712
|
||||
)
|
||||
|
||||
stmt = apply_filter(stmt, models.Session, filters)
|
||||
|
||||
|
|
@ -126,6 +130,15 @@ async def get_or_create_session(
|
|||
if honcho_session is not None:
|
||||
honcho_session = await db.merge(honcho_session, load=False)
|
||||
|
||||
# Reject operations on inactive sessions (marked for deletion)
|
||||
if not honcho_session.is_active:
|
||||
raise ResourceNotFoundException(
|
||||
f"Session {session.name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Track if we need to update cache
|
||||
needs_cache_update = False
|
||||
|
||||
# Check if session already exists
|
||||
if honcho_session is None:
|
||||
if session.peer_names:
|
||||
|
|
@ -145,12 +158,16 @@ async def get_or_create_session(
|
|||
workspace_name=workspace_name,
|
||||
name=session.name,
|
||||
h_metadata=session.metadata or {},
|
||||
configuration=session.configuration or {},
|
||||
configuration=session.configuration.model_dump(exclude_none=True)
|
||||
if session.configuration
|
||||
else {},
|
||||
)
|
||||
try:
|
||||
db.add(honcho_session)
|
||||
# Flush to ensure session exists in DB before adding peers
|
||||
# Flush to ensure session exists in DB before adding peers and set flag to warm cache
|
||||
await db.flush()
|
||||
needs_cache_update = True
|
||||
|
||||
except IntegrityError:
|
||||
await db.rollback()
|
||||
logger.debug(
|
||||
|
|
@ -163,10 +180,20 @@ async def get_or_create_session(
|
|||
return await get_or_create_session(db, session, workspace_name, _retry=True)
|
||||
else:
|
||||
# Update existing session with metadata and feature flags if provided
|
||||
if session.metadata is not None:
|
||||
if (
|
||||
session.metadata is not None
|
||||
and honcho_session.h_metadata != session.metadata
|
||||
):
|
||||
honcho_session.h_metadata = session.metadata
|
||||
needs_cache_update = True
|
||||
if session.configuration is not None:
|
||||
honcho_session.configuration = session.configuration
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
existing_config = (honcho_session.configuration or {}).copy()
|
||||
incoming_config = session.configuration.model_dump(exclude_none=True)
|
||||
merged_config = {**existing_config, **incoming_config}
|
||||
if honcho_session.configuration != merged_config:
|
||||
honcho_session.configuration = merged_config
|
||||
needs_cache_update = True
|
||||
|
||||
# Add all peers to session
|
||||
if session.peer_names:
|
||||
|
|
@ -187,10 +214,16 @@ async def get_or_create_session(
|
|||
await db.commit()
|
||||
await db.refresh(honcho_session)
|
||||
|
||||
cache_key = session_cache_key(workspace_name, session.name)
|
||||
await cache.set(
|
||||
cache_key, honcho_session, expire=settings.CACHE.DEFAULT_TTL_SECONDS
|
||||
)
|
||||
# Only update cache if session data changed or was newly created
|
||||
if needs_cache_update:
|
||||
cache_key = session_cache_key(workspace_name, session.name)
|
||||
await cache.set(
|
||||
cache_key, honcho_session, expire=settings.CACHE.DEFAULT_TTL_SECONDS
|
||||
)
|
||||
logger.debug(
|
||||
"Session %s cache updated in workspace %s", session.name, workspace_name
|
||||
)
|
||||
|
||||
return honcho_session
|
||||
|
||||
|
||||
|
|
@ -198,6 +231,8 @@ async def get_session(
|
|||
db: AsyncSession,
|
||||
session_name: str,
|
||||
workspace_name: str,
|
||||
*,
|
||||
include_inactive: bool = False,
|
||||
) -> models.Session:
|
||||
"""
|
||||
Get a session in a workspace.
|
||||
|
|
@ -206,12 +241,14 @@ async def get_session(
|
|||
db: Database session
|
||||
session_name: Name of the session
|
||||
workspace_name: Name of the workspace
|
||||
include_inactive: If True, return sessions even if they are marked for deletion.
|
||||
This should only be used for internal operations like the deletion task.
|
||||
|
||||
Returns:
|
||||
The session
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If the session does not exist
|
||||
ResourceNotFoundException: If the session does not exist or is inactive
|
||||
"""
|
||||
session = await _fetch_session(db, workspace_name, session_name)
|
||||
|
||||
|
|
@ -220,6 +257,12 @@ async def get_session(
|
|||
f"Session {session_name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Check if session is active (unless include_inactive is True)
|
||||
if not include_inactive and not session.is_active:
|
||||
raise ResourceNotFoundException(
|
||||
f"Session {session_name} not found in workspace {workspace_name}"
|
||||
)
|
||||
|
||||
# Merge cached object into session (cached objects are detached)
|
||||
session = await db.merge(session, load=False)
|
||||
|
||||
|
|
@ -251,16 +294,36 @@ async def update_session(
|
|||
db, schemas.SessionCreate(name=session_name), workspace_name=workspace_name
|
||||
)
|
||||
|
||||
if session.metadata is not None:
|
||||
# Track if anything changed
|
||||
needs_update = False
|
||||
|
||||
if session.metadata is not None and honcho_session.h_metadata != session.metadata:
|
||||
honcho_session.h_metadata = session.metadata
|
||||
needs_update = True
|
||||
|
||||
if session.configuration is not None:
|
||||
honcho_session.configuration = session.configuration
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
base_config = (honcho_session.configuration or {}).copy()
|
||||
merged_config = {
|
||||
**base_config,
|
||||
**session.configuration.model_dump(exclude_none=True),
|
||||
}
|
||||
if honcho_session.configuration != merged_config:
|
||||
honcho_session.configuration = merged_config
|
||||
needs_update = True
|
||||
|
||||
if not needs_update:
|
||||
logger.debug(
|
||||
"Session %s unchanged in workspace %s, skipping update",
|
||||
session_name,
|
||||
workspace_name,
|
||||
)
|
||||
return honcho_session
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(honcho_session)
|
||||
|
||||
# Invalidate cache - read-through pattern
|
||||
# Only invalidate if we actually updated
|
||||
cache_key = session_cache_key(workspace_name, session_name)
|
||||
await cache.delete(cache_key)
|
||||
|
||||
|
|
@ -268,11 +331,56 @@ async def update_session(
|
|||
return honcho_session
|
||||
|
||||
|
||||
async def _batch_delete_matching(
|
||||
db: AsyncSession,
|
||||
model: Any,
|
||||
filter_conditions: list[Any],
|
||||
batch_size: int = 5000,
|
||||
) -> int:
|
||||
"""
|
||||
Delete records in batches that match the given filter conditions.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
model: SQLAlchemy model class
|
||||
filter_conditions: List of SQLAlchemy filter conditions
|
||||
batch_size: Number of records to delete per batch
|
||||
|
||||
Returns:
|
||||
Total number of records deleted
|
||||
"""
|
||||
total_deleted = 0
|
||||
primary_key_column = model.__table__.primary_key.columns.values()[0]
|
||||
|
||||
while True:
|
||||
subquery = (
|
||||
select(primary_key_column).where(and_(*filter_conditions)).limit(batch_size)
|
||||
)
|
||||
delete_stmt = delete(model).where(primary_key_column.in_(subquery))
|
||||
delete_result = await db.execute(delete_stmt)
|
||||
batch_deleted = delete_result.rowcount or 0
|
||||
total_deleted += batch_deleted
|
||||
|
||||
if batch_deleted == 0:
|
||||
break
|
||||
|
||||
return total_deleted
|
||||
|
||||
|
||||
async def delete_session(
|
||||
db: AsyncSession, workspace_name: str, session_name: str
|
||||
) -> bool:
|
||||
"""
|
||||
Mark a session as inactive (soft delete).
|
||||
Delete a session and all associated data (hard delete).
|
||||
|
||||
This performs cascading deletes for all session-related data including:
|
||||
- Active queue sessions
|
||||
- Queue items
|
||||
- Message embeddings (batched)
|
||||
- Documents (theory-of-mind data, batched)
|
||||
- Messages (batched)
|
||||
- Session peer associations
|
||||
- The session itself
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
|
@ -285,17 +393,83 @@ async def delete_session(
|
|||
Raises:
|
||||
ResourceNotFoundException: If the session does not exist
|
||||
"""
|
||||
honcho_session = await get_session(db, session_name, workspace_name)
|
||||
honcho_session = await get_session(
|
||||
db, session_name, workspace_name, include_inactive=True
|
||||
)
|
||||
|
||||
honcho_session.is_active = False
|
||||
await db.commit()
|
||||
await db.refresh(honcho_session)
|
||||
# Perform cascading deletes in order
|
||||
# Order is important to avoid foreign key constraint violations
|
||||
try:
|
||||
# Delete ActiveQueueSession entries
|
||||
# Work unit keys have format: {task_type}:{workspace_name}:{session_name}:{...}
|
||||
await db.execute(
|
||||
delete(models.ActiveQueueSession).where(
|
||||
and_(
|
||||
func.split_part(models.ActiveQueueSession.work_unit_key, ":", 2)
|
||||
== workspace_name,
|
||||
func.split_part(models.ActiveQueueSession.work_unit_key, ":", 3)
|
||||
== session_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Invalidate cache - read-through pattern
|
||||
cache_key = session_cache_key(workspace_name, session_name)
|
||||
await cache.delete(cache_key)
|
||||
# Delete QueueItem entries
|
||||
await db.execute(
|
||||
delete(models.QueueItem).where(
|
||||
models.QueueItem.session_id == honcho_session.id
|
||||
)
|
||||
)
|
||||
|
||||
# Delete MessageEmbedding entries in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.MessageEmbedding,
|
||||
[
|
||||
models.MessageEmbedding.session_name == session_name,
|
||||
models.MessageEmbedding.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete Document entries associated with this session in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.Document,
|
||||
[
|
||||
models.Document.session_name == session_name,
|
||||
models.Document.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete Message entries in batches
|
||||
await _batch_delete_matching(
|
||||
db,
|
||||
models.Message,
|
||||
[
|
||||
models.Message.session_name == session_name,
|
||||
models.Message.workspace_name == workspace_name,
|
||||
],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
# Delete SessionPeer associations
|
||||
await db.execute(
|
||||
delete(models.SessionPeer).where(
|
||||
models.SessionPeer.session_name == session_name,
|
||||
models.SessionPeer.workspace_name == workspace_name,
|
||||
)
|
||||
)
|
||||
|
||||
# Finally, delete the session itself
|
||||
await db.delete(honcho_session)
|
||||
await db.commit()
|
||||
logger.debug("Session %s and all associated data deleted", session_name)
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete session %s: %s", session_name, e)
|
||||
await db.rollback()
|
||||
raise e
|
||||
|
||||
logger.debug("Session %s marked as inactive", session_name)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -306,9 +480,17 @@ async def clone_session(
|
|||
cutoff_message_id: str | None = None,
|
||||
) -> models.Session:
|
||||
"""
|
||||
Clone a session and its messages. If cutoff_message_id is provided,
|
||||
Clone a session and its data. If cutoff_message_id is provided,
|
||||
only clone messages up to and including that message.
|
||||
|
||||
The following data is copied to the new session:
|
||||
- Session metadata
|
||||
- Session configuration
|
||||
- All messages (or up to cutoff_message_id) with their content, metadata, and peer associations
|
||||
- Session-peer associations with their configurations (observe_me, observe_others)
|
||||
|
||||
The new session gets a unique ID (nanoid) and fresh timestamps.
|
||||
|
||||
Args:
|
||||
db: SQLAlchemy session
|
||||
workspace_name: Name of the workspace the target session is in
|
||||
|
|
@ -318,11 +500,12 @@ async def clone_session(
|
|||
Returns:
|
||||
The newly created session
|
||||
"""
|
||||
# Get the original session
|
||||
# Get the original session (must be active)
|
||||
stmt = (
|
||||
select(models.Session)
|
||||
.where(models.Session.workspace_name == workspace_name)
|
||||
.where(models.Session.name == original_session_name)
|
||||
.where(models.Session.is_active == True) # noqa: E712
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
original_session = result.scalar_one_or_none()
|
||||
|
|
@ -347,6 +530,7 @@ async def clone_session(
|
|||
workspace_name=workspace_name,
|
||||
name=generate_nanoid(),
|
||||
h_metadata=original_session.h_metadata,
|
||||
configuration=original_session.configuration,
|
||||
)
|
||||
db.add(new_session)
|
||||
await db.flush() # Flush to get the new session ID
|
||||
|
|
@ -382,7 +566,7 @@ async def clone_session(
|
|||
insert_stmt = insert(models.Message).returning(models.Message)
|
||||
result = await db.execute(insert_stmt, new_messages)
|
||||
|
||||
# Clone peers from original session to new session
|
||||
# Clone peers from original session to new session (including their configurations)
|
||||
stmt = select(models.SessionPeer).where(
|
||||
models.SessionPeer.session_name == original_session_name
|
||||
)
|
||||
|
|
@ -393,6 +577,7 @@ async def clone_session(
|
|||
session_name=new_session.name,
|
||||
peer_name=session_peer.peer_name,
|
||||
workspace_name=workspace_name,
|
||||
configuration=session_peer.configuration,
|
||||
)
|
||||
db.add(new_session_peer)
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ async def get_or_create_workspace(
|
|||
honcho_workspace = models.Workspace(
|
||||
name=workspace.name,
|
||||
h_metadata=workspace.metadata,
|
||||
configuration=workspace.configuration,
|
||||
configuration=workspace.configuration.model_dump(exclude_none=True),
|
||||
)
|
||||
try:
|
||||
db.add(honcho_workspace)
|
||||
|
|
@ -172,16 +172,36 @@ async def update_workspace(
|
|||
),
|
||||
)
|
||||
|
||||
if workspace.metadata is not None:
|
||||
# Track if anything changed
|
||||
needs_update = False
|
||||
|
||||
if (
|
||||
workspace.metadata is not None
|
||||
and honcho_workspace.h_metadata != workspace.metadata
|
||||
):
|
||||
honcho_workspace.h_metadata = workspace.metadata
|
||||
needs_update = True
|
||||
|
||||
if workspace.configuration is not None:
|
||||
honcho_workspace.configuration = workspace.configuration
|
||||
# Merge configuration instead of replacing to preserve existing keys
|
||||
base_config = (honcho_workspace.configuration or {}).copy()
|
||||
merged_config = {
|
||||
**base_config,
|
||||
**workspace.configuration.model_dump(exclude_none=True),
|
||||
}
|
||||
if honcho_workspace.configuration != merged_config:
|
||||
honcho_workspace.configuration = merged_config
|
||||
needs_update = True
|
||||
|
||||
# Early exit if unchanged
|
||||
if not needs_update:
|
||||
logger.debug("Workspace %s unchanged, skipping update", workspace_name)
|
||||
return honcho_workspace
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(honcho_workspace)
|
||||
|
||||
# Invalidate cache
|
||||
# Only invalidate if we actually updated
|
||||
cache_key = workspace_cache_key(workspace_name)
|
||||
await cache.delete(cache_key)
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue